diff --git a/packages/hardhat/contracts/FunctionsConsumer.sol b/packages/hardhat/contracts/FunctionsConsumer.sol index b057195..0cfe5b4 100644 --- a/packages/hardhat/contracts/FunctionsConsumer.sol +++ b/packages/hardhat/contracts/FunctionsConsumer.sol @@ -24,7 +24,7 @@ contract FunctionsConsumer is FunctionsClient, ConfirmedOwner { // Event to log responses event Response( bytes32 indexed requestId, - uint256 builderCount, + string weatherResult, bytes response, bytes err ); @@ -41,20 +41,10 @@ contract FunctionsConsumer is FunctionsClient, ConfirmedOwner { bytes32 donID; // State variables to hold function response data - uint256 public builderCount; + string public weatherResult; - // JavaScript source code - // Fetch character name from the Star Wars API. - // Documentation: https://swapi.dev/documentation#people - string source = - "const apiResponse = await Functions.makeHttpRequest({" - "url: `https://buidlguidl-v3.ew.r.appspot.com/api/stats`});" - "if (apiResponse.error) {" - "throw Error('Request failed');" - "}" - "const { data } = apiResponse;" - "const builderCount = data.builderCount;" - "return Functions.encodeUint256(builderCount);"; + // JavaScript source code to fetch weather data from the OpenWeather API + string public weatherSource; /** * @notice Initializes the contract with the Chainlink router address and sets the contract owner @@ -63,21 +53,29 @@ contract FunctionsConsumer is FunctionsClient, ConfirmedOwner { address _router, uint64 _subscriptionId, uint32 _gasLimit, - bytes32 _donID + bytes32 _donID, + string memory _weatherSource ) FunctionsClient(_router) ConfirmedOwner(msg.sender) { router = _router; subscriptionId = _subscriptionId; gasLimit = _gasLimit; donID = _donID; + weatherSource = _weatherSource; } /** - * @notice Sends an HTTP request for Buidl Guidl stats + * @notice Sends an HTTP request to fetch weather data from the OpenWeather API + * @param args String arguments passed into the source code and accessible via the global variable `args` + * arg 1: the zip code + * arg 2: the ISO 3166 country code * @return requestId The ID of the request */ - function sendRequest() external onlyOwner returns (bytes32 requestId) { + function sendRequest( + string[] calldata args + ) external onlyOwner returns (bytes32 requestId) { FunctionsRequest.Request memory req; - req.initializeRequestForInlineJavaScript(source); // Initialize the request with JS code + req.initializeRequestForInlineJavaScript(weatherSource); // Initialize the request with JS code + if (args.length > 0) req.setArgs(args); // Set the arguments for the request // Send the request and store the request ID s_lastRequestId = _sendRequest( @@ -106,10 +104,10 @@ contract FunctionsConsumer is FunctionsClient, ConfirmedOwner { } // Update the contract's state variables with the response and any errors s_lastResponse = response; - builderCount = abi.decode(response, (uint256)); + weatherResult = string(response); s_lastError = err; // Emit an event to log the response - emit Response(requestId, builderCount, s_lastResponse, s_lastError); + emit Response(requestId, weatherResult, s_lastResponse, s_lastError); } } diff --git a/packages/hardhat/deploy/05_FunctionsConsumer.ts b/packages/hardhat/deploy/05_FunctionsConsumer.ts index a70fa34..7f6e935 100644 --- a/packages/hardhat/deploy/05_FunctionsConsumer.ts +++ b/packages/hardhat/deploy/05_FunctionsConsumer.ts @@ -1,6 +1,8 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { DeployFunction } from "hardhat-deploy/types"; import { networkConfig } from "../helper-hardhat-config"; +import fs from "fs"; +import path from "path"; /** Deploy FunctionsConsumer contract * @param hre HardhatRuntimeEnvironment object. @@ -14,9 +16,15 @@ const functionsConsumer: DeployFunction = async function (hre: HardhatRuntimeEnv log("------------------------------------"); const chainId = await hre.ethers.provider.getNetwork().then(network => network.chainId); + const { routerAddress, subscriptionId, gasLimit, donId } = networkConfig[chainId].FunctionsConsumer; + const weatherSourceScriptPath = path.join(__dirname, "../functions-source-scripts/fetch-weather-data.js"); + const weatherSourceScript = fs.readFileSync(weatherSourceScriptPath, "utf8"); + + console.log("weatherSourceScriptPath", weatherSourceScriptPath); + console.log("weatherSourceScript", weatherSourceScript); - const args = [routerAddress, subscriptionId, gasLimit, donId]; + const args = [routerAddress, subscriptionId, gasLimit, donId, weatherSourceScript]; const FunctionsConsumer = await deploy("FunctionsConsumer", { from: deployer, diff --git a/packages/hardhat/functions-source-scripts/fetch-weather-data.js b/packages/hardhat/functions-source-scripts/fetch-weather-data.js new file mode 100644 index 0000000..dfda3b3 --- /dev/null +++ b/packages/hardhat/functions-source-scripts/fetch-weather-data.js @@ -0,0 +1,72 @@ +// This function fetches the latest temperature for a particular area from openweathermap API +// Args include the zipcode of your location, ISO 3166 country code +// units- unit in which we want the temperature (standard, metric, imperial) + +if (!secrets.apiKey) { + throw Error("Weather API Key is not available!"); +} + +const zipCode = `${args[0]},${args[1]}`; + +const geoCodingURL = "http://api.openweathermap.org/geo/1.0/zip?"; + +console.log(`Sending HTTP request to ${geoCodingURL}zip=${zipCode}`); + +const geoCodingRequest = Functions.makeHttpRequest({ + url: geoCodingURL, + method: "GET", + params: { + zip: zipCode, + appid: secrets.apiKey, + }, +}); + +const geoCodingResponse = await geoCodingRequest; + +if (geoCodingResponse.error) { + console.error(geoCodingResponse.error); + throw Error("Request failed, try checking the params provided"); +} + +console.log(geoCodingResponse); + +const latitude = geoCodingResponse.data.lat; +const longitude = geoCodingResponse.data.lon; +const unit = args[2]; + +const url = `https://api.openweathermap.org/data/2.5/weather?`; + +console.log(`Sending HTTP request to ${url}lat=${latitude}&lon=${longitude}&units=${unit}`); + +const weatherRequest = Functions.makeHttpRequest({ + url: url, + method: "GET", + params: { + lat: latitude, + lon: longitude, + appid: secrets.apiKey, + units: unit, + }, +}); + +// Execute the API request (Promise) +const weatherResponse = await weatherRequest; +if (weatherResponse.error) { + console.error(weatherResponse.error); + throw Error("Request failed, try checking the params provided"); +} + +// gets the current temperature +const temperature = weatherResponse.data.main.temp; + +// Gives the whole response from the request +console.log("Weather response", weatherResponse); + +// result is in JSON object, containing only temperature +const result = { + temp: temperature, +}; + +// Use JSON.stringify() to convert from JSON object to JSON string +// Finally, use the helper Functions.encodeString() to encode from string to bytes +return Functions.encodeString(JSON.stringify(result)); diff --git a/packages/hardhat/tasks/upload-secrets-to-DON.ts b/packages/hardhat/tasks/upload-secrets-to-DON.ts new file mode 100644 index 0000000..5066512 --- /dev/null +++ b/packages/hardhat/tasks/upload-secrets-to-DON.ts @@ -0,0 +1,68 @@ +// TODO: convert this to typescript + +// const { SecretsManager } = require("@chainlink/functions-toolkit") +// const { networks } = require("../../networks") +// const process = require("process") +// const path = require("path") + +// task("functions-upload-secrets-don", "Encrypts secrets and uploads them to the DON") +// .addParam( +// "slotid", +// "Storage slot number 0 or higher - if the slotid is already in use, the existing secrets for that slotid will be overwritten" +// ) +// .addOptionalParam( +// "ttl", +// "Time to live - minutes until the secrets hosted on the DON expire (defaults to 10, and must be at least 5)", +// 10, +// types.int +// ) +// .addOptionalParam( +// "configpath", +// "Path to Functions request config file", +// `${__dirname}/../../Functions-request-config.js`, +// types.string +// ) +// .setAction(async (taskArgs) => { +// const signer = await ethers.getSigner() +// const functionsRouterAddress = networks[network.name]["functionsRouter"] +// const donId = networks[network.name]["donId"] + +// const gatewayUrls = networks[network.name]["gatewayUrls"] + +// const slotId = parseInt(taskArgs.slotid) +// const minutesUntilExpiration = taskArgs.ttl + +// const secretsManager = new SecretsManager({ +// signer, +// functionsRouterAddress, +// donId, +// }) +// await secretsManager.initialize() + +// // Get the secrets object from Functions-request-config.js or other specific request config. +// const requestConfig = require(path.isAbsolute(taskArgs.configpath) +// ? taskArgs.configpath +// : path.join(process.cwd(), taskArgs.configpath)) + +// if (!requestConfig.secrets || requestConfig.secrets.length === 0) { +// console.log("No secrets found in the request config.") +// return +// } + +// console.log("Encrypting secrets and uploading to DON...") +// const encryptedSecretsObj = await secretsManager.encryptSecrets(requestConfig.secrets) + +// const { +// version, // Secrets version number (corresponds to timestamp when encrypted secrets were uploaded to DON) +// success, // Boolean value indicating if encrypted secrets were successfully uploaded to all nodes connected to the gateway +// } = await secretsManager.uploadEncryptedSecretsToDON({ +// encryptedSecretsHexstring: encryptedSecretsObj.encryptedSecrets, +// gatewayUrls, +// slotId, +// minutesUntilExpiration, +// }) + +// console.log( +// `\nYou can now use slotId ${slotId} and version ${version} to reference the encrypted secrets hosted on the DON.` +// ) +// }) diff --git a/packages/nextjs/components/functions/Showcase.tsx b/packages/nextjs/components/functions/Showcase.tsx index 9ac2920..02cf788 100644 --- a/packages/nextjs/components/functions/Showcase.tsx +++ b/packages/nextjs/components/functions/Showcase.tsx @@ -1,24 +1,34 @@ import { ExternalLinkButton } from "~~/components/common"; import { Address } from "~~/components/scaffold-eth"; -import { useScaffoldContract, useScaffoldContractRead, useScaffoldContractWrite } from "~~/hooks/scaffold-eth"; +import { + useScaffoldContract, // useScaffoldContractRead, useScaffoldContractWrite +} from "~~/hooks/scaffold-eth"; export const Showcase = () => { const { data: functionsConsumerContract } = useScaffoldContract({ contractName: "FunctionsConsumer" }); - const { writeAsync: updateBuilderCount } = useScaffoldContractWrite({ - contractName: "FunctionsConsumer", - functionName: "sendRequest", - onBlockConfirmation: txnReceipt => { - console.log("Transaction blockHash", txnReceipt.blockHash); - }, - }); + // const { writeAsync: fetchWeatherData } = useScaffoldContractWrite({ + // contractName: "FunctionsConsumer", + // functionName: "sendRequest", + // args: [["94521", "US"]], + // }); - const { data: builderCount } = useScaffoldContractRead({ - contractName: "FunctionsConsumer", - functionName: "builderCount", - }); + // const { data: weatherResult } = useScaffoldContractRead({ + // contractName: "FunctionsConsumer", + // functionName: "weatherResult", + // }); - console.log("builderCount", builderCount); + // const { data: s_lastError } = useScaffoldContractRead({ + // contractName: "FunctionsConsumer", + // functionName: "s_lastError", + // }); + + // const { data: s_lastResponse } = useScaffoldContractRead({ + // contractName: "FunctionsConsumer", + // functionName: " s_lastResponse", + // }); + + // console.log(weatherResult); return (
@@ -34,14 +44,23 @@ export const Showcase = () => {

- Chainlink functions are used to send request from FunctionsConsumer contract to the decentralized oracle - network (DON) which executes API request to Buidl Guidl server in off chain environment and returns the - result on chain where the FunctionsConsumer contract stores the response in a state variable + Chainlink functions are used to send a request from a smart contract to a decentralized oracle network (DON) + which executes the provided source code in off chain environment and returns the result on chain through the + `fulfillRequest` function

+
+

TODO

+
    +
  1. Figure out how to upload encrypted secret to DON
  2. +
  3. Set up vercel serverless function with cron job to upload secret once per day
  4. +
  5. Set up function to return weather data
  6. +
+
+
-

On Chain Stats

-
+

On Chain Weather

+ {/*
@@ -58,39 +77,7 @@ export const Showcase = () => {
-
-
-
-
0
-
Build Count
-
-
-
- -
-
-
-
-
-
0
-
Stream ETH
-
-
-
- -
-
-
+
*/}
diff --git a/packages/nextjs/generated/deployedContracts.ts b/packages/nextjs/generated/deployedContracts.ts index 3ed1abd..4801594 100644 --- a/packages/nextjs/generated/deployedContracts.ts +++ b/packages/nextjs/generated/deployedContracts.ts @@ -604,7 +604,7 @@ const contracts = { ], }, FunctionsConsumer: { - address: "0xCa71c907547f65D301Ae5EEb1e5FB56A79E66eA6", + address: "0x7855b9755E8A1704cd59288214fd50387e05c50C", abi: [ { inputs: [ @@ -628,10 +628,20 @@ const contracts = { name: "_donID", type: "bytes32", }, + { + internalType: "string", + name: "_weatherSource", + type: "string", + }, ], stateMutability: "nonpayable", type: "constructor", }, + { + inputs: [], + name: "EmptyArgs", + type: "error", + }, { inputs: [], name: "EmptySource", @@ -733,9 +743,9 @@ const contracts = { }, { indexed: false, - internalType: "uint256", - name: "builderCount", - type: "uint256", + internalType: "string", + name: "weatherResult", + type: "string", }, { indexed: false, @@ -760,19 +770,6 @@ const contracts = { stateMutability: "nonpayable", type: "function", }, - { - inputs: [], - name: "builderCount", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, { inputs: [ { @@ -849,7 +846,13 @@ const contracts = { type: "function", }, { - inputs: [], + inputs: [ + { + internalType: "string[]", + name: "args", + type: "string[]", + }, + ], name: "sendRequest", outputs: [ { @@ -874,6 +877,32 @@ const contracts = { stateMutability: "nonpayable", type: "function", }, + { + inputs: [], + name: "weatherResult", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "weatherSource", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, ], }, VRFConsumer: {