From d6479bdf2d4404ab0bd947874404e715a43a3961 Mon Sep 17 00:00:00 2001 From: Jay White Date: Wed, 13 Nov 2024 12:18:14 -0500 Subject: [PATCH] feat: copy chainlink example code ``` cp documentation/public/samples/ChainlinkFunctions/FunctionsConsumerExample.sol smart-contract/FunctionsConsumerExample.sol cp -r smart-contract-examples/functions-examples/examples/5-use-secrets-threshold example cp -r smart-contract-examples/functions-examples/package.json . ``` --- chainlink-functions/example/request.js | 250 ++++++++++++++++++ chainlink-functions/example/source.js | 47 ++++ chainlink-functions/package.json | 16 ++ .../FunctionsConsumerExample.sol | 112 ++++++++ 4 files changed, 425 insertions(+) create mode 100644 chainlink-functions/example/request.js create mode 100644 chainlink-functions/example/source.js create mode 100644 chainlink-functions/package.json create mode 100644 chainlink-functions/smart-contract/FunctionsConsumerExample.sol diff --git a/chainlink-functions/example/request.js b/chainlink-functions/example/request.js new file mode 100644 index 0000000..7c975a2 --- /dev/null +++ b/chainlink-functions/example/request.js @@ -0,0 +1,250 @@ +const fs = require("fs"); +const path = require("path"); +const { + SubscriptionManager, + SecretsManager, + simulateScript, + ResponseListener, + ReturnType, + decodeResult, + FulfillmentCode, +} = require("@chainlink/functions-toolkit"); +const functionsConsumerAbi = require("../../abi/functionsClient.json"); +const ethers = require("ethers"); +require("@chainlink/env-enc").config(); + +const consumerAddress = "0x8dFf78B7EE3128D00E90611FBeD20A71397064D9"; // REPLACE this with your Functions consumer address +const subscriptionId = 3; // REPLACE this with your subscription ID + +const makeRequestSepolia = async () => { + // hardcoded for Ethereum Sepolia + const routerAddress = "0xb83E47C2bC239B3bf370bc41e1459A34b41238D0"; + const linkTokenAddress = "0x779877A7B0D9E8603169DdbD7836e478b4624789"; + const donId = "fun-ethereum-sepolia-1"; + const explorerUrl = "https://sepolia.etherscan.io"; + const gatewayUrls = [ + "https://01.functions-gateway.testnet.chain.link/", + "https://02.functions-gateway.testnet.chain.link/", + ]; + + // Initialize functions settings + const source = fs + .readFileSync(path.resolve(__dirname, "source.js")) + .toString(); + + const args = ["1", "USD"]; + const secrets = { apiKey: process.env.COINMARKETCAP_API_KEY }; + const slotIdNumber = 0; // slot ID where to upload the secrets + const expirationTimeMinutes = 15; // expiration time in minutes of the secrets + const gasLimit = 300000; + + // Initialize ethers signer and provider to interact with the contracts onchain + const privateKey = process.env.PRIVATE_KEY; // fetch PRIVATE_KEY + if (!privateKey) + throw new Error( + "private key not provided - check your environment variables" + ); + + const rpcUrl = process.env.ETHEREUM_SEPOLIA_RPC_URL; // fetch Sepolia RPC URL + + if (!rpcUrl) + throw new Error(`rpcUrl not provided - check your environment variables`); + + const provider = new ethers.providers.JsonRpcProvider(rpcUrl); + + const wallet = new ethers.Wallet(privateKey); + const signer = wallet.connect(provider); // create ethers signer for signing transactions + + ///////// START SIMULATION //////////// + + console.log("Start simulation..."); + + const response = await simulateScript({ + source: source, + args: args, + bytesArgs: [], // bytesArgs - arguments can be encoded off-chain to bytes. + secrets: secrets, + }); + + console.log("Simulation result", response); + const errorString = response.errorString; + if (errorString) { + console.log(`❌ Error during simulation: `, errorString); + } else { + const returnType = ReturnType.uint256; + const responseBytesHexstring = response.responseBytesHexstring; + if (ethers.utils.arrayify(responseBytesHexstring).length > 0) { + const decodedResponse = decodeResult( + response.responseBytesHexstring, + returnType + ); + console.log(`✅ Decoded response to ${returnType}: `, decodedResponse); + } + } + + //////// ESTIMATE REQUEST COSTS //////// + console.log("\nEstimate request costs..."); + // Initialize and return SubscriptionManager + const subscriptionManager = new SubscriptionManager({ + signer: signer, + linkTokenAddress: linkTokenAddress, + functionsRouterAddress: routerAddress, + }); + await subscriptionManager.initialize(); + + // estimate costs in Juels + + const gasPriceWei = await signer.getGasPrice(); // get gasPrice in wei + + const estimatedCostInJuels = + await subscriptionManager.estimateFunctionsRequestCost({ + donId: donId, // ID of the DON to which the Functions request will be sent + subscriptionId: subscriptionId, // Subscription ID + callbackGasLimit: gasLimit, // Total gas used by the consumer contract's callback + gasPriceWei: BigInt(gasPriceWei), // Gas price in gWei + }); + + console.log( + `Fulfillment cost estimated to ${ethers.utils.formatEther( + estimatedCostInJuels + )} LINK` + ); + + //////// MAKE REQUEST //////// + + console.log("\nMake request..."); + + // First encrypt secrets and upload the encrypted secrets to the DON + const secretsManager = new SecretsManager({ + signer: signer, + functionsRouterAddress: routerAddress, + donId: donId, + }); + await secretsManager.initialize(); + + // Encrypt secrets and upload to DON + const encryptedSecretsObj = await secretsManager.encryptSecrets(secrets); + + console.log( + `Upload encrypted secret to gateways ${gatewayUrls}. slotId ${slotIdNumber}. Expiration in minutes: ${expirationTimeMinutes}` + ); + // Upload secrets + const uploadResult = await secretsManager.uploadEncryptedSecretsToDON({ + encryptedSecretsHexstring: encryptedSecretsObj.encryptedSecrets, + gatewayUrls: gatewayUrls, + slotId: slotIdNumber, + minutesUntilExpiration: expirationTimeMinutes, + }); + + if (!uploadResult.success) + throw new Error(`Encrypted secrets not uploaded to ${gatewayUrls}`); + + console.log( + `\n✅ Secrets uploaded properly to gateways ${gatewayUrls}! Gateways response: `, + uploadResult + ); + + const donHostedSecretsVersion = parseInt(uploadResult.version); // fetch the reference of the encrypted secrets + + const functionsConsumer = new ethers.Contract( + consumerAddress, + functionsConsumerAbi, + signer + ); + + // Actual transaction call + const transaction = await functionsConsumer.sendRequest( + source, // source + "0x", // user hosted secrets - encryptedSecretsUrls - empty in this example + slotIdNumber, // slot ID of the encrypted secrets + donHostedSecretsVersion, // version of the encrypted secrets + args, + [], // bytesArgs - arguments can be encoded off-chain to bytes. + subscriptionId, + gasLimit, + ethers.utils.formatBytes32String(donId) // jobId is bytes32 representation of donId + ); + + // Log transaction details + console.log( + `\n✅ Functions request sent! Transaction hash ${transaction.hash}. Waiting for a response...` + ); + + console.log( + `See your request in the explorer ${explorerUrl}/tx/${transaction.hash}` + ); + + const responseListener = new ResponseListener({ + provider: provider, + functionsRouterAddress: routerAddress, + }); // Instantiate a ResponseListener object to wait for fulfillment. + (async () => { + try { + const response = await new Promise((resolve, reject) => { + responseListener + .listenForResponseFromTransaction(transaction.hash) + .then((response) => { + resolve(response); // Resolves once the request has been fulfilled. + }) + .catch((error) => { + reject(error); // Indicate that an error occurred while waiting for fulfillment. + }); + }); + + const fulfillmentCode = response.fulfillmentCode; + + if (fulfillmentCode === FulfillmentCode.FULFILLED) { + console.log( + `\n✅ Request ${ + response.requestId + } successfully fulfilled. Cost is ${ethers.utils.formatEther( + response.totalCostInJuels + )} LINK.Complete reponse: `, + response + ); + } else if (fulfillmentCode === FulfillmentCode.USER_CALLBACK_ERROR) { + console.log( + `\n⚠️ Request ${ + response.requestId + } fulfilled. However, the consumer contract callback failed. Cost is ${ethers.utils.formatEther( + response.totalCostInJuels + )} LINK.Complete reponse: `, + response + ); + } else { + console.log( + `\n❌ Request ${ + response.requestId + } not fulfilled. Code: ${fulfillmentCode}. Cost is ${ethers.utils.formatEther( + response.totalCostInJuels + )} LINK.Complete reponse: `, + response + ); + } + + const errorString = response.errorString; + if (errorString) { + console.log(`\n❌ Error during the execution: `, errorString); + } else { + const responseBytesHexstring = response.responseBytesHexstring; + if (ethers.utils.arrayify(responseBytesHexstring).length > 0) { + const decodedResponse = decodeResult( + response.responseBytesHexstring, + ReturnType.uint256 + ); + console.log( + `\n✅ Decoded response to ${ReturnType.uint256}: `, + decodedResponse + ); + } + } + } catch (error) { + console.error("Error listening for response:", error); + } + })(); +}; + +makeRequestSepolia().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/chainlink-functions/example/source.js b/chainlink-functions/example/source.js new file mode 100644 index 0000000..0f4e6d5 --- /dev/null +++ b/chainlink-functions/example/source.js @@ -0,0 +1,47 @@ +// This example shows how to make call an API using a secret +// https://coinmarketcap.com/api/documentation/v1/ + +// Arguments can be provided when a request is initated on-chain and used in the request source code as shown below +const coinMarketCapCoinId = args[0]; +const currencyCode = args[1]; + +if (!secrets.apiKey) { + throw Error( + "COINMARKETCAP_API_KEY environment variable not set for CoinMarketCap API. Get a free key from https://coinmarketcap.com/api/" + ); +} + +// build HTTP request object + +const coinMarketCapRequest = Functions.makeHttpRequest({ + url: `https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest`, + // Get a free API key from https://coinmarketcap.com/api/ + headers: { + "Content-Type": "application/json", + "X-CMC_PRO_API_KEY": secrets.apiKey, + }, + params: { + convert: currencyCode, + id: coinMarketCapCoinId, + }, +}); + +// Make the HTTP request +const coinMarketCapResponse = await coinMarketCapRequest; + +if (coinMarketCapResponse.error) { + throw new Error("CoinMarketCap Error"); +} + +// fetch the price +const price = + coinMarketCapResponse.data.data[coinMarketCapCoinId]["quote"][currencyCode][ + "price" + ]; + +console.log(`Price: ${price.toFixed(2)} ${currencyCode}`); + +// price * 100 to move by 2 decimals (Solidity doesn't support decimals) +// Math.round() to round to the nearest integer +// Functions.encodeUint256() helper function to encode the result from uint256 to bytes +return Functions.encodeUint256(Math.round(price * 100)); diff --git a/chainlink-functions/package.json b/chainlink-functions/package.json new file mode 100644 index 0000000..a1d27b1 --- /dev/null +++ b/chainlink-functions/package.json @@ -0,0 +1,16 @@ +{ + "name": "functions-examples", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@chainlink/env-enc": "^1.0.5", + "@chainlink/functions-toolkit": "^0.2.7", + "ethers": "^5.7.2" + } +} diff --git a/chainlink-functions/smart-contract/FunctionsConsumerExample.sol b/chainlink-functions/smart-contract/FunctionsConsumerExample.sol new file mode 100644 index 0000000..8b28421 --- /dev/null +++ b/chainlink-functions/smart-contract/FunctionsConsumerExample.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {FunctionsClient} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/FunctionsClient.sol"; +import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol"; +import {FunctionsRequest} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/libraries/FunctionsRequest.sol"; + +/** + * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. + * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. + * DO NOT USE THIS CODE IN PRODUCTION. + */ +contract FunctionsConsumerExample is FunctionsClient, ConfirmedOwner { + using FunctionsRequest for FunctionsRequest.Request; + + bytes32 public s_lastRequestId; + bytes public s_lastResponse; + bytes public s_lastError; + + error UnexpectedRequestID(bytes32 requestId); + + event Response(bytes32 indexed requestId, bytes response, bytes err); + + constructor( + address router + ) FunctionsClient(router) ConfirmedOwner(msg.sender) {} + + /** + * @notice Send a simple request + * @param source JavaScript source code + * @param encryptedSecretsUrls Encrypted URLs where to fetch user secrets + * @param donHostedSecretsSlotID Don hosted secrets slotId + * @param donHostedSecretsVersion Don hosted secrets version + * @param args List of arguments accessible from within the source code + * @param bytesArgs Array of bytes arguments, represented as hex strings + * @param subscriptionId Billing ID + */ + function sendRequest( + string memory source, + bytes memory encryptedSecretsUrls, + uint8 donHostedSecretsSlotID, + uint64 donHostedSecretsVersion, + string[] memory args, + bytes[] memory bytesArgs, + uint64 subscriptionId, + uint32 gasLimit, + bytes32 donID + ) external onlyOwner returns (bytes32 requestId) { + FunctionsRequest.Request memory req; + req.initializeRequestForInlineJavaScript(source); + if (encryptedSecretsUrls.length > 0) + req.addSecretsReference(encryptedSecretsUrls); + else if (donHostedSecretsVersion > 0) { + req.addDONHostedSecrets( + donHostedSecretsSlotID, + donHostedSecretsVersion + ); + } + if (args.length > 0) req.setArgs(args); + if (bytesArgs.length > 0) req.setBytesArgs(bytesArgs); + s_lastRequestId = _sendRequest( + req.encodeCBOR(), + subscriptionId, + gasLimit, + donID + ); + return s_lastRequestId; + } + + /** + * @notice Send a pre-encoded CBOR request + * @param request CBOR-encoded request data + * @param subscriptionId Billing ID + * @param gasLimit The maximum amount of gas the request can consume + * @param donID ID of the job to be invoked + * @return requestId The ID of the sent request + */ + function sendRequestCBOR( + bytes memory request, + uint64 subscriptionId, + uint32 gasLimit, + bytes32 donID + ) external onlyOwner returns (bytes32 requestId) { + s_lastRequestId = _sendRequest( + request, + subscriptionId, + gasLimit, + donID + ); + return s_lastRequestId; + } + + /** + * @notice Store latest result/error + * @param requestId The request ID, returned by sendRequest() + * @param response Aggregated response from the user code + * @param err Aggregated error from the user code or from the execution pipeline + * Either response or error parameter will be set, but never both + */ + function fulfillRequest( + bytes32 requestId, + bytes memory response, + bytes memory err + ) internal override { + if (s_lastRequestId != requestId) { + revert UnexpectedRequestID(requestId); + } + s_lastResponse = response; + s_lastError = err; + emit Response(requestId, s_lastResponse, s_lastError); + } +}