├── app ├── .npmrc ├── .prettierignore ├── public │ ├── loading.gif │ ├── open-meteo.jpeg │ ├── how-it-works-x.jpg │ ├── how-it-works-meteo.png │ ├── arrow-right.svg │ ├── chainlink.svg │ ├── chevron-down.svg │ ├── arrow-go-to-up.svg │ ├── arrow-go-to-up-blue.svg │ ├── x.svg │ ├── angle.svg │ ├── code.svg │ ├── dev-expert.svg │ ├── github.svg │ ├── external.svg │ ├── menu.svg │ ├── external-muted.svg │ ├── close.svg │ ├── refresh.svg │ ├── onchain.svg │ ├── globe.svg │ ├── charger.svg │ └── rain.svg ├── src │ ├── app │ │ ├── favicon.ico │ │ ├── opengraph-image.png │ │ ├── twitter-image.png │ │ ├── x │ │ │ ├── twitter-image.png │ │ │ ├── opengraph-image.png │ │ │ ├── layout.tsx │ │ │ ├── offchain-response.tsx │ │ │ ├── onchain-data.tsx │ │ │ ├── history.tsx │ │ │ └── page.tsx │ │ ├── open-meteo │ │ │ ├── twitter-image.png │ │ │ ├── opengraph-image.png │ │ │ ├── layout.tsx │ │ │ ├── offchain-response.tsx │ │ │ ├── onchain-data.tsx │ │ │ └── history.tsx │ │ ├── api │ │ │ ├── geolocation │ │ │ │ └── route.ts │ │ │ ├── onchain-tweet │ │ │ │ └── route.ts │ │ │ └── onchain-weather │ │ │ │ └── route.ts │ │ └── layout.tsx │ ├── lib │ │ ├── utils.ts │ │ ├── fonts.ts │ │ ├── fetch-geocoding.ts │ │ ├── fetch-weather.ts │ │ ├── history.ts │ │ ├── request-onchain.ts │ │ └── fetch-tweet.ts │ ├── components │ │ ├── google-tag.tsx │ │ ├── ui │ │ │ ├── collapsible.tsx │ │ │ ├── input.tsx │ │ │ ├── popover.tsx │ │ │ ├── alert.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── table.tsx │ │ │ └── dialog.tsx │ │ ├── loading-spinner.tsx │ │ ├── site-header.tsx │ │ ├── main-nav.tsx │ │ ├── handle-input.tsx │ │ ├── mobile-nav.tsx │ │ ├── city-input.tsx │ │ └── autocomplete.tsx │ ├── hooks │ │ └── useDebounce.ts │ ├── config │ │ └── site.tsx │ ├── styles │ │ └── globals.css │ └── types.ts ├── postcss.config.js ├── .prettierrc ├── .env.example ├── next.config.js ├── .eslintrc.json ├── .gitignore ├── tsconfig.json ├── package.json └── tailwind.config.js ├── contracts ├── .npmignore ├── .solhintignore ├── contracts │ ├── test │ │ ├── LinkToken.sol │ │ └── MockV3Aggregator.sol │ ├── FunctionsConsumer.sol │ ├── WeatherConsumer.sol │ └── AutomatedFunctionsConsumer.sol ├── .solhint.json ├── tasks │ ├── utils │ │ ├── index.js │ │ ├── spin.js │ │ ├── network.js │ │ ├── prompt.js │ │ ├── logger.js │ │ └── price.js │ ├── block-number.js │ ├── index.js │ ├── Functions-billing │ │ ├── index.js │ │ ├── info.js │ │ ├── add.js │ │ ├── cancel.js │ │ ├── remove.js │ │ ├── transfer.js │ │ ├── accept.js │ │ ├── fund.js │ │ ├── create.js │ │ └── timeoutRequests.js │ ├── balance.js │ ├── Functions-consumer │ │ ├── requestUserInfo.js │ │ ├── requestLastUserTweet.js │ │ ├── index.js │ │ ├── checkUpkeep.js │ │ ├── setDonId.js │ │ ├── listDonSecrets.js │ │ ├── performManualUpkeep.js │ │ ├── readResultAndError.js │ │ ├── buildOffchainSecrets.js │ │ ├── deployXConsumer.js │ │ ├── uploadSecretsToDon.js │ │ ├── deployConsumer.js │ │ ├── deployAutoConsumer.js │ │ └── deployWeatherConsumer.js │ └── simulateScript.js ├── x-user-info.js ├── x-last-tweets.js ├── test │ └── unit │ │ └── FunctionsConsumer.spec.js ├── weather-api.js ├── LICENSE ├── calculation-example.js ├── Functions-request-config.js ├── scripts │ ├── listen.js │ └── startLocalFunctionsTestnet.js ├── hardhat.config.js ├── package.json ├── .gitignore └── API-request-example.js ├── .vscode └── settings.json ├── .github └── workflows │ ├── deploy.sh │ ├── get-version-number.sh │ └── deploy.yaml ├── package.json └── LICENSE /app/.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies = false -------------------------------------------------------------------------------- /app/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | -------------------------------------------------------------------------------- /contracts/.npmignore: -------------------------------------------------------------------------------- 1 | hardhat.config.js 2 | scripts 3 | test 4 | -------------------------------------------------------------------------------- /contracts/.solhintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | contracts/test 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "solidity.compileUsingRemoteVersion": "v0.8.19+commit.7dd6d404" 3 | } -------------------------------------------------------------------------------- /app/public/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/run-functions-dapp/HEAD/app/public/loading.gif -------------------------------------------------------------------------------- /app/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/run-functions-dapp/HEAD/app/src/app/favicon.ico -------------------------------------------------------------------------------- /app/public/open-meteo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/run-functions-dapp/HEAD/app/public/open-meteo.jpeg -------------------------------------------------------------------------------- /app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /app/public/how-it-works-x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/run-functions-dapp/HEAD/app/public/how-it-works-x.jpg -------------------------------------------------------------------------------- /app/src/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/run-functions-dapp/HEAD/app/src/app/opengraph-image.png -------------------------------------------------------------------------------- /app/src/app/twitter-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/run-functions-dapp/HEAD/app/src/app/twitter-image.png -------------------------------------------------------------------------------- /app/src/app/x/twitter-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/run-functions-dapp/HEAD/app/src/app/x/twitter-image.png -------------------------------------------------------------------------------- /app/public/how-it-works-meteo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/run-functions-dapp/HEAD/app/public/how-it-works-meteo.png -------------------------------------------------------------------------------- /app/src/app/x/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/run-functions-dapp/HEAD/app/src/app/x/opengraph-image.png -------------------------------------------------------------------------------- /.github/workflows/deploy.sh: -------------------------------------------------------------------------------- 1 | cd ../contracts 2 | npm install 3 | npx hardhat functions-upload-secrets-don --slotid 0 --network avalancheFuji --ttl 4320 -------------------------------------------------------------------------------- /app/src/app/open-meteo/twitter-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/run-functions-dapp/HEAD/app/src/app/open-meteo/twitter-image.png -------------------------------------------------------------------------------- /app/src/app/open-meteo/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/run-functions-dapp/HEAD/app/src/app/open-meteo/opengraph-image.png -------------------------------------------------------------------------------- /contracts/contracts/test/LinkToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.4.24; 3 | 4 | import "@chainlink/contracts/src/v0.4/LinkToken.sol"; 5 | -------------------------------------------------------------------------------- /contracts/contracts/test/MockV3Aggregator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.7.0; 3 | 4 | import "@chainlink/contracts/src/v0.7/tests/MockV3Aggregator.sol"; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "run-functions-dapp", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "vercel-build": "cd app && next build" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "endOfLine": "lf", 4 | "printWidth": 80, 5 | "semi": false, 6 | "singleQuote": true, 7 | "tabWidth": 2, 8 | "trailingComma": "all" 9 | } 10 | -------------------------------------------------------------------------------- /contracts/.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "compiler-version": ["warn", "^0.8.19"], 5 | "func-visibility": ["warn", { "ignoreConstructors": true }] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /contracts/tasks/utils/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("./network.js"), 3 | ...require("./price.js"), 4 | ...require("./prompt.js"), 5 | ...require("./spin.js"), 6 | ...require("./logger.js"), 7 | } 8 | -------------------------------------------------------------------------------- /contracts/tasks/block-number.js: -------------------------------------------------------------------------------- 1 | task("block-number", "Prints the current block number", async (_, { ethers }) => { 2 | await ethers.provider.getBlockNumber().then((blockNumber) => { 3 | console.log("Current block number: " + blockNumber) 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /contracts/tasks/utils/spin.js: -------------------------------------------------------------------------------- 1 | const ora = require("ora") 2 | 3 | function spin(config = {}) { 4 | const spinner = ora({ spinner: "dots2", ...config }) 5 | spinner.start() 6 | return spinner 7 | } 8 | 9 | module.exports = { 10 | spin, 11 | } 12 | -------------------------------------------------------------------------------- /app/src/lib/fonts.ts: -------------------------------------------------------------------------------- 1 | import { Figtree, Fira_Code } from 'next/font/google' 2 | 3 | export const figtree = Figtree({ 4 | subsets: ['latin'], 5 | variable: '--font-sans', 6 | }) 7 | 8 | export const firaCode = Fira_Code({ 9 | subsets: ['latin'], 10 | variable: '--mono-space', 11 | }) 12 | -------------------------------------------------------------------------------- /app/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_GTM_ID= 2 | GEOCODING_API_KEY= 3 | X_BEARER_TOKEN= 4 | CONTRACT_ADDRESS_METEO= 5 | CONTRACT_ADDRESS_X= 6 | X_SECRET_VERSION_ID= 7 | NETWORK_RPC_URL= 8 | PRIVATE_KEY= 9 | KV_URL= 10 | KV_REST_API_URL= 11 | KV_REST_API_TOKEN= 12 | KV_REST_API_READ_ONLY_TOKEN= 13 | RATELIMIT_IP_EXCEPTION_LIST= -------------------------------------------------------------------------------- /app/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: false, 4 | images: { 5 | domains: ['pbs.twimg.com', 'abs.twimg.com'], 6 | }, 7 | webpack: (config) => { 8 | config.resolve.fallback = { fs: false, net: false, tls: false } 9 | return config 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /contracts/tasks/index.js: -------------------------------------------------------------------------------- 1 | //exports.keepers = require('./automation') 2 | exports.FunctionsConsumer = require("./Functions-consumer") 3 | exports.FunctionsBilling = require("./Functions-billing") 4 | exports.balance = require("./balance") 5 | exports.blockNumber = require("./block-number") 6 | exports.simulateScript = require("./simulateScript") 7 | -------------------------------------------------------------------------------- /.github/workflows/get-version-number.sh: -------------------------------------------------------------------------------- 1 | TERMINAL_OUTPUT=$(cat) 2 | REGEX_GET_VERSION_FROM_TERMINAL="version ([{0-9}]+)" 3 | 4 | 5 | 6 | # Check if TERMINAL_OUTPUT matches the regex pattern 7 | if [[ $TERMINAL_OUTPUT =~ $REGEX_GET_VERSION_FROM_TERMINAL ]]; then 8 | version="${BASH_REMATCH[1]}" 9 | echo "$version" 10 | else 11 | echo "No match found." 12 | fi -------------------------------------------------------------------------------- /app/src/components/google-tag.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | import TagManager from "react-gtm-module"; 5 | 6 | const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID; 7 | 8 | export default function GoogleTag() { 9 | useEffect(() => { 10 | if (GTM_ID) { 11 | TagManager.initialize({ gtmId: GTM_ID }); 12 | } 13 | }, []); 14 | 15 | return <>; 16 | } -------------------------------------------------------------------------------- /app/src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 4 | 5 | const Collapsible = CollapsiblePrimitive.Root 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 10 | 11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 12 | -------------------------------------------------------------------------------- /app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "prettier", 5 | "plugin:tailwindcss/recommended" 6 | ], 7 | "plugins": ["tailwindcss"], 8 | "settings": { 9 | "tailwindcss": { 10 | "callees": ["cn"], 11 | "config": "./tailwind.config.js" 12 | } 13 | }, 14 | "rules": { 15 | "@next/next/no-html-link-for-pages": ["error", "./src/app"] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-billing/index.js: -------------------------------------------------------------------------------- 1 | exports.create = require("./create") 2 | exports.fund = require("./fund") 3 | exports.info = require("./info") 4 | exports.add = require("./add") 5 | exports.remove = require("./remove") 6 | exports.cancel = require("./cancel") 7 | exports.transfer = require("./transfer") 8 | exports.accept = require("./accept") 9 | exports.timeoutRequests = require("./timeoutRequests") 10 | -------------------------------------------------------------------------------- /app/public/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export function useDebounce(value: T, delay?: number): T { 4 | const [debouncedValue, setDebouncedValue] = useState(value) 5 | 6 | useEffect(() => { 7 | const timer = setTimeout(() => setDebouncedValue(value), delay ?? 500) 8 | return () => clearTimeout(timer) 9 | }, [value, delay]) 10 | 11 | return debouncedValue 12 | } 13 | -------------------------------------------------------------------------------- /app/src/config/site.tsx: -------------------------------------------------------------------------------- 1 | export const siteConfig = { 2 | title: 'Chainlink Functions Demo dApp', 3 | description: 'Chainlink Functions Demo dApp', 4 | links: { 5 | github: 'https://github.com/smartcontractkit/run-functions-dapp', 6 | docs: 'https://dev.chain.link/products/functions', 7 | functionsDocs: 8 | 'https://docs.chain.link/chainlink-functions/getting-started', 9 | }, 10 | } 11 | 12 | export type SiteConfig = typeof siteConfig 13 | -------------------------------------------------------------------------------- /contracts/x-user-info.js: -------------------------------------------------------------------------------- 1 | const username = args[0] 2 | 3 | if (!secrets.xBearerToken) { 4 | throw Error("No bearer token") 5 | } 6 | const xUserResponse = await Functions.makeHttpRequest({ 7 | url: `https://api.twitter.com/2/users/by/username/${username}?user.fields=profile_image_url`, 8 | headers: { Authorization: `Bearer ${secrets.xBearerToken}` }, 9 | }) 10 | if (xUserResponse.error) { 11 | throw Error("X User Request Error") 12 | } 13 | const { name, id } = xUserResponse.data.data 14 | return Functions.encodeString([name, id]) 15 | -------------------------------------------------------------------------------- /contracts/tasks/balance.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require("ethers") 2 | 3 | const network = process.env.NETWORK 4 | const provider = ethers.getDefaultProvider(network) 5 | 6 | task("balance", "Prints an account's balance") 7 | .addParam("account", "The account's address") 8 | .setAction(async (taskArgs) => { 9 | const account = ethers.utils.getAddress(taskArgs.account) 10 | const balance = await provider.getBalance(account) 11 | 12 | console.log(ethers.utils.formatEther(balance), "ETH") 13 | }) 14 | 15 | module.exports = {} 16 | -------------------------------------------------------------------------------- /contracts/x-last-tweets.js: -------------------------------------------------------------------------------- 1 | const id = args[0] 2 | if (!secrets.xBearerToken) { 3 | throw Error("No bearer token") 4 | } 5 | const xTweetsResponse = await Functions.makeHttpRequest({ 6 | url: `https://api.twitter.com/2/users/${id}/tweets`, 7 | headers: { Authorization: `Bearer ${secrets.xBearerToken}` }, 8 | }) 9 | if (xTweetsResponse.error) { 10 | throw Error(xTweetsResponse.code) 11 | } 12 | const lastTweet = xTweetsResponse.data.data[0].text 13 | const shortenedTweet = lastTweet.substring(0, 255) 14 | return Functions.encodeString(shortenedTweet) 15 | -------------------------------------------------------------------------------- /contracts/test/unit/FunctionsConsumer.spec.js: -------------------------------------------------------------------------------- 1 | // const { assert } = require("chai") 2 | // const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers") 3 | // const { network } = require("hardhat") 4 | 5 | describe("Functions Consumer Unit Tests", async function () { 6 | // We define a fixture to reuse the same setup in every test. 7 | // We use loadFixture to run this setup once, snapshot that state, 8 | // and reset Hardhat Network to that snapshot in every test. 9 | 10 | it("empty test", async () => { 11 | // TODO 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /app/public/chainlink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/app/x/layout.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | 3 | export default function XLayout({ children }: { children: React.ReactNode }) { 4 | return ( 5 | <> 6 | {children} 7 |
8 |

How It Works

9 | how-it-works 16 |
17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | 11 | /coverage 12 | 13 | # next.js 14 | 15 | /.next/ 16 | /out/ 17 | 18 | # production 19 | 20 | /build 21 | 22 | # misc 23 | 24 | .DS_Store 25 | \*.pem 26 | 27 | # debug 28 | 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | .pnpm-debug.log* 33 | 34 | # local env files 35 | 36 | .env 37 | .env\*.local 38 | 39 | # vercel 40 | 41 | .vercel 42 | 43 | # typescript 44 | 45 | \*.tsbuildinfo 46 | next-env.d.ts 47 | -------------------------------------------------------------------------------- /app/public/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /app/src/app/open-meteo/layout.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | 3 | export default function OpenMeteoLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode 7 | }) { 8 | return ( 9 | <> 10 | {children} 11 |
12 |

How It Works

13 | how-it-works 20 |
21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /contracts/weather-api.js: -------------------------------------------------------------------------------- 1 | const lat = args[0] 2 | const long = args[1] 3 | 4 | const weatherResponse = await Functions.makeHttpRequest({ 5 | url: `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${long}¤t=temperature_2m`, 6 | }) 7 | 8 | if (weatherResponse.error) { 9 | throw Error("Weather API Error") 10 | } 11 | 12 | const currentTemperature = weatherResponse.data.current["temperature_2m"] 13 | 14 | if (!currentTemperature) { 15 | throw Error("Weather API did not return temperature") 16 | } 17 | 18 | console.log(`Current temperature at ${lat}, ${long} is ${currentTemperature}°C`) 19 | 20 | return Functions.encodeString(currentTemperature) 21 | -------------------------------------------------------------------------------- /app/public/arrow-go-to-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /app/public/arrow-go-to-up-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /app/src/components/loading-spinner.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | import Image from 'next/image' 3 | 4 | const LoadingSpinner = ({ 5 | background, 6 | className, 7 | }: { 8 | background?: string 9 | className?: string 10 | }) => { 11 | return ( 12 |
19 | loading 20 |
26 |
27 | ) 28 | } 29 | 30 | export default LoadingSpinner 31 | -------------------------------------------------------------------------------- /contracts/tasks/utils/network.js: -------------------------------------------------------------------------------- 1 | const BASE_URLS = { 2 | 1: "https://etherscan.io/", 3 | 137: "https://polygonscan.com/", 4 | 43114: "https://snowtrace.io/", 5 | 80001: "https://mumbai.polygonscan.com/", 6 | 11155111: "https://sepolia.etherscan.io/", 7 | 43113: "https://testnet.snowtrace.io/", 8 | } 9 | 10 | /** 11 | * Returns the Etherscan API domain for a given chainId. 12 | * 13 | * @param chainId Ethereum chain ID 14 | */ 15 | function getEtherscanURL(chainId) { 16 | const idNotFound = !Object.keys(BASE_URLS).includes(chainId.toString()) 17 | if (idNotFound) { 18 | throw new Error("Invalid chain Id") 19 | } 20 | return BASE_URLS[chainId] 21 | } 22 | 23 | module.exports = { 24 | getEtherscanURL, 25 | } 26 | -------------------------------------------------------------------------------- /app/public/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/app/api/geolocation/route.ts: -------------------------------------------------------------------------------- 1 | import { fetchGeoLocation } from '@/lib/fetch-geocoding' 2 | import { GeoLocation } from '@/types' 3 | import { NextResponse } from 'next/server' 4 | 5 | const filterUnique = (arr: GeoLocation[] | undefined) => { 6 | const seen = new Set() 7 | return arr?.filter((item) => { 8 | const key = item.city + item.countryCode 9 | return seen.has(key) ? false : seen.add(key) 10 | }) 11 | } 12 | 13 | export async function GET(request: Request) { 14 | const { searchParams } = new URL(request.url) 15 | const q = searchParams.get('q') || '' 16 | 17 | if (!q) return NextResponse.error() 18 | 19 | const result = await fetchGeoLocation(q) 20 | const data = filterUnique(result) 21 | 22 | return NextResponse.json({ data }) 23 | } 24 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": "./", 18 | "paths": { 19 | "@/*": ["./src/*"] 20 | }, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /contracts/tasks/utils/prompt.js: -------------------------------------------------------------------------------- 1 | const readline = require("readline") 2 | const chalk = require("chalk") 3 | 4 | function ask(query) { 5 | const rl = readline.createInterface({ 6 | input: process.stdin, 7 | output: process.stdout, 8 | }) 9 | 10 | return new Promise((resolve) => 11 | rl.question(query, (ans) => { 12 | rl.close() 13 | resolve(ans) 14 | }) 15 | ) 16 | } 17 | 18 | async function prompt(query) { 19 | if (!process.env.SKIP_PROMPTS) { 20 | if (query) console.log(`${query}\n`) 21 | const reply = await ask(`${chalk.green("Continue?")} Enter (y) Yes / (n) No\n`) 22 | if (reply.toLowerCase() !== "y" && reply.toLowerCase() !== "yes") { 23 | console.log("Aborted.") 24 | process.exit(1) 25 | } 26 | } 27 | } 28 | 29 | module.exports = { 30 | ask, 31 | prompt, 32 | } 33 | -------------------------------------------------------------------------------- /contracts/tasks/utils/logger.js: -------------------------------------------------------------------------------- 1 | const { Console } = require("console") 2 | const { Transform } = require("stream") 3 | 4 | function table(input) { 5 | // @see https://stackoverflow.com/a/67859384 6 | const ts = new Transform({ 7 | transform(chunk, enc, cb) { 8 | cb(null, chunk) 9 | }, 10 | }) 11 | const logger = new Console({ stdout: ts }) 12 | logger.table(input) 13 | const table = (ts.read() || "").toString() 14 | let result = "" 15 | for (let row of table.split(/[\r\n]+/)) { 16 | let r = row.replace(/[^┬]*┬/, "┌") 17 | r = r.replace(/^├─*┼/, "├") 18 | r = r.replace(/│[^│]*/, "") 19 | r = r.replace(/^└─*┴/, "└") 20 | r = r.replace(/'/g, " ") 21 | result += `${r}\n` 22 | } 23 | console.log(result) 24 | } 25 | 26 | const logger = { table } 27 | 28 | module.exports = { 29 | logger, 30 | } 31 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-consumer/requestUserInfo.js: -------------------------------------------------------------------------------- 1 | const { SecretsManager } = require("@chainlink/functions-toolkit") 2 | const { networks } = require("../../networks") 3 | const process = require("process") 4 | const path = require("path") 5 | 6 | task("functions-request-info", "Request User Info") 7 | .addParam("contract", "Contract Address") 8 | .addParam("username", "Username") 9 | .addParam("slotid", "Version") 10 | .addParam("secretversion", "Secret version") 11 | .setAction(async (taskArgs) => { 12 | const contract = await ethers.getContractAt("XUserDataConsumer", taskArgs.contract) 13 | const tx = await contract.requestUserInfo( 14 | taskArgs.username, 15 | parseInt(taskArgs.slotid), 16 | parseInt(taskArgs.secretversion) 17 | ) 18 | console.log("Tx Hash:", tx.hash) 19 | const receipt = await tx.wait() 20 | console.log("Tx Receipt:", receipt) 21 | }) 22 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-consumer/requestLastUserTweet.js: -------------------------------------------------------------------------------- 1 | const { SecretsManager } = require("@chainlink/functions-toolkit") 2 | const { networks } = require("../../networks") 3 | const process = require("process") 4 | const path = require("path") 5 | 6 | task("functions-request-tweet", "Request User Last Tweet") 7 | .addParam("contract", "Contract Address") 8 | .addParam("userid", "User Id") 9 | .addParam("slotid", "Version") 10 | .addParam("secretversion", "Secret version") 11 | .setAction(async (taskArgs) => { 12 | const contract = await ethers.getContractAt("XUserDataConsumer", taskArgs.contract) 13 | const tx = await contract.requestLastTweet( 14 | taskArgs.userid, 15 | parseInt(taskArgs.slotid), 16 | parseInt(taskArgs.secretversion) 17 | ) 18 | console.log("Tx Hash:", tx.hash) 19 | const receipt = await tx.wait() 20 | console.log("Tx Receipt:", receipt) 21 | }) 22 | -------------------------------------------------------------------------------- /app/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | }, 22 | ) 23 | Input.displayName = 'Input' 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-consumer/index.js: -------------------------------------------------------------------------------- 1 | exports.readResultAndError = require("./readResultAndError.js") 2 | exports.requestData = require("./request.js") 3 | exports.deployConsumer = require("./deployConsumer.js") 4 | exports.deployWeatherConsumer = require("./deployWeatherConsumer.js") 5 | exports.deployXConsumer = require("./deployXConsumer.js") 6 | exports.deployAutoConsumer = require("./deployAutoConsumer.js") 7 | exports.setDonId = require("./setDonId.js") 8 | exports.requestUserInfo = require("./requestUserInfo.js") 9 | exports.requestLastTweet = require("./requestLastUserTweet.js") 10 | exports.buildOffchainSecrets = require("./buildOffchainSecrets.js") 11 | exports.checkUpkeep = require("./checkUpkeep.js") 12 | exports.performUpkeep = require("./performManualUpkeep.js") 13 | exports.setAutoRequest = require("./setAutoRequest.js") 14 | exports.uploadSecretsToDon = require("./uploadSecretsToDon.js") 15 | exports.listDonSecrets = require("./listDonSecrets.js") 16 | -------------------------------------------------------------------------------- /app/src/components/site-header.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import { MobileNav } from '@/components/mobile-nav' 3 | import { MainNav } from '@/components/main-nav' 4 | 5 | export function SiteHeader() { 6 | return ( 7 |
8 | 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-consumer/checkUpkeep.js: -------------------------------------------------------------------------------- 1 | task("functions-check-upkeep", "Checks if checkUpkeep returns true for an Automation compatible contract") 2 | .addParam("contract", "Address of the contract to check") 3 | .addOptionalParam( 4 | "data", 5 | "Hex string representing bytes that are passed to the checkUpkeep function (defaults to empty bytes)" 6 | ) 7 | .setAction(async (taskArgs) => { 8 | const checkData = taskArgs.data ?? [] 9 | 10 | console.log( 11 | `Checking if upkeep is required for Automation consumer contract ${taskArgs.contract} on network ${network.name}` 12 | ) 13 | const autoConsumerContractFactory = await ethers.getContractFactory("AutomatedFunctionsConsumer") 14 | const autoConsumerContract = await autoConsumerContractFactory.attach(taskArgs.contract) 15 | 16 | const checkUpkeep = await autoConsumerContract.checkUpkeep(checkData) 17 | 18 | console.log(`\nUpkeep needed: ${checkUpkeep[0]}\nPerform data: ${checkUpkeep[1]}`) 19 | }) 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 SmartContract Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/public/angle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-billing/info.js: -------------------------------------------------------------------------------- 1 | const { SubscriptionManager } = require("@chainlink/functions-toolkit") 2 | const { networks } = require("../../networks") 3 | 4 | task( 5 | "functions-sub-info", 6 | "Gets the Functions billing subscription balance, owner, and list of authorized consumer contract addresses" 7 | ) 8 | .addParam("subid", "Subscription ID") 9 | .setAction(async (taskArgs) => { 10 | const subscriptionId = parseInt(taskArgs.subid) 11 | 12 | const signer = await ethers.getSigner() 13 | const linkTokenAddress = networks[network.name]["linkToken"] 14 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 15 | 16 | const sm = new SubscriptionManager({ signer, linkTokenAddress, functionsRouterAddress }) 17 | await sm.initialize() 18 | 19 | const subInfo = await sm.getSubscriptionInfo(subscriptionId) 20 | // parse balances into LINK for readability 21 | subInfo.balance = ethers.utils.formatEther(subInfo.balance) + " LINK" 22 | subInfo.blockedBalance = ethers.utils.formatEther(subInfo.blockedBalance) + " LINK" 23 | console.log(`\nInfo for subscription ${subscriptionId}:\n`, subInfo) 24 | }) 25 | -------------------------------------------------------------------------------- /contracts/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 SmartContract Chainlink Limited SEZC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/src/app/api/onchain-tweet/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | 3 | import { getTweetOnchain, requestTweetOnchain } from '@/lib/request-onchain' 4 | import { addToTweetHistory } from '@/lib/history' 5 | 6 | export async function POST(request: NextRequest) { 7 | const params = await request.json() 8 | if (!params || !params.username) return NextResponse.error() 9 | 10 | const { username } = params 11 | 12 | const { txHash, requestId } = await requestTweetOnchain(username) 13 | 14 | if (!txHash) return NextResponse.error() 15 | 16 | try { 17 | await addToTweetHistory({ 18 | txHash, 19 | requestId, 20 | username, 21 | }) 22 | } catch (error) { 23 | console.log('Adding request to history failed.') 24 | } 25 | 26 | return NextResponse.json({ txHash, requestId }) 27 | } 28 | 29 | export async function GET(request: NextRequest) { 30 | const { searchParams } = new URL(request.url) 31 | const requestId = searchParams.get('requestId') || '' 32 | 33 | if (!requestId) return NextResponse.error() 34 | 35 | const [, text] = await getTweetOnchain(requestId) 36 | return NextResponse.json({ text }) 37 | } 38 | -------------------------------------------------------------------------------- /app/public/code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 8 | 10 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/lib/fetch-geocoding.ts: -------------------------------------------------------------------------------- 1 | import 'server-only' 2 | import { cache } from 'react' 3 | import { GeoLocation } from '@/types' 4 | 5 | const geoCodingApiUrl = 'https://api.api-ninjas.com/v1/geocoding' 6 | const geocodingApiKey = process.env.GEOCODING_API_KEY 7 | 8 | const fetchGeocodingData = cache(async (params: URLSearchParams) => { 9 | const response = await fetch(`${geoCodingApiUrl}?${params.toString()}`, { 10 | headers: { 11 | 'X-Api-Key': geocodingApiKey!, 12 | }, 13 | cache: 'force-cache', 14 | }) 15 | if (response.status !== 200) { 16 | throw new Error(`Status ${response.status}`) 17 | } 18 | return response.json() 19 | }) 20 | 21 | export const fetchGeoLocation = async ( 22 | city: string, 23 | country = '', 24 | state = '', 25 | ): Promise => { 26 | const params = new URLSearchParams({ 27 | city, 28 | country, 29 | state, 30 | }) 31 | const data = await fetchGeocodingData(params) 32 | return data.map((item: any) => ({ 33 | city: item.name, 34 | state: item.state, 35 | countryCode: item.country, 36 | coordinates: { 37 | latitude: item.latitude, 38 | longitude: item.longitude, 39 | }, 40 | })) 41 | } 42 | -------------------------------------------------------------------------------- /contracts/calculation-example.js: -------------------------------------------------------------------------------- 1 | // This example shows how to calculate a continuously compounding interested rate. 2 | // This calculation would require significant on-chain gas, but is easy for a decentralized oracle network. 3 | 4 | // Arguments can be provided when a request is initated on-chain and used in the request source code as shown below 5 | const principalAmount = parseInt(args[4]) 6 | const APYTimes100 = parseInt(args[5]) 7 | const APYAsDecimalPercentage = APYTimes100 / 100 / 100 8 | 9 | const timeInYears = 1 / 12 // represents 1 month 10 | const eulersNumber = 2.7183 11 | 12 | // Continuously-compounding interest formula: A = Pe^(rt) 13 | const totalAmountAfterInterest = principalAmount * eulersNumber ** (APYAsDecimalPercentage * timeInYears) 14 | 15 | // The source code MUST return a Buffer or the request will return an error message 16 | // Use one of the following functions to convert to a Buffer representing the response bytes that are returned to the consumer smart contract: 17 | // - Functions.encodeUint256 18 | // - Functions.encodeInt256 19 | // - Functions.encodeString 20 | // Or return a custom Buffer for a custom byte encoding 21 | return Functions.encodeUint256(Math.round(totalAmountAfterInterest)) 22 | -------------------------------------------------------------------------------- /app/src/app/x/offchain-response.tsx: -------------------------------------------------------------------------------- 1 | import CodeBlock from '@/components/code-block' 2 | import { ScrollArea } from '@/components/ui/scroll-area' 3 | import { fetchTweetData, getTweetText } from '@/lib/fetch-tweet' 4 | import { firaCode } from '@/lib/fonts' 5 | import { cn } from '@/lib/utils' 6 | 7 | type OffchainResponseProps = { 8 | handle: string 9 | } 10 | 11 | export const OffchainResponse = async ({ handle }: OffchainResponseProps) => { 12 | const tweetData = await fetchTweetData(handle) 13 | const rawData = JSON.stringify(tweetData, null, 4) 14 | const parsedData = getTweetText(tweetData) 15 | 16 | return ( 17 | <> 18 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-consumer/setDonId.js: -------------------------------------------------------------------------------- 1 | const { networks } = require("../../networks") 2 | 3 | task( 4 | "functions-set-donid", 5 | "Updates the oracle address for a FunctionsConsumer consumer contract using the FunctionsOracle address from `network-config.js`" 6 | ) 7 | .addParam("contract", "Address of the consumer contract to update") 8 | .setAction(async (taskArgs) => { 9 | const donId = networks[network.name]["donId"] 10 | console.log(`Setting donId to ${donId} in Functions consumer contract ${taskArgs.contract} on ${network.name}`) 11 | const consumerContractFactory = await ethers.getContractFactory("FunctionsConsumer") 12 | const consumerContract = await consumerContractFactory.attach(taskArgs.contract) 13 | 14 | const donIdBytes32 = hre.ethers.utils.formatBytes32String(donId) 15 | const updateTx = await consumerContract.setDonId(donIdBytes32) 16 | 17 | console.log( 18 | `\nWaiting ${networks[network.name].confirmations} blocks for transaction ${updateTx.hash} to be confirmed...` 19 | ) 20 | await updateTx.wait(networks[network.name].confirmations) 21 | 22 | console.log(`\nUpdated donId to ${donId} for Functions consumer contract ${taskArgs.contract} on ${network.name}`) 23 | }) 24 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-billing/add.js: -------------------------------------------------------------------------------- 1 | const { SubscriptionManager } = require("@chainlink/functions-toolkit") 2 | const { networks } = require("../../networks") 3 | 4 | task("functions-sub-add", "Adds a consumer contract to the Functions billing subscription") 5 | .addParam("subid", "Subscription ID") 6 | .addParam("contract", "Address of the Functions consumer contract to authorize for billing") 7 | .setAction(async (taskArgs) => { 8 | const consumerAddress = taskArgs.contract 9 | const subscriptionId = parseInt(taskArgs.subid) 10 | 11 | const signer = await ethers.getSigner() 12 | const linkTokenAddress = networks[network.name]["linkToken"] 13 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 14 | const txOptions = { confirmations: networks[network.name].confirmations } 15 | 16 | const sm = new SubscriptionManager({ signer, linkTokenAddress, functionsRouterAddress }) 17 | await sm.initialize() 18 | 19 | console.log(`\nAdding ${consumerAddress} to subscription ${subscriptionId}...`) 20 | const addConsumerTx = await sm.addConsumer({ subscriptionId, consumerAddress, txOptions }) 21 | console.log(`Added consumer contract ${consumerAddress} in Tx: ${addConsumerTx.transactionHash}`) 22 | }) 23 | -------------------------------------------------------------------------------- /contracts/tasks/simulateScript.js: -------------------------------------------------------------------------------- 1 | const { simulateScript, decodeResult } = require("@chainlink/functions-toolkit") 2 | const path = require("path") 3 | const process = require("process") 4 | 5 | task("functions-simulate-script", "Executes the JavaScript source code locally") 6 | .addOptionalParam( 7 | "configpath", 8 | "Path to Functions request config file", 9 | `${__dirname}/../Functions-request-config.js`, 10 | types.string 11 | ) 12 | .setAction(async (taskArgs, hre) => { 13 | const requestConfig = require(path.isAbsolute(taskArgs.configpath) 14 | ? taskArgs.configpath 15 | : path.join(process.cwd(), taskArgs.configpath)) 16 | 17 | // Simulate the JavaScript execution locally 18 | const { responseBytesHexstring, errorString, capturedTerminalOutput } = await simulateScript(requestConfig) 19 | console.log(`${capturedTerminalOutput}\n`) 20 | if (responseBytesHexstring) { 21 | console.log( 22 | `Response returned by script during local simulation: ${decodeResult( 23 | responseBytesHexstring, 24 | requestConfig.expectedReturnType 25 | ).toString()}\n` 26 | ) 27 | } 28 | if (errorString) { 29 | console.log(`Error returned by simulated script:\n${errorString}\n`) 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /app/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css' 2 | 3 | import { siteConfig } from '@/config/site' 4 | import { figtree } from '@/lib/fonts' 5 | import { cn } from '@/lib/utils' 6 | import { SiteHeader } from '@/components/site-header' 7 | import GoogleTag from '@/components/google-tag' 8 | import { Metadata } from 'next' 9 | 10 | export const metadata: Metadata = { 11 | title: siteConfig.title, 12 | openGraph: { 13 | title: siteConfig.title, 14 | siteName: siteConfig.title, 15 | images: '/opengraph-image.png', 16 | type: 'website', 17 | }, 18 | twitter: { 19 | card: 'summary_large_image', 20 | title: siteConfig.title, 21 | description: siteConfig.description, 22 | images: [ 23 | { 24 | url: './opengraph-image.png', 25 | alt: siteConfig.title, 26 | }, 27 | ], 28 | }, 29 | } 30 | 31 | export default function RootLayout({ 32 | children, 33 | }: { 34 | children: React.ReactNode 35 | }) { 36 | return ( 37 | 38 | 44 | 45 | {children} 46 | 47 | 48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /contracts/Functions-request-config.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const { Location, ReturnType, CodeLanguage } = require("@chainlink/functions-toolkit") 3 | 4 | // Configure the request by setting the fields below 5 | const requestConfig = { 6 | // String containing the source code to be executed 7 | source: fs.readFileSync("./x-last-tweets.js").toString(), 8 | //source: fs.readFileSync("./API-request-example.js").toString(), 9 | // Location of source code (only Inline is currently supported) 10 | codeLocation: Location.Inline, 11 | // Optional. Secrets can be accessed within the source code with `secrets.varName` (ie: secrets.apiKey). The secrets object can only contain string values. 12 | secrets: { apiKey: process.env.COINMARKETCAP_API_KEY ?? "", xBearerToken: process.env.X_BEARER_TOKEN }, 13 | // Optional if secrets are expected in the sourceLocation of secrets (only Remote or DONHosted is supported) 14 | secretsLocation: Location.DONHosted, 15 | // Args (string only array) can be accessed within the source code with `args[index]` (ie: args[0]). 16 | args: ["44196397"], // 44196397 is Elon Musk's x ID 17 | // Code language (only JavaScript is currently supported) 18 | codeLanguage: CodeLanguage.JavaScript, 19 | // Expected type of the returned value 20 | expectedReturnType: ReturnType.bytes, 21 | } 22 | 23 | module.exports = requestConfig 24 | -------------------------------------------------------------------------------- /contracts/tasks/utils/price.js: -------------------------------------------------------------------------------- 1 | function numberWithCommas(x) { 2 | return x.toString().replace(/\B(?, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverTrigger, PopoverContent } 32 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-consumer/listDonSecrets.js: -------------------------------------------------------------------------------- 1 | const { SecretsManager } = require("@chainlink/functions-toolkit") 2 | const { networks } = require("../../networks") 3 | 4 | task("functions-list-don-secrets", "Displays encrypted secrets hosted on the DON").setAction(async (taskArgs) => { 5 | const signer = await ethers.getSigner() 6 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 7 | const donId = networks[network.name]["donId"] 8 | 9 | const gatewayUrls = networks[network.name]["gatewayUrls"] 10 | if (!gatewayUrls || gatewayUrls.length === 0) { 11 | throw Error(`No gatewayUrls found for ${network.name} network.`) 12 | } 13 | 14 | const secretsManager = new SecretsManager({ 15 | signer, 16 | functionsRouterAddress, 17 | donId, 18 | }) 19 | await secretsManager.initialize() 20 | 21 | const { result } = await secretsManager.listDONHostedEncryptedSecrets(gatewayUrls) 22 | console.log(`\nYour encrypted secrets currently hosted on DON ${donId}`) 23 | console.log("\n\nGateway:", result.gatewayUrl) 24 | let i = 0 25 | result.nodeResponses.forEach((nodeResponse) => { 26 | console.log(`\nNode Response #${i}`) 27 | i++ 28 | if (nodeResponse.rows) { 29 | nodeResponse.rows.forEach((row) => { 30 | console.log(row) 31 | }) 32 | } else { 33 | console.log("No encrypted secrets found") 34 | } 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /app/src/components/main-nav.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import Image from 'next/image' 3 | 4 | import { siteConfig } from '@/config/site' 5 | import { cn } from '@/lib/utils' 6 | import { buttonVariants } from '@/components/ui/button' 7 | 8 | export function MainNav() { 9 | return ( 10 |
11 | 20 | github 21 | 22 | Open in Github 23 | 24 | 25 | 34 | docs 35 | 36 | Functions Resources 37 | 38 | 39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /app/public/dev-expert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/lib/fetch-weather.ts: -------------------------------------------------------------------------------- 1 | import 'server-only' 2 | import { cache } from 'react' 3 | import { Coordinates, WeatherResponse } from '@/types' 4 | 5 | export const weatherApiUrl = 'https://api.open-meteo.com/v1/forecast' 6 | 7 | const fetchWeatherData = cache( 8 | async (params: URLSearchParams, revalidate = 60) => { 9 | const response = await fetch(`${weatherApiUrl}?${params.toString()}`, { 10 | next: { 11 | revalidate, 12 | }, 13 | }) 14 | if (response.status !== 200) { 15 | throw new Error(`Status ${response.status}`) 16 | } 17 | return response.json() 18 | }, 19 | ) 20 | 21 | export const fetchCurrentWeather = async ( 22 | location: Coordinates, 23 | ): Promise => { 24 | const params = new URLSearchParams({ 25 | latitude: location.latitude.toString(), 26 | longitude: location.longitude.toString(), 27 | current: ['temperature_2m', 'weather_code'].join(','), 28 | }) 29 | const data = await fetchWeatherData(params) 30 | return data 31 | } 32 | 33 | export const getCurrentTemperature = (weatherResponse: WeatherResponse) => { 34 | const temperature = weatherResponse.current.temperature_2m 35 | return temperature 36 | } 37 | 38 | export const getCurrentTemperatureUnit = (weatherResponse: WeatherResponse) => { 39 | const temperatureUnit = weatherResponse.current_units.temperature_2m 40 | return temperatureUnit 41 | } 42 | 43 | export const getCurrentWeatherCode = (weatherResponse: WeatherResponse) => { 44 | const weatherCode = weatherResponse.current.weather_code 45 | return weatherCode 46 | } 47 | -------------------------------------------------------------------------------- /app/public/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 223 30% 9%; 8 | --foreground: 0 0% 100%; 9 | 10 | --muted: 210 40% 96.1%; 11 | --muted-foreground: 220 7% 74%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 47.4% 11.2%; 15 | 16 | --card: 223 30% 9%; 17 | --card-foreground: 221 8% 46%; 18 | 19 | --border: 221 28% 20%; 20 | --input: 221 28% 20%; 21 | 22 | --primary: 222.2 47.4% 11.2%; 23 | --primary-foreground: 210 40% 98%; 24 | 25 | --secondary: 210 40% 96.1%; 26 | --secondary-foreground: 222.2 47.4% 11.2%; 27 | 28 | --accent: 223 30% 9%; 29 | --accent-foreground: 0 0% 100%; 30 | 31 | --destructive: 0 100% 50%; 32 | --destructive-foreground: 210 40% 98%; 33 | 34 | --ring: 222 38% 19%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | } 39 | 40 | @layer base { 41 | * { 42 | @apply border-border; 43 | } 44 | html { 45 | @apply scroll-smooth; 46 | } 47 | body { 48 | @apply bg-background text-foreground; 49 | font-feature-settings: 'rlig' 1, 'calt' 1; 50 | } 51 | } 52 | 53 | .react-syntax-highlighter-line-row:first-child { 54 | padding-top: 16px; 55 | } 56 | .react-syntax-highlighter-line-row:first-child 57 | > .react-syntax-highlighter-line-number { 58 | padding-top: 16px; 59 | } 60 | .react-syntax-highlighter-line-row:last-child { 61 | margin-bottom: 16px; 62 | } 63 | .react-syntax-highlighter-line-row:last-child 64 | > .react-syntax-highlighter-line-number { 65 | bottom: -16px !important; 66 | } 67 | .explainer-link { 68 | text-decoration: underline; 69 | } -------------------------------------------------------------------------------- /contracts/tasks/Functions-billing/cancel.js: -------------------------------------------------------------------------------- 1 | const { SubscriptionManager } = require("@chainlink/functions-toolkit") 2 | 3 | const utils = require("../utils") 4 | const { networks } = require("../../networks") 5 | 6 | task( 7 | "functions-sub-cancel", 8 | "Cancels Functions billing subscription and refunds unused balance. Cancellation is only possible if there are no pending requests" 9 | ) 10 | .addParam("subid", "Subscription ID to cancel") 11 | .addOptionalParam( 12 | "refundaddress", 13 | "Address where the remaining subscription balance is sent (defaults to caller's address)" 14 | ) 15 | .setAction(async (taskArgs) => { 16 | const subscriptionId = parseInt(taskArgs.subid) 17 | const refundAddress = taskArgs.refundaddress ?? (await ethers.getSigners())[0].address 18 | 19 | const signer = await ethers.getSigner() 20 | const linkTokenAddress = networks[network.name]["linkToken"] 21 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 22 | const confirmations = networks[network.name].confirmations 23 | const txOptions = { confirmations } 24 | 25 | const sm = new SubscriptionManager({ signer, linkTokenAddress, functionsRouterAddress }) 26 | await sm.initialize() 27 | 28 | await utils.prompt( 29 | `\nPlease confirm that you wish to cancel Subscription ${subscriptionId} and have its LINK balance sent to wallet ${refundAddress}.` 30 | ) 31 | 32 | console.log(`Canceling subscription ${subscriptionId}`) 33 | const cancelTx = await sm.cancelSubscription({ subscriptionId, refundAddress, txOptions }) 34 | console.log(`\nSubscription ${subscriptionId} cancelled in Tx: ${cancelTx.transactionHash}`) 35 | }) 36 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-billing/remove.js: -------------------------------------------------------------------------------- 1 | const { networks } = require("../../networks") 2 | const { SubscriptionManager } = require("@chainlink/functions-toolkit") 3 | 4 | task("functions-sub-remove", "Removes a consumer contract from an Functions billing subscription") 5 | .addParam("subid", "Subscription ID") 6 | .addParam("contract", "Address of the consumer contract to remove from billing subscription") 7 | .setAction(async (taskArgs) => { 8 | const signer = await ethers.getSigner() 9 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 10 | const linkTokenAddress = networks[network.name]["linkToken"] 11 | 12 | const consumerAddress = taskArgs.contract 13 | const subscriptionId = parseInt(taskArgs.subid) 14 | const confirmations = networks[network.name].confirmations 15 | const txOptions = { confirmations } 16 | 17 | const sm = new SubscriptionManager({ signer, linkTokenAddress, functionsRouterAddress }) 18 | await sm.initialize() 19 | 20 | console.log(`\nRemoving ${consumerAddress} from subscription ${subscriptionId}...`) 21 | let removeConsumerTx = await sm.removeConsumer({ subscriptionId, consumerAddress, txOptions }) 22 | 23 | const subInfo = await sm.getSubscriptionInfo(subscriptionId) 24 | // parse balances into LINK for readability 25 | subInfo.balance = ethers.utils.formatEther(subInfo.balance) + " LINK" 26 | subInfo.blockedBalance = ethers.utils.formatEther(subInfo.blockedBalance) + " LINK" 27 | console.log( 28 | `\nRemoved ${consumerAddress} from subscription ${subscriptionId} in Tx: ${removeConsumerTx.transactionHash}\nUpdated Subscription Info:\n`, 29 | subInfo 30 | ) 31 | }) 32 | -------------------------------------------------------------------------------- /app/src/components/handle-input.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useEffect, useState } from 'react' 4 | import { usePathname, useRouter, useSearchParams } from 'next/navigation' 5 | import Image from 'next/image' 6 | import { Button } from '@/components/ui/button' 7 | import { Input } from './ui/input' 8 | 9 | export const HandleInput = () => { 10 | const router = useRouter() 11 | const pathname = usePathname() 12 | const searchParams = useSearchParams() 13 | 14 | const [inputValue, setInputValue] = useState('') 15 | 16 | useEffect(() => { 17 | const handle = searchParams.get('handle') 18 | if (handle) { 19 | setInputValue(handle) 20 | } 21 | }, [searchParams]) 22 | 23 | const submit = () => { 24 | const newParams = new URLSearchParams({ 25 | handle: inputValue, 26 | }) 27 | router.push(`${pathname}?${newParams}`) 28 | } 29 | 30 | return ( 31 | <> 32 |
33 | @ 34 | setInputValue(e.target.value)} 37 | className="placeholder:text-card-foreground border-0 p-0" 38 | placeholder="X account handle" 39 | /> 40 |
41 | 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-billing/transfer.js: -------------------------------------------------------------------------------- 1 | const { SubscriptionManager } = require("@chainlink/functions-toolkit") 2 | 3 | const { networks } = require("../../networks") 4 | const utils = require("../utils") 5 | 6 | task("functions-sub-transfer", "Request ownership of an Functions subscription be transferred to a new address") 7 | .addParam("subid", "Subscription ID") 8 | .addParam("newowner", "Address of the new owner") 9 | .setAction(async (taskArgs) => { 10 | const subscriptionId = parseInt(taskArgs.subid) 11 | const newOwner = taskArgs.newowner 12 | const confirmations = networks[network.name].confirmations 13 | const txOptions = { confirmations } 14 | 15 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 16 | const linkTokenAddress = networks[network.name]["linkToken"] 17 | 18 | const signer = (await ethers.getSigners())[0] // First wallet. 19 | 20 | await utils.prompt( 21 | `\nTransferring the subscription to a new owner will require generating a new signature for encrypted secrets. 22 | Any previous encrypted secrets will no longer work with subscription ID ${subscriptionId} and must be regenerated by the new owner.` 23 | ) 24 | 25 | const sm = new SubscriptionManager({ signer, linkTokenAddress, functionsRouterAddress }) 26 | await sm.initialize() 27 | 28 | console.log(`\nRequesting transfer of subscription ${subscriptionId} to new owner ${newOwner}`) 29 | const requestTransferTx = await sm.requestSubscriptionTransfer({ subscriptionId, newOwner, txOptions }) 30 | console.log( 31 | `Transfer request completed in Tx: ${requestTransferTx.transactionHash}\nAccount ${newOwner} needs to accept transfer for it to complete.` 32 | ) 33 | }) 34 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-consumer/performManualUpkeep.js: -------------------------------------------------------------------------------- 1 | const { networks } = require("../../networks") 2 | 3 | task("functions-perform-upkeep", "Manually call performUpkeep in an Automation compatible contract") 4 | .addParam("contract", "Address of the contract to call") 5 | .addOptionalParam( 6 | "data", 7 | "Hex string representing bytes that are passed to the performUpkeep function (defaults to empty bytes)" 8 | ) 9 | .setAction(async (taskArgs) => { 10 | // A manual gas limit is required as the gas limit estimated by Ethers is not always accurate 11 | const overrides = { 12 | gasLimit: 1000000, 13 | gasPrice: networks[network.name].gasPrice, 14 | } 15 | 16 | // Call performUpkeep 17 | const performData = taskArgs.data ?? [] 18 | 19 | console.log( 20 | `Calling performUpkeep for Automation consumer contract ${taskArgs.contract} on network ${network.name}${ 21 | taskArgs.data ? ` with data ${performData}` : "" 22 | }` 23 | ) 24 | const autoConsumerContractFactory = await ethers.getContractFactory("AutomatedFunctionsConsumer") 25 | const autoConsumerContract = await autoConsumerContractFactory.attach(taskArgs.contract) 26 | 27 | const checkUpkeep = await autoConsumerContract.performUpkeep(performData, overrides) 28 | 29 | console.log( 30 | `Waiting ${networks[network.name].confirmations} blocks for transaction ${checkUpkeep.hash} to be confirmed...` 31 | ) 32 | await checkUpkeep.wait(networks[network.name].confirmations) 33 | 34 | console.log(`\nSuccessfully called performUpkeep`) 35 | 36 | const reqId = await autoConsumerContract.s_lastRequestId() 37 | console.log("\nLast request ID received by the Automation Consumer Contract...", reqId) 38 | }) 39 | -------------------------------------------------------------------------------- /app/src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "text-destructive border-destructive/50 dark:border-destructive [&>svg]:text-destructive text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )) 33 | Alert.displayName = "Alert" 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )) 45 | AlertTitle.displayName = "AlertTitle" 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | AlertDescription.displayName = "AlertDescription" 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /app/src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area' 5 | 6 | import { cn } from '@/lib/utils' 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 16 | 20 | {children} 21 | 22 | 23 | 24 | 25 | )) 26 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 27 | 28 | const ScrollBar = React.forwardRef< 29 | React.ElementRef, 30 | React.ComponentPropsWithoutRef 31 | >(({ className, orientation = 'vertical', ...props }, ref) => ( 32 | 45 | 46 | 47 | )) 48 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 49 | 50 | export { ScrollArea, ScrollBar } 51 | -------------------------------------------------------------------------------- /contracts/scripts/listen.js: -------------------------------------------------------------------------------- 1 | // Loads environment variables from .env.enc file (if it exists) 2 | require("@chainlink/env-enc").config("../.env.enc") 3 | 4 | const { networks } = require("../networks") 5 | 6 | const { ResponseListener, decodeResult, ReturnType } = require("@chainlink/functions-toolkit") 7 | const { providers } = require("ethers") 8 | 9 | const subscriptionId = "TODO" // TODO @dev update this to show your subscription Id 10 | 11 | if (!subscriptionId || isNaN(subscriptionId)) { 12 | throw Error("Please update the subId variable in scripts/listen.js to your subscription ID.") 13 | } 14 | 15 | const networkName = "polygonMumbai" // TODO @dev update this to your network name 16 | 17 | // Mount Response Listener 18 | const provider = new providers.JsonRpcProvider(networks[networkName].url) 19 | const functionsRouterAddress = networks[networkName]["functionsRouter"] 20 | 21 | const responseListener = new ResponseListener({ provider, functionsRouterAddress }) 22 | // Remove existing listeners 23 | console.log("\nRemoving existing listeners...") 24 | responseListener.stopListeningForResponses() 25 | 26 | console.log(`\nListening for Functions Responses for subscriptionId ${subscriptionId} on network ${networkName}...`) 27 | // Listen for response 28 | responseListener.listenForResponses(subscriptionId, (response) => { 29 | console.log(`\n✅ Request ${response.requestId} fulfilled. Functions Status Code: ${response.fulfillmentCode}`) 30 | if (!response.errorString) { 31 | console.log( 32 | "\nFunctions response received!\nData written on chain:", 33 | response.responseBytesHexstring, 34 | "\n and that decodes to an int256 value of: ", 35 | decodeResult(response.responseBytesHexstring, ReturnType.int256).toString(), 36 | "\n" 37 | ) 38 | } else { 39 | console.log("\n❌ Error during the execution: ", response.errorString, "\n") 40 | } 41 | }) 42 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-consumer/readResultAndError.js: -------------------------------------------------------------------------------- 1 | const { decodeResult } = require("@chainlink/functions-toolkit") 2 | const path = require("path") 3 | const process = require("process") 4 | 5 | task( 6 | "functions-read", 7 | "Reads the latest response (or error) returned to a FunctionsConsumer or AutomatedFunctionsConsumer consumer contract" 8 | ) 9 | .addParam("contract", "Address of the consumer contract to read") 10 | .addOptionalParam( 11 | "configpath", 12 | "Path to Functions request config file", 13 | `${__dirname}/../../Functions-request-config.js`, 14 | types.string 15 | ) 16 | .setAction(async (taskArgs) => { 17 | console.log(`Reading data from Functions consumer contract ${taskArgs.contract} on network ${network.name}`) 18 | const consumerContractFactory = await ethers.getContractFactory("FunctionsConsumer") 19 | const consumerContract = await consumerContractFactory.attach(taskArgs.contract) 20 | 21 | let latestError = await consumerContract.s_lastError() 22 | if (latestError.length > 0 && latestError !== "0x") { 23 | const errorString = Buffer.from(latestError.slice(2), "hex").toString() 24 | console.log(`\nOn-chain error message: ${errorString}`) 25 | } 26 | 27 | let latestResponse = await consumerContract.s_lastResponse() 28 | if (latestResponse.length > 0 && latestResponse !== "0x") { 29 | const requestConfig = require(path.isAbsolute(taskArgs.configpath) 30 | ? taskArgs.configpath 31 | : path.join(process.cwd(), taskArgs.configpath)) 32 | console.log( 33 | `\nOn-chain response represented as a hex string: ${latestResponse}\n${decodeResult( 34 | latestResponse, 35 | requestConfig.expectedReturnType 36 | ).toString()}` 37 | ) 38 | } else if (latestResponse == "0x") { 39 | console.log("Empty response: ", latestResponse) 40 | } 41 | }) 42 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-billing/accept.js: -------------------------------------------------------------------------------- 1 | const { SubscriptionManager } = require("@chainlink/functions-toolkit") 2 | 3 | const { networks } = require("../../networks") 4 | 5 | task("functions-sub-accept", "Accepts ownership of an Functions subscription after a transfer is requested") 6 | .addParam("subid", "Subscription ID") 7 | .setAction(async (taskArgs) => { 8 | const accounts = await ethers.getSigners() 9 | if (accounts.length < 2) { 10 | throw Error("This command requires a second wallet's private key to be made available in networks.js") 11 | } 12 | const accepter = accounts[1] // Second wallet. 13 | 14 | const subscriptionId = parseInt(taskArgs.subid) 15 | const confirmations = networks[network.name].confirmations 16 | const txOptions = { confirmations } 17 | 18 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 19 | const linkTokenAddress = networks[network.name]["linkToken"] 20 | 21 | const sm = new SubscriptionManager({ signer: accepter, linkTokenAddress, functionsRouterAddress }) 22 | await sm.initialize() 23 | 24 | const currentOwner = (await sm.getSubscriptionInfo(subscriptionId)).owner 25 | console.log(`\nAccepting ownership of subscription ${subscriptionId} from ${currentOwner}...`) 26 | const acceptTx = await sm.acceptSubTransfer({ subscriptionId, txOptions }) 27 | 28 | console.log( 29 | `Acceptance request completed in Tx: ${acceptTx.transactionHash}. \n${accepter.address} is now the owner of subscription ${subscriptionId}.` 30 | ) 31 | 32 | const subInfo = await sm.getSubscriptionInfo(subscriptionId) 33 | // parse balances into LINK for readability 34 | subInfo.balance = ethers.utils.formatEther(subInfo.balance) + " LINK" 35 | subInfo.blockedBalance = ethers.utils.formatEther(subInfo.blockedBalance) + " LINK" 36 | console.log("\nUpdated Subscription Info: ", subInfo) 37 | }) 38 | -------------------------------------------------------------------------------- /app/public/external.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/public/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /app/public/external-muted.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-billing/fund.js: -------------------------------------------------------------------------------- 1 | const { SubscriptionManager } = require("@chainlink/functions-toolkit") 2 | const chalk = require("chalk") 3 | const { networks } = require("../../networks") 4 | const utils = require("../utils") 5 | 6 | task("functions-sub-fund", "Funds a billing subscription for Functions consumer contracts") 7 | .addParam("amount", "Amount to fund subscription in LINK") 8 | .addParam("subid", "Subscription ID to fund") 9 | .setAction(async (taskArgs) => { 10 | const signer = await ethers.getSigner() 11 | const linkTokenAddress = networks[network.name]["linkToken"] 12 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 13 | const txOptions = { confirmations: networks[network.name].confirmations } 14 | 15 | const subscriptionId = parseInt(taskArgs.subid) 16 | const linkAmount = taskArgs.amount 17 | const juelsAmount = ethers.utils.parseUnits(linkAmount, 18).toString() 18 | 19 | const sm = new SubscriptionManager({ signer, linkTokenAddress, functionsRouterAddress }) 20 | await sm.initialize() 21 | 22 | await utils.prompt( 23 | `\nPlease confirm that you wish to fund Subscription ${subscriptionId} with ${chalk.blue( 24 | linkAmount + " LINK" 25 | )} from your wallet.` 26 | ) 27 | 28 | console.log(`\nFunding subscription ${subscriptionId} with ${linkAmount} LINK...`) 29 | 30 | const fundTxReceipt = await sm.fundSubscription({ juelsAmount, subscriptionId, txOptions }) 31 | console.log( 32 | `\nSubscription ${subscriptionId} funded with ${linkAmount} LINK in Tx: ${fundTxReceipt.transactionHash}` 33 | ) 34 | 35 | const subInfo = await sm.getSubscriptionInfo(subscriptionId) 36 | 37 | // parse balances into LINK for readability 38 | subInfo.balance = ethers.utils.formatEther(subInfo.balance) + " LINK" 39 | subInfo.blockedBalance = ethers.utils.formatEther(subInfo.blockedBalance) + " LINK" 40 | 41 | console.log("\nUpdated subscription Info: ", subInfo) 42 | }) 43 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web3-starter", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "prettier:check": "prettier --check .", 11 | "prettier:write": "prettier --write ." 12 | }, 13 | "dependencies": { 14 | "@radix-ui/react-alert-dialog": "^1.0.4", 15 | "@radix-ui/react-collapsible": "^1.0.3", 16 | "@radix-ui/react-dialog": "^1.0.4", 17 | "@radix-ui/react-dropdown-menu": "^2.0.5", 18 | "@radix-ui/react-popover": "^1.0.7", 19 | "@radix-ui/react-scroll-area": "^1.0.4", 20 | "@radix-ui/react-slot": "^1.0.2", 21 | "@types/react-syntax-highlighter": "^15.5.9", 22 | "class-variance-authority": "^0.6.0", 23 | "clsx": "^1.2.1", 24 | "cmdk": "^0.2.0", 25 | "date-fns": "^2.30.0", 26 | "lucide-react": "^0.220.0", 27 | "next": "14.2.35", 28 | "next-themes": "^0.2.1", 29 | "react": "^18.2.0", 30 | "react-dom": "^18.2.0", 31 | "react-gtm-module": "^2.0.11", 32 | "react-syntax-highlighter": "^15.5.0", 33 | "react-wrap-balancer": "^0.5.0", 34 | "tailwind-merge": "^1.12.0", 35 | "tailwindcss-animate": "^1.0.5" 36 | }, 37 | "devDependencies": { 38 | "@types/node": "^17.0.31", 39 | "@types/react": "^18.0.9", 40 | "@types/react-dom": "^18.0.3", 41 | "@types/react-gtm-module": "^2.0.3", 42 | "@upstash/ratelimit": "^0.4.4", 43 | "@vercel/kv": "^0.2.4", 44 | "autoprefixer": "^10.4.14", 45 | "bufferutil": "^4.0.8", 46 | "encoding": "^0.1.13", 47 | "eslint": "^8.15.0", 48 | "eslint-config-next": "^12.1.6", 49 | "eslint-config-prettier": "^8.8.0", 50 | "eslint-plugin-tailwindcss": "^3.12.0", 51 | "ethers": "^6.8.1", 52 | "lokijs": "^1.5.12", 53 | "pino-pretty": "^10.2.3", 54 | "postcss": "^8.4.23", 55 | "prettier": "2.8.8", 56 | "tailwindcss": "^3.3.1", 57 | "typescript": "^5.0.4", 58 | "utf-8-validate": "^5.0.10" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "underline-offset-4 hover:underline text-primary", 21 | }, 22 | size: { 23 | default: "h-10 py-2 px-4", 24 | sm: "h-9 px-3 rounded-md", 25 | lg: "h-11 px-8 rounded-md", 26 | }, 27 | }, 28 | defaultVariants: { 29 | variant: "default", 30 | size: "default", 31 | }, 32 | } 33 | ) 34 | 35 | export interface ButtonProps 36 | extends React.ButtonHTMLAttributes, 37 | VariantProps { 38 | asChild?: boolean 39 | } 40 | 41 | const Button = React.forwardRef( 42 | ({ className, variant, size, asChild = false, ...props }, ref) => { 43 | const Comp = asChild ? Slot : "button" 44 | return ( 45 | 50 | ) 51 | } 52 | ) 53 | Button.displayName = "Button" 54 | 55 | export { Button, buttonVariants } 56 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-consumer/buildOffchainSecrets.js: -------------------------------------------------------------------------------- 1 | const { SecretsManager } = require("@chainlink/functions-toolkit") 2 | const { networks } = require("../../networks") 3 | const fs = require("fs") 4 | const path = require("path") 5 | const process = require("process") 6 | 7 | task( 8 | "functions-build-offchain-secrets", 9 | "Builds an off-chain secrets object that can be uploaded and referenced via URL" 10 | ) 11 | .addOptionalParam( 12 | "output", 13 | "Output JSON file name (defaults to offchain-encrypted-secrets.json)", 14 | "offchain-encrypted-secrets.json", 15 | types.string 16 | ) 17 | .addOptionalParam( 18 | "configpath", 19 | "Path to Functions request config file", 20 | `${__dirname}/../../Functions-request-config.js`, 21 | types.string 22 | ) 23 | .setAction(async (taskArgs) => { 24 | const signer = await ethers.getSigner() 25 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 26 | const donId = networks[network.name]["donId"] 27 | 28 | const secretsManager = new SecretsManager({ 29 | signer, 30 | functionsRouterAddress, 31 | donId, 32 | }) 33 | await secretsManager.initialize() 34 | 35 | // Get the secrets object from Functions-request-config.js or other specific request config. 36 | const requestConfig = require(path.isAbsolute(taskArgs.configpath) 37 | ? taskArgs.configpath 38 | : path.join(process.cwd(), taskArgs.configpath)) 39 | 40 | if (!requestConfig.secrets || requestConfig.secrets.length === 0) { 41 | console.log("No secrets found in the request config.") 42 | return 43 | } 44 | 45 | const outputfile = taskArgs.output 46 | console.log(`\nEncrypting secrets and writing to JSON file '${outputfile}'...`) 47 | 48 | const encryptedSecretsObj = await secretsManager.encryptSecrets(requestConfig.secrets) 49 | fs.writeFileSync(outputfile, JSON.stringify(encryptedSecretsObj)) 50 | 51 | console.log(`\nWrote offchain secrets file to '${outputfile}'.`) 52 | }) 53 | -------------------------------------------------------------------------------- /app/src/app/open-meteo/offchain-response.tsx: -------------------------------------------------------------------------------- 1 | import CodeBlock from '@/components/code-block' 2 | import { ScrollArea } from '@/components/ui/scroll-area' 3 | import { 4 | fetchCurrentWeather, 5 | getCurrentTemperature, 6 | getCurrentTemperatureUnit, 7 | } from '@/lib/fetch-weather' 8 | import { firaCode } from '@/lib/fonts' 9 | import { cn } from '@/lib/utils' 10 | import { Coordinates } from '@/types' 11 | 12 | type OffchainResponseProps = { 13 | coordinates: Coordinates 14 | } 15 | 16 | export const OffchainResponse = async ({ 17 | coordinates, 18 | }: OffchainResponseProps) => { 19 | const weatherData = await fetchCurrentWeather(coordinates) 20 | 21 | const rawData = JSON.stringify(weatherData, null, 4) 22 | const temperature = getCurrentTemperature(weatherData) 23 | const parsedData = `${temperature}` 24 | const temperatureUnit = getCurrentTemperatureUnit(weatherData) 25 | 26 | return ( 27 | <> 28 | 31 | 34 | 35 | 36 |
37 |
38 | 41 |
42 | {parsedData} 43 |
44 |
45 |
46 | 49 |
50 | {temperatureUnit} 51 |
52 |
53 |
54 | 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /contracts/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomicfoundation/hardhat-toolbox") 2 | require("hardhat-contract-sizer") 3 | require("./tasks") 4 | const { networks } = require("./networks") 5 | 6 | // Enable gas reporting (optional) 7 | const REPORT_GAS = process.env.REPORT_GAS?.toLowerCase() === "true" ? true : false 8 | 9 | const SOLC_SETTINGS = { 10 | optimizer: { 11 | enabled: true, 12 | runs: 1_000, 13 | }, 14 | } 15 | 16 | /** @type import('hardhat/config').HardhatUserConfig */ 17 | module.exports = { 18 | defaultNetwork: "localFunctionsTestnet", 19 | solidity: { 20 | compilers: [ 21 | { 22 | version: "0.8.19", 23 | settings: SOLC_SETTINGS, 24 | }, 25 | { 26 | version: "0.8.7", 27 | settings: SOLC_SETTINGS, 28 | }, 29 | { 30 | version: "0.7.0", 31 | settings: SOLC_SETTINGS, 32 | }, 33 | { 34 | version: "0.6.6", 35 | settings: SOLC_SETTINGS, 36 | }, 37 | { 38 | version: "0.4.24", 39 | settings: SOLC_SETTINGS, 40 | }, 41 | ], 42 | }, 43 | networks: { 44 | ...networks, 45 | }, 46 | etherscan: { 47 | apiKey: { 48 | mainnet: networks.ethereum.verifyApiKey, 49 | avalanche: networks.avalanche.verifyApiKey, 50 | polygon: networks.polygon.verifyApiKey, 51 | sepolia: networks.ethereumSepolia.verifyApiKey, 52 | polygonMumbai: networks.polygonMumbai.verifyApiKey, 53 | avalancheFujiTestnet: networks.avalancheFuji.verifyApiKey, 54 | }, 55 | }, 56 | gasReporter: { 57 | enabled: REPORT_GAS, 58 | currency: "USD", 59 | outputFile: "gas-report.txt", 60 | noColors: true, 61 | }, 62 | contractSizer: { 63 | runOnCompile: false, 64 | only: ["FunctionsConsumer", "AutomatedFunctionsConsumer", "FunctionsBillingRegistry"], 65 | }, 66 | paths: { 67 | sources: "./contracts", 68 | tests: "./test", 69 | cache: "./build/cache", 70 | artifacts: "./build/artifacts", 71 | }, 72 | mocha: { 73 | timeout: 200000, // 200 seconds max for running tests 74 | }, 75 | } 76 | -------------------------------------------------------------------------------- /app/src/app/api/onchain-weather/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { Ratelimit } from '@upstash/ratelimit' 3 | import { kv } from '@vercel/kv' 4 | 5 | import { getWeatherOnchain, requestWeatherOnchain } from '@/lib/request-onchain' 6 | import { addToWeatherHistory } from '@/lib/history' 7 | 8 | const ratelimit = new Ratelimit({ 9 | redis: kv, 10 | limiter: Ratelimit.slidingWindow(3, '10 m'), 11 | }) 12 | 13 | const RATELIMIT_IP_EXCEPTION_LIST = 14 | process.env.RATELIMIT_IP_EXCEPTION_LIST?.split(',') || [] 15 | 16 | export async function POST(request: NextRequest) { 17 | const ip = request.ip ?? '127.0.0.1' 18 | 19 | // Only rate limit if IP is not in allowlist 20 | if (RATELIMIT_IP_EXCEPTION_LIST.indexOf(ip) == -1) { 21 | const { success } = await ratelimit.limit(ip) 22 | if (!success) 23 | return NextResponse.json({ error: 'Too many requests. Try again later.' }) 24 | } 25 | 26 | const params = await request.json() 27 | if (!params || !params.latitude || !params.longitude) 28 | return NextResponse.error() 29 | 30 | const result = await requestWeatherOnchain({ 31 | latitude: params.latitude, 32 | longitude: params.longitude, 33 | }) 34 | if (!result || !result.requestId) return NextResponse.error() 35 | 36 | const data = { 37 | requestId: result.requestId, 38 | txHash: result.tx.hash, 39 | } 40 | try { 41 | await addToWeatherHistory({ 42 | txHash: data.txHash, 43 | latitude: params.latitude, 44 | longitude: params.longitude, 45 | city: params.city, 46 | country: params.country, 47 | }) 48 | } catch (error) { 49 | console.log('Adding request to history failed.') 50 | } 51 | return NextResponse.json({ data }) 52 | } 53 | 54 | export async function GET(request: NextRequest) { 55 | const { searchParams } = new URL(request.url) 56 | const requestId = searchParams.get('requestId') || '' 57 | if (!requestId) return NextResponse.error() 58 | 59 | const data = await getWeatherOnchain(requestId) 60 | 61 | return NextResponse.json({ data }) 62 | } 63 | -------------------------------------------------------------------------------- /app/public/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /app/public/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-consumer/deployXConsumer.js: -------------------------------------------------------------------------------- 1 | const { types } = require("hardhat/config") 2 | const { networks } = require("../../networks") 3 | 4 | task("deploy-x-consumer", "Deploys the X User Consumer contract") 5 | .addParam("subid", "Billing subscription ID used to pay for the request") 6 | .addOptionalParam( 7 | "callbackgaslimit", 8 | "Maximum amount of gas that can be used to call fulfillRequest in the consumer contract", 9 | 200_000, 10 | types.int 11 | ) 12 | .setAction(async (taskArgs) => { 13 | console.log(`Deploying XUserDataConsumer contract to ${network.name}`) 14 | const subscriptionId = parseInt(taskArgs.subid) 15 | const callbackGasLimit = parseInt(taskArgs.callbackgaslimit) 16 | 17 | const functionsRouter = networks[network.name]["functionsRouter"] 18 | const donIdBytes32 = hre.ethers.utils.formatBytes32String(networks[network.name]["donId"]) 19 | 20 | console.log("\n__Compiling Contracts__") 21 | await run("compile") 22 | 23 | const overrides = {} 24 | // If specified, use the gas price from the network config instead of Ethers estimated price 25 | if (networks[network.name].gasPrice) { 26 | overrides.gasPrice = networks[network.name].gasPrice 27 | } 28 | // If specified, use the nonce from the network config instead of automatically calculating it 29 | if (networks[network.name].nonce) { 30 | overrides.nonce = networks[network.name].nonce 31 | } 32 | 33 | const deployArgs = [functionsRouter, donIdBytes32, subscriptionId, callbackGasLimit] 34 | 35 | const consumerContractFactory = await ethers.getContractFactory("XUserDataConsumer") 36 | const consumerContract = await consumerContractFactory.deploy(...deployArgs, overrides) 37 | 38 | console.log( 39 | `\nWaiting ${networks[network.name].confirmations} blocks for transaction ${ 40 | consumerContract.deployTransaction.hash 41 | } to be confirmed...` 42 | ) 43 | await consumerContract.deployTransaction.wait(networks[network.name].confirmations) 44 | 45 | console.log("\nDeployed XUserDataConsumer contract to:", consumerContract.address) 46 | 47 | await run("functions-upload-secrets-don", { 48 | slotid: "0", 49 | }) 50 | 51 | console.log(`\nX User Consumer contract deployed to ${consumerContract.address} on ${network.name}`) 52 | }) 53 | -------------------------------------------------------------------------------- /contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions-hardhat-starter-kit", 3 | "license": "MIT", 4 | "version": "0.1.0", 5 | "description": "Tooling for interacting with Chainlink Functions", 6 | "scripts": { 7 | "compile": "hardhat compile", 8 | "test": "npm run test:unit", 9 | "test:unit": "hardhat test test/unit/*.spec.js", 10 | "startLocalFunctionsTestnet": "node scripts/startLocalFunctionsTestnet.js", 11 | "listen": "nodemon scripts/listen.js", 12 | "lint": "npm run lint:contracts && npm run format:check", 13 | "lint:fix": "solhint 'contracts/**/*.sol' --fix", 14 | "lint:contracts": "solhint 'contracts/*.sol'", 15 | "lint:contracts:fix": "solhint 'contracts/**/*.sol' --fix", 16 | "format:check": "prettier --check .", 17 | "format:fix": "prettier --write ." 18 | }, 19 | "dependencies": { 20 | "@chainlink/contracts": "^0.7.1", 21 | "@chainlink/env-enc": "^1.0.5", 22 | "@chainlink/functions-toolkit": "^0.2.4", 23 | "@ethersproject/abi": "^5.7.0", 24 | "@ethersproject/providers": "^5.7.1", 25 | "@nomicfoundation/hardhat-chai-matchers": "^1.0.3", 26 | "@nomicfoundation/hardhat-network-helpers": "^1.0.6", 27 | "@nomicfoundation/hardhat-toolbox": "^2.0.0", 28 | "@nomiclabs/hardhat-ethers": "^2.2.2", 29 | "@nomiclabs/hardhat-etherscan": "^3.1.0", 30 | "@openzeppelin/contracts-upgradeable": "^4.9.3", 31 | "@typechain/ethers-v5": "^10.1.0", 32 | "@typechain/hardhat": "^6.1.3", 33 | "axios": "^1.1.3", 34 | "chai": "^4.3.6", 35 | "eth-crypto": "^2.4.0", 36 | "ethers": "^5.7.2", 37 | "hardhat": "^2.17.3", 38 | "hardhat-contract-sizer": "^2.6.1", 39 | "hardhat-gas-reporter": "^1.0.9", 40 | "lint-staged": "^13.0.3", 41 | "nodemon": "^3.0.1", 42 | "ora": "5.4.1", 43 | "prettier": "^2.7.1", 44 | "prettier-plugin-solidity": "^1.0.0-beta.24", 45 | "readline": "^1.3.0", 46 | "solhint": "^3.3.7", 47 | "solhint-plugin-prettier": "^0.0.5", 48 | "solidity-coverage": "^0.8.2", 49 | "typechain": "^8.1.0" 50 | }, 51 | "lint-staged": { 52 | "*.{js,json,yml,yaml}": [ 53 | "prettier --write" 54 | ], 55 | "*.sol": [ 56 | "prettier --write", 57 | "solhint" 58 | ] 59 | }, 60 | "prettier": { 61 | "trailingComma": "es5", 62 | "tabWidth": 2, 63 | "semi": false, 64 | "singleQuote": false, 65 | "printWidth": 120 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-billing/create.js: -------------------------------------------------------------------------------- 1 | const { SubscriptionManager } = require("@chainlink/functions-toolkit") 2 | const chalk = require("chalk") 3 | const { networks } = require("../../networks") 4 | const utils = require("../utils") 5 | 6 | task("functions-sub-create", "Creates a new billing subscription for Functions consumer contracts") 7 | .addOptionalParam("amount", "Initial amount used to fund the subscription in LINK") 8 | .addOptionalParam( 9 | "contract", 10 | "Address of the consumer contract address authorized to use the new billing subscription" 11 | ) 12 | .setAction(async (taskArgs) => { 13 | const signer = await ethers.getSigner() 14 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 15 | const linkTokenAddress = networks[network.name]["linkToken"] 16 | 17 | const linkAmount = taskArgs.amount 18 | const confirmations = linkAmount > 0 ? networks[network.name].confirmations : 1 19 | const consumerAddress = taskArgs.contract 20 | const txOptions = { confirmations } 21 | 22 | const sm = new SubscriptionManager({ signer, linkTokenAddress, functionsRouterAddress }) 23 | await sm.initialize() 24 | 25 | console.log("\nCreating Functions billing subscription...") 26 | const subscriptionId = await sm.createSubscription({ consumerAddress, txOptions }) 27 | console.log(`\nCreated Functions billing subscription: ${subscriptionId}`) 28 | 29 | // Fund subscription 30 | if (linkAmount) { 31 | await utils.prompt( 32 | `\nPlease confirm that you wish to fund Subscription ${subscriptionId} with ${chalk.blue( 33 | linkAmount + " LINK" 34 | )} from your wallet.` 35 | ) 36 | 37 | console.log(`\nFunding subscription ${subscriptionId} with ${linkAmount} LINK...`) 38 | const juelsAmount = ethers.utils.parseUnits(linkAmount, 18).toString() 39 | const fundTxReceipt = await sm.fundSubscription({ juelsAmount, subscriptionId, txOptions }) 40 | console.log( 41 | `\nSubscription ${subscriptionId} funded with ${linkAmount} LINK in Tx: ${fundTxReceipt.transactionHash}` 42 | ) 43 | 44 | const subInfo = await sm.getSubscriptionInfo(subscriptionId) 45 | // parse balances into LINK for readability 46 | subInfo.balance = ethers.utils.formatEther(subInfo.balance) + " LINK" 47 | subInfo.blockedBalance = ethers.utils.formatEther(subInfo.blockedBalance) + " LINK" 48 | 49 | console.log("\nSubscription Info: ", subInfo) 50 | } 51 | }) 52 | -------------------------------------------------------------------------------- /app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { fontFamily } = require('tailwindcss/defaultTheme') 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | darkMode: ['class'], 6 | content: ['./src/app/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}'], 7 | theme: { 8 | container: { 9 | center: true, 10 | screens: { 11 | '2xl': '1440px', 12 | }, 13 | }, 14 | extend: { 15 | colors: { 16 | border: 'hsl(var(--border))', 17 | input: 'hsl(var(--input))', 18 | ring: 'hsl(var(--ring))', 19 | background: 'hsl(var(--background))', 20 | foreground: 'hsl(var(--foreground))', 21 | primary: { 22 | DEFAULT: 'hsl(var(--primary))', 23 | foreground: 'hsl(var(--primary-foreground))', 24 | }, 25 | secondary: { 26 | DEFAULT: 'hsl(var(--secondary))', 27 | foreground: 'hsl(var(--secondary-foreground))', 28 | }, 29 | destructive: { 30 | DEFAULT: 'hsl(var(--destructive))', 31 | foreground: 'hsl(var(--destructive-foreground))', 32 | }, 33 | muted: { 34 | DEFAULT: 'hsl(var(--muted))', 35 | foreground: 'hsl(var(--muted-foreground))', 36 | }, 37 | accent: { 38 | DEFAULT: 'hsl(var(--accent))', 39 | foreground: 'hsl(var(--accent-foreground))', 40 | }, 41 | popover: { 42 | DEFAULT: 'hsl(var(--popover))', 43 | foreground: 'hsl(var(--popover-foreground))', 44 | }, 45 | card: { 46 | DEFAULT: 'hsl(var(--card))', 47 | foreground: 'hsl(var(--card-foreground))', 48 | }, 49 | }, 50 | borderRadius: { 51 | lg: `var(--radius)`, 52 | md: `calc(var(--radius) - 2px)`, 53 | sm: 'calc(var(--radius) - 4px)', 54 | }, 55 | fontFamily: { 56 | sans: ['var(--font-sans)', ...fontFamily.sans], 57 | }, 58 | keyframes: { 59 | 'accordion-down': { 60 | from: { height: 0 }, 61 | to: { height: 'var(--radix-accordion-content-height)' }, 62 | }, 63 | 'accordion-up': { 64 | from: { height: 'var(--radix-accordion-content-height)' }, 65 | to: { height: 0 }, 66 | }, 67 | }, 68 | animation: { 69 | 'accordion-down': 'accordion-down 0.2s ease-out', 70 | 'accordion-up': 'accordion-up 0.2s ease-out', 71 | }, 72 | }, 73 | }, 74 | plugins: [require('tailwindcss-animate')], 75 | } 76 | -------------------------------------------------------------------------------- /app/src/components/mobile-nav.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import Link from 'next/link' 5 | 6 | import { siteConfig } from '@/config/site' 7 | import { cn } from '@/lib/utils' 8 | import { Button, buttonVariants } from '@/components/ui/button' 9 | import { 10 | Popover, 11 | PopoverContent, 12 | PopoverTrigger, 13 | } from '@/components/ui/popover' 14 | import Image from 'next/image' 15 | 16 | export function MobileNav() { 17 | const [open, setOpen] = React.useState(false) 18 | 19 | return ( 20 | 21 | 22 | 34 | 35 | 36 |
37 | 46 | github 47 | 48 | Open in Github 49 | 50 | 51 | 60 | docs 61 | 62 | Functions Resources 63 | 64 | 65 |
66 |
67 |
68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /app/src/types.ts: -------------------------------------------------------------------------------- 1 | export type Coordinates = { 2 | latitude: number 3 | longitude: number 4 | } 5 | 6 | export type GeoLocation = { 7 | city: string 8 | state: string 9 | countryCode: string 10 | coordinates: Coordinates 11 | } 12 | 13 | export type WeatherResponse = { 14 | timezone: string 15 | current: { 16 | temperature_2m: number 17 | time: string 18 | interval: string 19 | weather_code: string 20 | } 21 | current_units: { 22 | temperature_2m: string 23 | time: string 24 | interval: string 25 | weather_code: string 26 | } 27 | } & Coordinates 28 | 29 | export type WeatherHistoryEntry = { 30 | txHash: string 31 | temperature: string 32 | timestamp: number 33 | temperatureUnit: string 34 | weatherCode: string 35 | city: string 36 | country: string 37 | } 38 | 39 | export type UserData = { 40 | id: string 41 | username: string 42 | name: string 43 | profile_image_url: string 44 | } 45 | 46 | export type UserError = { 47 | value: string 48 | detail: string 49 | title: string 50 | resource_type: string 51 | parameter: string 52 | resource_id: string 53 | type: string 54 | } 55 | 56 | export type DataResponse = { 57 | errors?: UserError[] 58 | user?: UserData 59 | tweet?: TweetData 60 | media?: string[] 61 | } 62 | 63 | export type UserDataResponse = { 64 | data?: UserData 65 | errors?: UserError[] 66 | } 67 | 68 | export type TweetData = { 69 | edit_history_tweet_ids: string[] 70 | id: string 71 | text: string 72 | created_at: string 73 | attachments?: { 74 | media_keys?: string[] 75 | } 76 | } 77 | 78 | export type MediaData = { 79 | media_key: string 80 | type: 'animated_gif' | 'photo' | 'video' 81 | url?: string 82 | preview_image_url?: string 83 | } 84 | 85 | export type LastTweetsResponse = { 86 | data: TweetData[] 87 | includes?: { 88 | media: MediaData[] 89 | } 90 | } 91 | 92 | export type TweetMediaResponse = { 93 | data?: TweetData 94 | includes?: { 95 | media: MediaData[] 96 | } 97 | errors?: { 98 | parameters: { 99 | [key: string]: string[] 100 | } 101 | message: string 102 | } 103 | } 104 | 105 | export type TweetHistoryEntry = { 106 | requestId: string 107 | txHash: string 108 | username: string 109 | name: string 110 | profileImageUrl: string 111 | tweetText: string 112 | timestamp: number 113 | media: string[] 114 | tweetId: string 115 | } 116 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-billing/timeoutRequests.js: -------------------------------------------------------------------------------- 1 | const { SubscriptionManager, fetchRequestCommitment } = require("@chainlink/functions-toolkit") 2 | const { networks } = require("../../networks") 3 | const { providers } = require("ethers") 4 | 5 | task( 6 | "functions-timeout-requests", 7 | "Times out expired Functions requests which have not been fulfilled within 5 minutes" 8 | ) 9 | .addParam("requestids", "1 or more request IDs to timeout separated by commas") 10 | .addOptionalParam("toblock", "Ending search block number (defaults to latest block)") 11 | .addOptionalParam("pastblockstosearch", "Number of past blocks to search", 1000, types.int) 12 | .setAction(async (taskArgs) => { 13 | const requestIdsToTimeout = taskArgs.requestids.split(",") 14 | console.log(`Timing out requests ${requestIdsToTimeout} on ${network.name}`) 15 | const toBlock = taskArgs.toblock ? Number(taskArgs.toblock) : "latest" 16 | const pastBlocksToSearch = parseInt(taskArgs.pastblockstosearch) 17 | 18 | const signer = await ethers.getSigner() 19 | const linkTokenAddress = networks[network.name]["linkToken"] 20 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 21 | const donId = networks[network.name]["donId"] 22 | const txOptions = { confirmations: networks[network.name].confirmations } 23 | 24 | const sm = new SubscriptionManager({ signer, linkTokenAddress, functionsRouterAddress }) 25 | await sm.initialize() 26 | 27 | const requestCommitments = [] 28 | for (const requestId of requestIdsToTimeout) { 29 | try { 30 | const requestCommitment = await fetchRequestCommitment({ 31 | requestId, 32 | provider: new providers.JsonRpcProvider(networks[network.name].url), 33 | functionsRouterAddress, 34 | donId, 35 | toBlock, 36 | pastBlocksToSearch, 37 | }) 38 | console.log(`Fetched commitment for request ID ${requestId}`) 39 | if (requestCommitment.timeoutTimestamp < BigInt(Math.round(Date.now() / 1000))) { 40 | requestCommitments.push(requestCommitment) 41 | } else { 42 | console.log(`Request ID ${requestId} has not expired yet (skipping)`) 43 | } 44 | } catch (error) { 45 | console.log(`Failed to fetch commitment for request ID ${requestId} (skipping): ${error}`) 46 | } 47 | } 48 | 49 | if (requestCommitments.length > 0) { 50 | await sm.timeoutRequests({ 51 | requestCommitments, 52 | txOptions, 53 | }) 54 | console.log("Requests successfully timed out") 55 | } 56 | }) 57 | -------------------------------------------------------------------------------- /app/public/onchain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /contracts/.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # hardhat 5 | artifacts 6 | cache 7 | deployments 8 | node_modules 9 | coverage 10 | coverage.json 11 | typechain 12 | 13 | # don't push the environment vars! 14 | *.env 15 | 16 | # These environment variables are encrypted, but should not be pushed unless a secure password is used 17 | offchain-encrypted-secrets.json 18 | .env.enc 19 | offchain-encrypted-secrets.json 20 | 21 | # Built application files 22 | .DS* 23 | *.apk 24 | *.ap_ 25 | *.aab 26 | 27 | # Files for the ART/Dalvik VM 28 | *.dex 29 | 30 | # Java class files 31 | *.class 32 | 33 | # Generated files 34 | bin/ 35 | gen/ 36 | out/ 37 | # Uncomment the following line in case you need and you don't have the release build type files in your app 38 | # release/ 39 | 40 | # Gradle files 41 | .gradle/ 42 | build/ 43 | 44 | # Local configuration file (sdk path, etc) 45 | local.properties 46 | 47 | # Proguard folder generated by Eclipse 48 | proguard/ 49 | 50 | # Log Files 51 | *.log 52 | 53 | # Android Studio Navigation editor temp files 54 | .navigation/ 55 | 56 | # Android Studio captures folder 57 | captures/ 58 | 59 | # IntelliJ 60 | *.iml 61 | .idea/workspace.xml 62 | .idea/tasks.xml 63 | .idea/gradle.xml 64 | .idea/assetWizardSettings.xml 65 | .idea/dictionaries 66 | .idea/libraries 67 | # Android Studio 3 in .gitignore file. 68 | .idea/caches 69 | .idea/modules.xml 70 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 71 | .idea/navEditor.xml 72 | 73 | # Keystore files 74 | # Uncomment the following lines if you do not want to check your keystore files in. 75 | #*.jks 76 | #*.keystore 77 | 78 | # External native build folder generated in Android Studio 2.2 and later 79 | .externalNativeBuild 80 | 81 | # Google Services (e.g. APIs or Firebase) 82 | # google-services.json 83 | 84 | # Freeline 85 | freeline.py 86 | freeline/ 87 | freeline_project_description.json 88 | 89 | # fastlane 90 | fastlane/report.xml 91 | fastlane/Preview.html 92 | fastlane/screenshots 93 | fastlane/test_output 94 | fastlane/readme.md 95 | 96 | # Version control 97 | vcs.xml 98 | 99 | # lint 100 | lint/intermediates/ 101 | lint/generated/ 102 | lint/outputs/ 103 | lint/tmp/ 104 | # lint/reports/ 105 | 106 | gas-report.txt 107 | 108 | contracts/test/fuzzing/crytic-export 109 | 110 | Functions-request.json 111 | offchain-secrets.json 112 | 113 | allowlist.csv 114 | invalidUsers.csv 115 | updatedAllowlist.csv 116 | 117 | # OpenZeppelin Upgrades local network artifacts 118 | .openzeppelin 119 | 120 | # Chainlink Functions request artifacts 121 | .chainlink_functions -------------------------------------------------------------------------------- /contracts/scripts/startLocalFunctionsTestnet.js: -------------------------------------------------------------------------------- 1 | const process = require("process") 2 | const path = require("path") 3 | const fs = require("fs") 4 | const { startLocalFunctionsTestnet } = require("@chainlink/functions-toolkit") 5 | const { utils, Wallet } = require("ethers") 6 | // Loads environment variables from .env.enc file (if it exists) 7 | require("@chainlink/env-enc").config("../.env.enc") 8 | ;(async () => { 9 | const requestConfigPath = path.join(process.cwd(), "Functions-request-config.js") // @dev Update this to point to your desired request config file 10 | console.log(`Using Functions request config file ${requestConfigPath}\n`) 11 | 12 | const localFunctionsTestnetInfo = await startLocalFunctionsTestnet( 13 | requestConfigPath, 14 | { 15 | logging: { 16 | debug: false, 17 | verbose: false, 18 | quiet: true, // Set this to `false` to see logs from the local testnet 19 | }, 20 | } // Ganache server options (optional) 21 | ) 22 | 23 | console.table({ 24 | "FunctionsRouter Contract Address": localFunctionsTestnetInfo.functionsRouterContract.address, 25 | "DON ID": localFunctionsTestnetInfo.donId, 26 | "Mock LINK Token Contract Address": localFunctionsTestnetInfo.linkTokenContract.address, 27 | }) 28 | 29 | // Fund wallets with ETH and LINK 30 | const addressToFund = new Wallet(process.env["PRIVATE_KEY"]).address 31 | await localFunctionsTestnetInfo.getFunds(addressToFund, { 32 | weiAmount: utils.parseEther("100").toString(), // 100 ETH 33 | juelsAmount: utils.parseEther("100").toString(), // 100 LINK 34 | }) 35 | if (process.env["SECOND_PRIVATE_KEY"]) { 36 | const secondAddressToFund = new Wallet(process.env["SECOND_PRIVATE_KEY"]).address 37 | await localFunctionsTestnetInfo.getFunds(secondAddressToFund, { 38 | weiAmount: utils.parseEther("100").toString(), // 100 ETH 39 | juelsAmount: utils.parseEther("100").toString(), // 100 LINK 40 | }) 41 | } 42 | 43 | // Update values in networks.js 44 | let networksConfig = fs.readFileSync(path.join(process.cwd(), "networks.js")).toString() 45 | const regex = /localFunctionsTestnet:\s*{\s*([^{}]*)\s*}/s 46 | const newContent = `localFunctionsTestnet: { 47 | url: "http://localhost:8545/", 48 | accounts, 49 | confirmations: 1, 50 | nativeCurrencySymbol: "ETH", 51 | linkToken: "${localFunctionsTestnetInfo.linkTokenContract.address}", 52 | functionsRouter: "${localFunctionsTestnetInfo.functionsRouterContract.address}", 53 | donId: "${localFunctionsTestnetInfo.donId}", 54 | }` 55 | networksConfig = networksConfig.replace(regex, newContent) 56 | fs.writeFileSync(path.join(process.cwd(), "networks.js"), networksConfig) 57 | })() 58 | -------------------------------------------------------------------------------- /contracts/tasks/Functions-consumer/uploadSecretsToDon.js: -------------------------------------------------------------------------------- 1 | const { SecretsManager } = require("@chainlink/functions-toolkit") 2 | const { networks } = require("../../networks") 3 | const process = require("process") 4 | const path = require("path") 5 | 6 | task("functions-upload-secrets-don", "Encrypts secrets and uploads them to the DON") 7 | .addParam( 8 | "slotid", 9 | "Storage slot number 0 or higher - if the slotid is already in use, the existing secrets for that slotid will be overwritten" 10 | ) 11 | .addOptionalParam( 12 | "ttl", 13 | "Time to live - minutes until the secrets hosted on the DON expire (defaults to 10, and must be at least 5)", 14 | 10, 15 | types.int 16 | ) 17 | .addOptionalParam( 18 | "configpath", 19 | "Path to Functions request config file", 20 | `${__dirname}/../../Functions-request-config.js`, 21 | types.string 22 | ) 23 | .setAction(async (taskArgs) => { 24 | const signer = await ethers.getSigner() 25 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 26 | const donId = networks[network.name]["donId"] 27 | 28 | const gatewayUrls = networks[network.name]["gatewayUrls"] 29 | 30 | const slotId = parseInt(taskArgs.slotid) 31 | const minutesUntilExpiration = taskArgs.ttl 32 | 33 | const secretsManager = new SecretsManager({ 34 | signer, 35 | functionsRouterAddress, 36 | donId, 37 | }) 38 | await secretsManager.initialize() 39 | 40 | // Get the secrets object from Functions-request-config.js or other specific request config. 41 | const requestConfig = require(path.isAbsolute(taskArgs.configpath) 42 | ? taskArgs.configpath 43 | : path.join(process.cwd(), taskArgs.configpath)) 44 | 45 | if (!requestConfig.secrets || requestConfig.secrets.length === 0) { 46 | console.log("No secrets found in the request config.") 47 | return 48 | } 49 | 50 | console.log("Encrypting secrets and uploading to DON...") 51 | const encryptedSecretsObj = await secretsManager.encryptSecrets(requestConfig.secrets) 52 | 53 | const { 54 | version, // Secrets version number (corresponds to timestamp when encrypted secrets were uploaded to DON) 55 | success, // Boolean value indicating if encrypted secrets were successfully uploaded to all nodes connected to the gateway 56 | } = await secretsManager.uploadEncryptedSecretsToDON({ 57 | encryptedSecretsHexstring: encryptedSecretsObj.encryptedSecrets, 58 | gatewayUrls, 59 | slotId, 60 | minutesUntilExpiration, 61 | }) 62 | 63 | console.log( 64 | `\nYou can now use slotId ${slotId} and version ${version} to reference the encrypted secrets hosted on the DON.` 65 | ) 66 | }) 67 | -------------------------------------------------------------------------------- /app/src/components/city-input.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useEffect, useState } from 'react' 4 | import { usePathname, useRouter, useSearchParams } from 'next/navigation' 5 | import Image from 'next/image' 6 | import { useDebounce } from '@/hooks/useDebounce' 7 | import { Button } from '@/components/ui/button' 8 | import AutoComplete, { Option } from '@/components/autocomplete' 9 | import { GeoLocation } from '@/types' 10 | 11 | export const CityInput = () => { 12 | const router = useRouter() 13 | const pathname = usePathname() 14 | const searchParams = useSearchParams() 15 | 16 | const [inputValue, setInputValue] = useState('') 17 | const valueDebounced = useDebounce(inputValue, 500) 18 | 19 | const [options, setOptions] = useState([]) 20 | const [loading, setLoading] = useState(false) 21 | const [value, setValue] = useState