From 6859dd4c12835413ccafa06da21f93124cc5dbac Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Thu, 7 Sep 2023 18:33:07 -0500 Subject: [PATCH 01/12] Local testnet simulator implementation --- src/localFunctionsTestnet.ts | 122 ++++++++--- src/simulateScript/simulateScript.ts | 3 +- src/simulationConfig.ts | 2 + test/integration/ResponseListener.test.ts | 10 +- test/integration/integration.test.ts | 14 +- .../integration/localFunctionsTestnet.test.ts | 198 ++++++++++++++++++ test/unit/simulateScript.test.ts | 4 - 7 files changed, 315 insertions(+), 38 deletions(-) create mode 100644 test/integration/localFunctionsTestnet.test.ts diff --git a/src/localFunctionsTestnet.ts b/src/localFunctionsTestnet.ts index 2723b81..9334d4d 100644 --- a/src/localFunctionsTestnet.ts +++ b/src/localFunctionsTestnet.ts @@ -13,6 +13,7 @@ import { callReportGasLimit, simulatedSecretsKeys, simulatedTransmitters, + numberOfSimulatedNodeExecutions, } from './simulationConfig' import { LinkTokenSource, @@ -23,9 +24,9 @@ import { FunctionsClientExampleSource, } from './v1_contract_sources' -import type { Server, Ethereum } from 'ganache' +import type { Server, ServerOptions } from 'ganache' -import type { FunctionsRequestParams, RequestCommitment } from './types' +import type { FunctionsRequestParams, RequestCommitment, SimulationResult } from './types' export interface RequestEventData { requestId: string @@ -49,6 +50,8 @@ interface FunctionsContracts { export const startLocalFunctionsTestnet = async ( port = 8545, + secrets?: Record, + options?: ServerOptions, ): Promise< { server: Server @@ -63,13 +66,7 @@ export const startLocalFunctionsTestnet = async ( close: () => Promise } & FunctionsContracts > => { - const server = Ganache.server({ - logging: { - debug: false, - verbose: false, - quiet: true, - }, - }) + const server = Ganache.server(options) server.listen(port, 'localhost', (err: Error | null) => { if (err) { @@ -113,7 +110,7 @@ export const startLocalFunctionsTestnet = async ( callbackGasLimit, commitment, } - handleOracleRequest(requestEvent, contracts.mockCoordinator, admin) + handleOracleRequest(requestEvent, contracts.mockCoordinator, admin, secrets) }, ) @@ -155,14 +152,11 @@ const handleOracleRequest = async ( requestEventData: RequestEventData, mockCoordinator: Contract, admin: Wallet, + secrets: Record = {}, ) => { const requestData = await constructRequestDataObject(requestEventData.data) - const response = await simulateScript({ - source: requestData.source, - secrets: {}, // TODO: Decrypt secrets - args: requestData.args, - // TODO: Support bytes args - }) + const response = await simulateDONExecution(requestData, secrets) + const errorHexstring = response.errorString ? '0x' + Buffer.from(response.errorString.toString()).toString('hex') : undefined @@ -183,6 +177,76 @@ const handleOracleRequest = async ( await reportTx.wait(1) } +const simulateDONExecution = async ( + requestData: FunctionsRequestParams, + secrets: Record, +): Promise<{ responseBytesHexstring?: string; errorString?: string }> => { + // Perform the simulation numberOfSimulatedNodeExecution times + const simulations = [...Array(numberOfSimulatedNodeExecutions)].map(() => + simulateScript({ + source: requestData.source, + secrets, + args: requestData.args, + bytesArgs: requestData.bytesArgs, + }), + ) + const responses = await Promise.all(simulations) + + const successfulResponses = responses.filter(response => response.errorString === undefined) + const errorResponses = responses.filter(response => response.errorString !== undefined) + + if (successfulResponses.length > errorResponses.length) { + return { + responseBytesHexstring: aggregateMedian( + successfulResponses.map(response => response.responseBytesHexstring!), + ), + } + } else { + return { + errorString: aggregateModeString(errorResponses.map(response => response.errorString!)), + } + } +} + +const aggregateMedian = (responses: string[]): string => { + const bufResponses = responses.map(response => Buffer.from(response.slice(2), 'hex')) + + bufResponses.sort((a, b) => { + if (a.length !== b.length) { + return a.length - b.length + } + for (let i = 0; i < a.length; ++i) { + if (a[i] !== b[i]) { + return a[i] - b[i] + } + } + return 0 + }) + + return '0x' + bufResponses[Math.floor((bufResponses.length - 1) / 2)].toString('hex') +} + +const aggregateModeString = (items: string[]): string => { + const counts = new Map() + + for (const str of items) { + const existingCount = counts.get(str) || 0 + counts.set(str, existingCount + 1) + } + + let modeString = items[0] + let maxCount = counts.get(modeString) || 0 + + for (const [str, count] of counts.entries()) { + if (count > maxCount) { + maxCount = count + modeString = str + } + } + + return modeString +} + const encodeReport = ( requestId: string, commitment: RequestCommitment, @@ -226,35 +290,37 @@ const encodeReport = ( const constructRequestDataObject = async (requestData: string): Promise => { const decodedRequestData = await cbor.decodeAll(Buffer.from(requestData.slice(2), 'hex')) + const requestDataObject = {} as FunctionsRequestParams for (let i = 0; i < decodedRequestData.length - 1; i += 2) { const elem = decodedRequestData[i] - // TODO: support encrypted secrets & bytesArgs switch (elem) { case 'codeLocation': requestDataObject.codeLocation = decodedRequestData[i + 1] break - // case 'secretsLocation': - // requestDataObject.secretsLocation = decodedRequestData[i + 1] - // break + case 'secretsLocation': + // Unused as secrets provided as an argument to startLocalFunctionsTestnet() are used instead + break case 'language': requestDataObject.codeLanguage = decodedRequestData[i + 1] break case 'source': requestDataObject.source = decodedRequestData[i + 1] break - // case 'encryptedSecretsReference': - // requestDataObject.encryptedSecretsReference = decodedRequestData[i + 1] - // break + case 'secrets': + // Unused as secrets provided as an argument to startLocalFunctionsTestnet() are used instead + break case 'args': requestDataObject.args = decodedRequestData[i + 1] break - // case 'bytesArgs': - // requestDataObject.bytesArgs = decodedRequestData[i + 1] - // break - // default: - // throw Error(`Invalid request data key ${elem}`) + case 'bytesArgs': + requestDataObject.bytesArgs = decodedRequestData[i + 1].map((bytesArg: Buffer) => { + return '0x' + bytesArg.toString('hex') + }) + break + default: + // Ignore unknown keys } } diff --git a/src/simulateScript/simulateScript.ts b/src/simulateScript/simulateScript.ts index dd5dc32..ba1bd67 100644 --- a/src/simulateScript/simulateScript.ts +++ b/src/simulateScript/simulateScript.ts @@ -74,8 +74,7 @@ export const simulateScript = async ({ // Check if deno is installed try { - const result = execSync('deno --version', { stdio: 'pipe' }) - console.log(`Performing simulation with the following versions:\n${result.toString()}\n`) + execSync('deno --version', { stdio: 'pipe' }) } catch { throw Error( 'Deno must be installed and accessible via the PATH environment variable (ie: the `deno --version` command must work).\nVisit https://deno.land/#installation for installation instructions.', diff --git a/src/simulationConfig.ts b/src/simulationConfig.ts index b3c8907..6e14e6a 100644 --- a/src/simulationConfig.ts +++ b/src/simulationConfig.ts @@ -31,6 +31,8 @@ export const simulatedAllowListConfig = { export const callReportGasLimit = 5_000_000 +export const numberOfSimulatedNodeExecutions = 4 + export const simulatedWallets = { node0: { address: '0xAe24F6e7e046a0C764DF51F333dE5e2fE360AC72', diff --git a/test/integration/ResponseListener.test.ts b/test/integration/ResponseListener.test.ts index 2e8f9b8..d8e7532 100644 --- a/test/integration/ResponseListener.test.ts +++ b/test/integration/ResponseListener.test.ts @@ -20,8 +20,14 @@ describe('Functions toolkit classes', () => { let allowlistedUser_A: Wallet beforeAll(async () => { - const port = 9501 - const localFunctionsTestnet = await startLocalFunctionsTestnet(port) + const port = 8002 + const localFunctionsTestnet = await startLocalFunctionsTestnet(port, undefined, { + logging: { + debug: false, + verbose: false, + quiet: true, + }, + }) linkTokenAddress = localFunctionsTestnet.linkToken.address functionsRouterAddress = localFunctionsTestnet.router.address diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts index 5fb540a..f90aea8 100644 --- a/test/integration/integration.test.ts +++ b/test/integration/integration.test.ts @@ -28,7 +28,14 @@ describe('Functions toolkit classes', () => { let subFunder_A: Wallet beforeAll(async () => { - const localFunctionsTestnet = await startLocalFunctionsTestnet() + const port = 8001 + const localFunctionsTestnet = await startLocalFunctionsTestnet(port, undefined, { + logging: { + debug: false, + verbose: false, + quiet: true, + }, + }) linkTokenContract = localFunctionsTestnet.linkToken linkTokenAddress = localFunctionsTestnet.linkToken.address @@ -38,7 +45,10 @@ describe('Functions toolkit classes', () => { consumerAddress = localFunctionsTestnet.exampleClient.address close = localFunctionsTestnet.close - const [admin, walletA, walletB, walletC, _] = createTestWallets(localFunctionsTestnet.server) + const [admin, walletA, walletB, walletC, _] = createTestWallets( + localFunctionsTestnet.server, + port, + ) allowlistedUser_A = walletA allowlistedUser_B_NoLINK = walletB subFunder_A = walletC diff --git a/test/integration/localFunctionsTestnet.test.ts b/test/integration/localFunctionsTestnet.test.ts new file mode 100644 index 0000000..7bce321 --- /dev/null +++ b/test/integration/localFunctionsTestnet.test.ts @@ -0,0 +1,198 @@ +import { + SubscriptionManager, + startLocalFunctionsTestnet, + simulatedDonId, + decodeResult, + ResponseListener, + ReturnType, +} from '../../src' +import { createTestWallets } from '../utils' + +import { Contract, Wallet, utils } from 'ethers' + +describe('Local Functions Testnet', () => { + let linkTokenAddress: string + let functionsRouterAddress: string + let exampleClient: Contract + let close: () => Promise + + let allowlistedUser_A: Wallet + + beforeAll(async () => { + const mockSecrets = { + test: 'hello world', + } + + const port = 8003 + const localFunctionsTestnet = await startLocalFunctionsTestnet(port, mockSecrets, { + logging: { + debug: false, + verbose: false, + quiet: true, + }, + }) + + linkTokenAddress = localFunctionsTestnet.linkToken.address + functionsRouterAddress = localFunctionsTestnet.router.address + exampleClient = localFunctionsTestnet.exampleClient + close = localFunctionsTestnet.close + + const [_admin, walletA, _] = createTestWallets(localFunctionsTestnet.server, port) + allowlistedUser_A = walletA + + await localFunctionsTestnet.getFunds(allowlistedUser_A.address, { + ethAmount: 0, + linkAmount: 100, + }) + }) + + afterAll(async () => { + await close() + }) + + it('Successfully fulfills a request', async () => { + const subscriptionManager = new SubscriptionManager({ + signer: allowlistedUser_A, + linkTokenAddress, + functionsRouterAddress, + }) + await subscriptionManager.initialize() + + const subscriptionId = await subscriptionManager.createSubscription() + await subscriptionManager.fundSubscription({ + juelsAmount: utils.parseUnits('1', 'ether').toString(), + subscriptionId, + }) + await subscriptionManager.addConsumer({ + subscriptionId, + consumerAddress: exampleClient.address, + txOptions: { + confirmations: 1, + }, + }) + + const functionsListener = new ResponseListener({ + provider: allowlistedUser_A.provider, + functionsRouterAddress, + }) + + const reqTx = await exampleClient.sendRequest( + { + codeLocation: 0, + secretsLocation: 1, + language: 0, + source: + 'return Functions.encodeString(secrets.test + " " + args[0] + " " + args[1] + bytesArgs[0] + bytesArgs[1])', + encryptedSecretsReference: '0xabcd', + requestSignature: [], + args: ['hello', 'world'], + bytesArgs: ['0x1234', '0x5678'], + }, + subscriptionId, + utils.formatBytes32String(simulatedDonId), + ) + + const req = await reqTx.wait(1) + const requestId = req.events[0].topics[1] + const response = await functionsListener.listenForResponse(requestId) + + const responseString = decodeResult(response.responseBytesHexstring, ReturnType.string) + expect(responseString).toBe('hello world hello world0x12340x5678') + }) + + it('Successfully aggregates a random number', async () => { + const subscriptionManager = new SubscriptionManager({ + signer: allowlistedUser_A, + linkTokenAddress, + functionsRouterAddress, + }) + await subscriptionManager.initialize() + + const subscriptionId = await subscriptionManager.createSubscription() + await subscriptionManager.fundSubscription({ + juelsAmount: utils.parseUnits('1', 'ether').toString(), + subscriptionId, + }) + await subscriptionManager.addConsumer({ + subscriptionId, + consumerAddress: exampleClient.address, + txOptions: { + confirmations: 1, + }, + }) + + const functionsListener = new ResponseListener({ + provider: allowlistedUser_A.provider, + functionsRouterAddress, + }) + + const reqTx = await exampleClient.sendRequest( + { + codeLocation: 0, + secretsLocation: 1, + language: 0, + source: 'return Functions.encodeUint256(Math.floor(Math.random() * 1_000_000_000))', + encryptedSecretsReference: '0xabcd', + requestSignature: [], + args: ['hello', 'world'], + bytesArgs: ['0x1234', '0x5678'], + }, + subscriptionId, + utils.formatBytes32String(simulatedDonId), + ) + + const req = await reqTx.wait(1) + const requestId = req.events[0].topics[1] + const response = await functionsListener.listenForResponse(requestId) + + expect(response.responseBytesHexstring.length).toBeGreaterThan(2) + }) + + it('Successfully aggregates a random error', async () => { + const subscriptionManager = new SubscriptionManager({ + signer: allowlistedUser_A, + linkTokenAddress, + functionsRouterAddress, + }) + await subscriptionManager.initialize() + + const subscriptionId = await subscriptionManager.createSubscription() + await subscriptionManager.fundSubscription({ + juelsAmount: utils.parseUnits('1', 'ether').toString(), + subscriptionId, + }) + await subscriptionManager.addConsumer({ + subscriptionId, + consumerAddress: exampleClient.address, + txOptions: { + confirmations: 1, + }, + }) + + const functionsListener = new ResponseListener({ + provider: allowlistedUser_A.provider, + functionsRouterAddress, + }) + + const reqTx = await exampleClient.sendRequest( + { + codeLocation: 0, + secretsLocation: 1, + language: 0, + source: 'throw Error(`${Math.floor(Math.random() * 100)}`)', + encryptedSecretsReference: '0xabcd', + requestSignature: [], + args: ['hello', 'world'], + bytesArgs: ['0x1234', '0x5678'], + }, + subscriptionId, + utils.formatBytes32String(simulatedDonId), + ) + + const req = await reqTx.wait(1) + const requestId = req.events[0].topics[1] + const response = await functionsListener.listenForResponse(requestId) + + expect(parseInt(response.errorString)).toBeGreaterThan(0) + }) +}) diff --git a/test/unit/simulateScript.test.ts b/test/unit/simulateScript.test.ts index 51d0ca3..7a9df2f 100644 --- a/test/unit/simulateScript.test.ts +++ b/test/unit/simulateScript.test.ts @@ -28,14 +28,10 @@ describe('simulateScript', () => { const server = createTestServer() const port = (server.address() as AddressInfo).port - console.log(port) - const result = await simulateScript({ source: `const response = await fetch('http://localhost:${port}'); const jsonResponse = await response.json(); console.log(jsonResponse); return Functions.encodeString(jsonResponse.message);`, }) - console.log(result) - const expected = { capturedTerminalOutput: '{ message: "Hello, world!" }\n', responseBytesHexstring: '0x48656c6c6f2c20776f726c6421', From 833b4f4653658b3372823fcc73a5c17022b5f218 Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Fri, 8 Sep 2023 14:26:55 -0500 Subject: [PATCH 02/12] Added README & refactored tests --- README.md | 42 ++++++++ package-lock.json | 9 +- src/localFunctionsTestnet.ts | 101 ++++++++---------- src/types.ts | 38 ++++++- test/integration/ResponseListener.test.ts | 34 ++---- test/integration/integration.test.ts | 50 +++------ .../integration/localFunctionsTestnet.test.ts | 57 +++++----- test/utils/index.ts | 80 +++++++++++++- 8 files changed, 249 insertions(+), 162 deletions(-) diff --git a/README.md b/README.md index 22edd30..a834039 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ The typical subscriptions-related operations are - [Functions Response Listener](#functions-response-listener) - [Functions Utilities](#functions-utilities) - [Local Functions Simulator](#local-functions-simulator) + - [Local Functions Testnet](#local-functions-testnet) - [Decoding Response Bytes](#decoding-response-bytes) - [Storing Encrypted Secrets in Gists](#storing-encrypted-secrets-in-gists) - [Building Functions Request CBOR Bytes](#building-functions-request-cbor-bytes) @@ -535,8 +536,49 @@ const result = await simulateScript({ } ``` +**_NOTE:_** When running `simulateScript`, depending on your security settings, you may get a popup asking if you would like to accept incoming network connections. You can safely ignore this popup and it should disappear when the simulation is complete. + **_NOTE:_** The `simulateScript` function is a debugging tool and hence is not a perfect representation of the actual Chainlink oracle execution environment. Therefore, it is important to make a Functions request on a supported testnet blockchain before mainnet usage. +### Local Functions Testnet + +For debugging smart contracts and the end-to-end request flow on your local machine, you can use the `localFunctionsTestnet` function. This creates a local testnet RPC node with a mock Chainlink Functions contracts. You can then deploy your own Functions consumer contract to this local network, create and manage subscriptions, and send requests. Request processing will simulate the behavior of an actual DON where the request is executed 4 times and the discrete median response is transmitted back to the consumer contract. (Note that Functions uses the following calculation to select the discrete median response: `const medianResponse = responses[responses.length - 1) / 2]`). + +The `localFunctionsTestnet` function takes the following values as arguments. + +``` +const localFunctionsTestnet = await startLocalFunctionsTestnet( + secrets?: Record, // Secrets which can be accessed by the JavaScript code during request execution + options?: ServerOptions, // Ganache server options + port?: number, // Defaults to 8545 +) +``` + +Observe that `localFunctionsTestnet` takes in a secrets object as an optional argument. This is because the local testnet does not have the ability to access or decrypt encrypted secrets provided within the request transaction. Instead, you can provide secrets as an argument here which can be accessed by the JavaScript code during request executions. Secrets specified as an argument to `localFunctionsTestnet` will be made accessible within the JavaScript code regardless of the `secretsLocation` or `encryptedSecretsReference` values sent in the request transaction. + +`localFunctionsTestnet` returns a promise which resolves to the following type. + +``` +{ + server: Server // Ganache server + adminWallet: { address: string, privateKey: string } // Funded admin wallet + getFunds: (address: string, { weiAmount, juelsAmount }: { weiAmount?: BigInt | string; juelsAmount?: BigInt | string }) => Promise // Method which can be called to send funds to any address + close: () => Promise // Method to close the server + donId: string // DON ID for simulated DON + // The following values are all Ethers.js contract types: https://docs.ethers.org/v5/api/contract/contract/ + linkTokenContract: Contract // Mock LINK token contract + functionsRouterContract: Contract // Mock FunctionsRouter contract +} +``` + +Now you can connect to the local Functions testnet RPC node with your preferred blockchain tooling, deploy a FunctionsConsumer contract, instantiate and initialize the`SubscriptionManager`, create, add the consumer contract and fund the subscription, send requests, and use the `ResponseListener` to listen for responses all on your machine. + +**_NOTE:_** When simulating request executions, depending on your security settings, you may get multiple popups asking if you would like to accept incoming network connections. You can safely ignore these popups and they should disappear when the executions are complete. + +**_NOTE:_** Cost estimates and other configuration values may differ significantly from actual values on live testnet or mainnet chains. + +**_NOTE:_** The `localFunctionsTestnet` function is a debugging tool and hence is not a perfect representation of the actual Chainlink oracle execution environment. Therefore, it is important to make a Functions request on a supported testnet blockchain before mainnet usage. + ### Decoding Response Bytes On-chain responses are encoded as Solidity `bytes` which are most frequently displayed as hex strings. However, these hex strings often need to be decoded into a useable type. In order to decode hex strings into human-readable values, this package provides the `decodeResult` function. Currently, the `decodeResult` function supports decoding hex strings into `uint256`, `int256` or `string` values. diff --git a/package-lock.json b/package-lock.json index f6294cb..2d769de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@chainlink/functions-toolkit", - "version": "0.0.2", + "version": "0.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@chainlink/functions-toolkit", - "version": "0.0.2", + "version": "0.0.3", "license": "MIT", "dependencies": { "axios": "^1.4.0", @@ -6955,7 +6955,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -8662,9 +8661,7 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "extraneous": true, "os": [ - "darwin", - "linux", - "win32" + "darwin" ], "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" diff --git a/src/localFunctionsTestnet.ts b/src/localFunctionsTestnet.ts index 9334d4d..347df56 100644 --- a/src/localFunctionsTestnet.ts +++ b/src/localFunctionsTestnet.ts @@ -21,51 +21,24 @@ import { FunctionsRouterSource, MockFunctionsCoordinatorSource, TermsOfServiceAllowListSource, - FunctionsClientExampleSource, } from './v1_contract_sources' -import type { Server, ServerOptions } from 'ganache' - -import type { FunctionsRequestParams, RequestCommitment, SimulationResult } from './types' - -export interface RequestEventData { - requestId: string - requestingContract: string - requestInitiator: string - subscriptionId: any - subscriptionOwner: string - data: string - dataVersion: number - flags: string - callbackGasLimit: number - commitment: RequestCommitment -} +import type { ServerOptions } from 'ganache' -interface FunctionsContracts { - linkToken: Contract - router: Contract - mockCoordinator: Contract - exampleClient: Contract -} +import type { + FunctionsRequestParams, + RequestCommitment, + LocalFunctionsTestnet, + GetFunds, + FunctionsContracts, + RequestEventData, +} from './types' export const startLocalFunctionsTestnet = async ( - port = 8545, secrets?: Record, options?: ServerOptions, -): Promise< - { - server: Server - adminWallet: { - address: string - privateKey: string - } - getFunds: ( - address: string, - { ethAmount, linkAmount }: { ethAmount: number; linkAmount: number }, - ) => Promise - close: () => Promise - } & FunctionsContracts -> => { + port = 8545, +): Promise => { const server = Ganache.server(options) server.listen(port, 'localhost', (err: Error | null) => { @@ -84,7 +57,7 @@ export const startLocalFunctionsTestnet = async ( const contracts = await deployFunctionsOracle(admin) - contracts.mockCoordinator.on( + contracts.functionsMockCoordinatorContract.on( 'OracleRequest', ( requestId, @@ -110,29 +83,41 @@ export const startLocalFunctionsTestnet = async ( callbackGasLimit, commitment, } - handleOracleRequest(requestEvent, contracts.mockCoordinator, admin, secrets) + handleOracleRequest(requestEvent, contracts.functionsMockCoordinatorContract, admin, secrets) }, ) - const getFunds = async ( - address: string, - { ethAmount, linkAmount }: { ethAmount: number; linkAmount: number }, - ): Promise => { - const weiAmount = utils.parseEther(ethAmount.toString()) - const juelsAmount = utils.parseEther(linkAmount.toString()) - + const getFunds: GetFunds = async (address, { weiAmount, juelsAmount }) => { + if (!juelsAmount) { + juelsAmount = BigInt(0) + } + if (!weiAmount) { + weiAmount = BigInt(0) + } + if (typeof weiAmount !== 'string' && typeof weiAmount !== 'bigint') { + throw Error(`weiAmount must be a BigInt or string, got ${typeof weiAmount}`) + } + if (typeof juelsAmount !== 'string' && typeof juelsAmount !== 'bigint') { + throw Error(`juelsAmount must be a BigInt or string, got ${typeof juelsAmount}`) + } + weiAmount = BigInt(weiAmount) + juelsAmount = BigInt(juelsAmount) const ethTx = await admin.sendTransaction({ to: address, - value: weiAmount, + value: weiAmount.toString(), }) - const linkTx = await contracts.linkToken.connect(admin).transfer(address, juelsAmount) + const linkTx = await contracts.linkTokenContract.connect(admin).transfer(address, juelsAmount) await ethTx.wait(1) await linkTx.wait(1) - console.log(`Sent ${ethAmount} ETH and ${linkAmount} LINK to ${address}`) + console.log( + `Sent ${utils.formatEther(weiAmount.toString())} ETH and ${utils.formatEther( + juelsAmount.toString(), + )} LINK to ${address}`, + ) } const close = async (): Promise => { - contracts.mockCoordinator.removeAllListeners('OracleRequest') + contracts.functionsMockCoordinatorContract.removeAllListeners('OracleRequest') await server.close() } @@ -369,13 +354,6 @@ export const deployFunctionsOracle = async (deployer: Wallet): Promise Promise + +export type LocalFunctionsTestnet = { + server: Server + adminWallet: { + address: string + privateKey: string + } + getFunds: GetFunds + close: () => Promise +} & FunctionsContracts diff --git a/test/integration/ResponseListener.test.ts b/test/integration/ResponseListener.test.ts index d8e7532..6ab1221 100644 --- a/test/integration/ResponseListener.test.ts +++ b/test/integration/ResponseListener.test.ts @@ -4,10 +4,9 @@ import { FunctionsResponse, SubscriptionManager, ResponseListener, - startLocalFunctionsTestnet, simulatedDonId, } from '../../src' -import { createTestWallets } from '../utils' +import { setupLocalTestnet } from '../utils' import { Contract, Wallet, utils } from 'ethers' @@ -16,34 +15,15 @@ describe('Functions toolkit classes', () => { let functionsRouterAddress: string let exampleClient: Contract let close: () => Promise - let allowlistedUser_A: Wallet beforeAll(async () => { - const port = 8002 - const localFunctionsTestnet = await startLocalFunctionsTestnet(port, undefined, { - logging: { - debug: false, - verbose: false, - quiet: true, - }, - }) - - linkTokenAddress = localFunctionsTestnet.linkToken.address - functionsRouterAddress = localFunctionsTestnet.router.address - exampleClient = localFunctionsTestnet.exampleClient - close = localFunctionsTestnet.close - - const [admin, walletA, walletB, walletC, _] = createTestWallets( - localFunctionsTestnet.server, - port, - ) - allowlistedUser_A = walletA - - await localFunctionsTestnet.getFunds(allowlistedUser_A.address, { - ethAmount: 0, - linkAmount: 100, - }) + const testSetup = await setupLocalTestnet(8002) + linkTokenAddress = testSetup.linkTokenAddress + functionsRouterAddress = testSetup.functionsRouterAddress + exampleClient = testSetup.exampleConsumer + close = testSetup.close + allowlistedUser_A = testSetup.user_A }) afterAll(async () => { diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts index f90aea8..bec9981 100644 --- a/test/integration/integration.test.ts +++ b/test/integration/integration.test.ts @@ -2,13 +2,12 @@ import EthCrypto from 'eth-crypto' import { SubscriptionManager, SecretsManager, - startLocalFunctionsTestnet, RequestCommitment, simulatedDonId, simulatedSecretsKeys, } from '../../src' import { mockOffchainSecretsEndpoints, mockGatewayUrl } from './apiFixture' -import { createTestWallets } from '../utils' +import { setupLocalTestnet } from '../utils' import { BigNumber, Contract, Wallet, utils } from 'ethers' @@ -21,46 +20,23 @@ describe('Functions toolkit classes', () => { let exampleClient: Contract let consumerAddress: string let close: () => Promise - let allowlistedUser_A: Wallet let allowlistedUser_B_NoLINK: Wallet - // let unallowlistedUser: Wallet let subFunder_A: Wallet beforeAll(async () => { - const port = 8001 - const localFunctionsTestnet = await startLocalFunctionsTestnet(port, undefined, { - logging: { - debug: false, - verbose: false, - quiet: true, - }, - }) - - linkTokenContract = localFunctionsTestnet.linkToken - linkTokenAddress = localFunctionsTestnet.linkToken.address - functionsRouterAddress = localFunctionsTestnet.router.address - functionsCoordinator = localFunctionsTestnet.mockCoordinator - exampleClient = localFunctionsTestnet.exampleClient - consumerAddress = localFunctionsTestnet.exampleClient.address - close = localFunctionsTestnet.close - - const [admin, walletA, walletB, walletC, _] = createTestWallets( - localFunctionsTestnet.server, - port, - ) - allowlistedUser_A = walletA - allowlistedUser_B_NoLINK = walletB - subFunder_A = walletC - - await localFunctionsTestnet.getFunds(allowlistedUser_A.address, { - ethAmount: 0, - linkAmount: 100, - }) - await localFunctionsTestnet.getFunds(subFunder_A.address, { - ethAmount: 0, - linkAmount: 100, - }) + const testSetup = await setupLocalTestnet(8001) + donId = testSetup.donId + linkTokenContract = testSetup.linkTokenContract + linkTokenAddress = testSetup.linkTokenAddress + functionsCoordinator = testSetup.functionsCoordinator + functionsRouterAddress = testSetup.functionsRouterAddress + exampleClient = testSetup.exampleConsumer + consumerAddress = testSetup.exampleConsumerAddress + close = testSetup.close + allowlistedUser_A = testSetup.user_A + allowlistedUser_B_NoLINK = testSetup.user_B_NoLINK + subFunder_A = testSetup.subFunder }) afterAll(async () => { diff --git a/test/integration/localFunctionsTestnet.test.ts b/test/integration/localFunctionsTestnet.test.ts index 7bce321..7fb49e8 100644 --- a/test/integration/localFunctionsTestnet.test.ts +++ b/test/integration/localFunctionsTestnet.test.ts @@ -1,49 +1,34 @@ import { SubscriptionManager, - startLocalFunctionsTestnet, simulatedDonId, decodeResult, ResponseListener, ReturnType, } from '../../src' -import { createTestWallets } from '../utils' +import { setupLocalTestnet } from '../utils' -import { Contract, Wallet, utils } from 'ethers' +import { utils } from 'ethers' + +import type { GetFunds } from '../../src' + +import type { Contract, Wallet } from 'ethers' describe('Local Functions Testnet', () => { let linkTokenAddress: string let functionsRouterAddress: string let exampleClient: Contract let close: () => Promise - let allowlistedUser_A: Wallet + let getFunds: GetFunds beforeAll(async () => { - const mockSecrets = { - test: 'hello world', - } - - const port = 8003 - const localFunctionsTestnet = await startLocalFunctionsTestnet(port, mockSecrets, { - logging: { - debug: false, - verbose: false, - quiet: true, - }, - }) - - linkTokenAddress = localFunctionsTestnet.linkToken.address - functionsRouterAddress = localFunctionsTestnet.router.address - exampleClient = localFunctionsTestnet.exampleClient - close = localFunctionsTestnet.close - - const [_admin, walletA, _] = createTestWallets(localFunctionsTestnet.server, port) - allowlistedUser_A = walletA - - await localFunctionsTestnet.getFunds(allowlistedUser_A.address, { - ethAmount: 0, - linkAmount: 100, - }) + const testSetup = await setupLocalTestnet(8003) + linkTokenAddress = testSetup.linkTokenAddress + functionsRouterAddress = testSetup.functionsRouterAddress + exampleClient = testSetup.exampleConsumer + close = testSetup.close + allowlistedUser_A = testSetup.user_A + getFunds = testSetup.getFunds }) afterAll(async () => { @@ -195,4 +180,18 @@ describe('Local Functions Testnet', () => { expect(parseInt(response.errorString)).toBeGreaterThan(0) }) + + it('getFunds throws error for invalid weiAmount', async () => { + expect(async () => { + // @ts-ignore + await getFunds('0xc0ffee254729296a45a3885639AC7E10F9d54979', { weiAmount: 1 }) + }).rejects.toThrow(/weiAmount must be a BigInt or string/) + }) + + it('getFunds throws error for invalid juelsAmount', async () => { + expect(async () => { + // @ts-ignore + await getFunds('0xc0ffee254729296a45a3885639AC7E10F9d54979', { juelsAmount: 1 }) + }).rejects.toThrow(/juelsAmount must be a BigInt or string/) + }) }) diff --git a/test/utils/index.ts b/test/utils/index.ts index 55f61d4..093f4f4 100644 --- a/test/utils/index.ts +++ b/test/utils/index.ts @@ -1,9 +1,83 @@ -import { Wallet, providers } from 'ethers' +import { startLocalFunctionsTestnet } from '../../src' +import { FunctionsClientExampleSource } from '../../src/v1_contract_sources' + +import { Wallet, providers, ContractFactory, utils } from 'ethers' + +import type { GetFunds } from '../../src' + +import type { Contract } from 'ethers' import type { Server } from 'ganache' -export const createTestWallets = (server: Server, port = 8545): Wallet[] => { +export const setupLocalTestnet = async ( + port: number, +): Promise<{ + donId: string + linkTokenContract: Contract + linkTokenAddress: string + functionsCoordinator: Contract + functionsRouterAddress: string + exampleConsumer: Contract + exampleConsumerAddress: string + close: () => Promise + user_A: Wallet + user_B_NoLINK: Wallet + subFunder: Wallet + getFunds: GetFunds +}> => { + const localFunctionsTestnet = await startLocalFunctionsTestnet( + { test: 'hello world' }, + { + logging: { + debug: false, + verbose: false, + quiet: true, + }, + }, + port, + ) + + const provider = new providers.JsonRpcProvider(`http://localhost:${port}/`) + const admin = new Wallet(localFunctionsTestnet.adminWallet.privateKey, provider) + const functionsTestConsumerContractFactory = new ContractFactory( + FunctionsClientExampleSource.abi, + FunctionsClientExampleSource.bytecode, + admin, + ) + const exampleConsumer = await functionsTestConsumerContractFactory + .connect(admin) + .deploy(localFunctionsTestnet.functionsRouterContract.address) + + const [_admin, user_A, user_B_NoLINK, subFunder, _] = createTestWallets( + localFunctionsTestnet.server, + port, + ) + + const juelsAmount = BigInt(utils.parseUnits('100', 'ether').toString()) + await localFunctionsTestnet.getFunds(user_A.address, { + juelsAmount, + }) + await localFunctionsTestnet.getFunds(subFunder.address, { + juelsAmount, + }) + + return { + donId: localFunctionsTestnet.donId, + linkTokenContract: localFunctionsTestnet.linkTokenContract, + linkTokenAddress: localFunctionsTestnet.linkTokenContract.address, + functionsCoordinator: localFunctionsTestnet.functionsMockCoordinatorContract, + functionsRouterAddress: localFunctionsTestnet.functionsRouterContract.address, + exampleConsumer: exampleConsumer, + exampleConsumerAddress: exampleConsumer.address, + close: localFunctionsTestnet.close, + user_A, + user_B_NoLINK, + subFunder, + getFunds: localFunctionsTestnet.getFunds, + } +} + +const createTestWallets = (server: Server, port = 8545): Wallet[] => { const accounts = server.provider.getInitialAccounts() - const [addr0, addr1, addr2, addr3, addr4, addr5] = Object.keys(accounts) const wallets: Wallet[] = [] const provider = new providers.JsonRpcProvider(`http://localhost:${port}`) From 1c32c42c6be4cde34707d4f9192fcdb2279ecf41 Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Fri, 8 Sep 2023 14:29:09 -0500 Subject: [PATCH 03/12] Fix workflow for Linux --- .github/workflows/cd.yml | 2 +- .github/workflows/prettier.yaml | 2 +- .github/workflows/test-converage.yaml | 2 +- .github/workflows/test-package.yaml | 2 +- .github/workflows/test.yaml | 2 +- src/localFunctionsTestnet.ts | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 8db3b4e..7d31dcc 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -27,7 +27,7 @@ jobs: registry-url: 'https://registry.npmjs.org' - name: Run npm ci - run: npm ci + run: npm ci --no-optional - name: Setup project run: npm run build diff --git a/.github/workflows/prettier.yaml b/.github/workflows/prettier.yaml index 5807b37..3a01144 100644 --- a/.github/workflows/prettier.yaml +++ b/.github/workflows/prettier.yaml @@ -17,7 +17,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci + run: npm ci --no-optional - name: Run Prettier check run: npx prettier --check . diff --git a/.github/workflows/test-converage.yaml b/.github/workflows/test-converage.yaml index a9a9a6a..788d39a 100644 --- a/.github/workflows/test-converage.yaml +++ b/.github/workflows/test-converage.yaml @@ -19,7 +19,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci + run: npm ci --no-optional - name: Setup Deno uses: denolib/setup-deno@v2 diff --git a/.github/workflows/test-package.yaml b/.github/workflows/test-package.yaml index 333fcd4..ed5064f 100644 --- a/.github/workflows/test-package.yaml +++ b/.github/workflows/test-package.yaml @@ -19,7 +19,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci + run: npm ci --no-optional - name: Setup Deno uses: denolib/setup-deno@v2 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1ef86c8..711afcb 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,7 +19,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci + run: npm ci --no-optional - name: Setup Deno uses: denolib/setup-deno@v2 diff --git a/src/localFunctionsTestnet.ts b/src/localFunctionsTestnet.ts index 347df56..13a38d9 100644 --- a/src/localFunctionsTestnet.ts +++ b/src/localFunctionsTestnet.ts @@ -139,7 +139,7 @@ const handleOracleRequest = async ( admin: Wallet, secrets: Record = {}, ) => { - const requestData = await constructRequestDataObject(requestEventData.data) + const requestData = await buildRequestDataObject(requestEventData.data) const response = await simulateDONExecution(requestData, secrets) const errorHexstring = response.errorString @@ -273,7 +273,7 @@ const encodeReport = ( return encodedReport } -const constructRequestDataObject = async (requestData: string): Promise => { +const buildRequestDataObject = async (requestData: string): Promise => { const decodedRequestData = await cbor.decodeAll(Buffer.from(requestData.slice(2), 'hex')) const requestDataObject = {} as FunctionsRequestParams From 25816e7ffb5474c75649e848aa1bd4837ae028d0 Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Fri, 8 Sep 2023 14:30:26 -0500 Subject: [PATCH 04/12] fix workflow --- .github/workflows/cd.yml | 2 +- .github/workflows/prettier.yaml | 2 +- .github/workflows/test-converage.yaml | 2 +- .github/workflows/test-package.yaml | 2 +- .github/workflows/test.yaml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 7d31dcc..9bda065 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -27,7 +27,7 @@ jobs: registry-url: 'https://registry.npmjs.org' - name: Run npm ci - run: npm ci --no-optional + run: npm ci --omit=optional - name: Setup project run: npm run build diff --git a/.github/workflows/prettier.yaml b/.github/workflows/prettier.yaml index 3a01144..7d84d97 100644 --- a/.github/workflows/prettier.yaml +++ b/.github/workflows/prettier.yaml @@ -17,7 +17,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci --no-optional + run: npm ci --omit=optional - name: Run Prettier check run: npx prettier --check . diff --git a/.github/workflows/test-converage.yaml b/.github/workflows/test-converage.yaml index 788d39a..a7f7ebd 100644 --- a/.github/workflows/test-converage.yaml +++ b/.github/workflows/test-converage.yaml @@ -19,7 +19,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci --no-optional + run: npm ci --omit=optional - name: Setup Deno uses: denolib/setup-deno@v2 diff --git a/.github/workflows/test-package.yaml b/.github/workflows/test-package.yaml index ed5064f..51bdd2c 100644 --- a/.github/workflows/test-package.yaml +++ b/.github/workflows/test-package.yaml @@ -19,7 +19,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci --no-optional + run: npm ci --omit=optional - name: Setup Deno uses: denolib/setup-deno@v2 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 711afcb..78233c1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,7 +19,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci --no-optional + run: npm ci --omit=optional - name: Setup Deno uses: denolib/setup-deno@v2 From d2354a5ee5b2d503eb623fb26624f04de1a966f9 Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Fri, 8 Sep 2023 14:53:25 -0500 Subject: [PATCH 05/12] fix workflow --- .github/workflows/cd.yml | 3 ++- .github/workflows/prettier.yaml | 3 ++- .github/workflows/test-converage.yaml | 3 ++- .github/workflows/test-package.yaml | 3 ++- .github/workflows/test.yaml | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 9bda065..f22dc69 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -27,7 +27,8 @@ jobs: registry-url: 'https://registry.npmjs.org' - name: Run npm ci - run: npm ci --omit=optional + run: npm ci + continue-on-error: true # This is required to ignore unsupported platform errors due to the fsevents sub-dependency - name: Setup project run: npm run build diff --git a/.github/workflows/prettier.yaml b/.github/workflows/prettier.yaml index 7d84d97..d74f2da 100644 --- a/.github/workflows/prettier.yaml +++ b/.github/workflows/prettier.yaml @@ -17,7 +17,8 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci --omit=optional + run: npm ci + continue-on-error: true # This is required to ignore unsupported platform errors due to the fsevents sub-dependency - name: Run Prettier check run: npx prettier --check . diff --git a/.github/workflows/test-converage.yaml b/.github/workflows/test-converage.yaml index a7f7ebd..60a85d1 100644 --- a/.github/workflows/test-converage.yaml +++ b/.github/workflows/test-converage.yaml @@ -19,7 +19,8 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci --omit=optional + run: npm ci + continue-on-error: true # This is required to ignore unsupported platform errors due to the fsevents sub-dependency - name: Setup Deno uses: denolib/setup-deno@v2 diff --git a/.github/workflows/test-package.yaml b/.github/workflows/test-package.yaml index 51bdd2c..fe26cbe 100644 --- a/.github/workflows/test-package.yaml +++ b/.github/workflows/test-package.yaml @@ -19,7 +19,8 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci --omit=optional + run: npm ci + continue-on-error: true # This is required to ignore unsupported platform errors due to the fsevents sub-dependency - name: Setup Deno uses: denolib/setup-deno@v2 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 78233c1..cc25e16 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,7 +19,8 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci --omit=optional + run: npm ci + continue-on-error: true # This is required to ignore unsupported platform errors due to the fsevents sub-dependency - name: Setup Deno uses: denolib/setup-deno@v2 From a6b322aa3d0ff930c74c143ffedbe5b09059a049 Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Fri, 8 Sep 2023 14:56:23 -0500 Subject: [PATCH 06/12] fix workflow --- .github/workflows/cd.yml | 3 +-- .github/workflows/prettier.yaml | 3 +-- .github/workflows/test-converage.yaml | 3 +-- .github/workflows/test-package.yaml | 3 +-- .github/workflows/test.yaml | 3 +-- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index f22dc69..7d31dcc 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -27,8 +27,7 @@ jobs: registry-url: 'https://registry.npmjs.org' - name: Run npm ci - run: npm ci - continue-on-error: true # This is required to ignore unsupported platform errors due to the fsevents sub-dependency + run: npm ci --no-optional - name: Setup project run: npm run build diff --git a/.github/workflows/prettier.yaml b/.github/workflows/prettier.yaml index d74f2da..3a01144 100644 --- a/.github/workflows/prettier.yaml +++ b/.github/workflows/prettier.yaml @@ -17,8 +17,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci - continue-on-error: true # This is required to ignore unsupported platform errors due to the fsevents sub-dependency + run: npm ci --no-optional - name: Run Prettier check run: npx prettier --check . diff --git a/.github/workflows/test-converage.yaml b/.github/workflows/test-converage.yaml index 60a85d1..788d39a 100644 --- a/.github/workflows/test-converage.yaml +++ b/.github/workflows/test-converage.yaml @@ -19,8 +19,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci - continue-on-error: true # This is required to ignore unsupported platform errors due to the fsevents sub-dependency + run: npm ci --no-optional - name: Setup Deno uses: denolib/setup-deno@v2 diff --git a/.github/workflows/test-package.yaml b/.github/workflows/test-package.yaml index fe26cbe..ed5064f 100644 --- a/.github/workflows/test-package.yaml +++ b/.github/workflows/test-package.yaml @@ -19,8 +19,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci - continue-on-error: true # This is required to ignore unsupported platform errors due to the fsevents sub-dependency + run: npm ci --no-optional - name: Setup Deno uses: denolib/setup-deno@v2 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index cc25e16..711afcb 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,8 +19,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci - continue-on-error: true # This is required to ignore unsupported platform errors due to the fsevents sub-dependency + run: npm ci --no-optional - name: Setup Deno uses: denolib/setup-deno@v2 From 2a6b03a9160c226cc1c899b28e1a7bbb2d3a6dfc Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Fri, 8 Sep 2023 14:57:11 -0500 Subject: [PATCH 07/12] fix workflow --- .github/workflows/cd.yml | 2 +- .github/workflows/prettier.yaml | 2 +- .github/workflows/test-converage.yaml | 2 +- .github/workflows/test-package.yaml | 2 +- .github/workflows/test.yaml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 7d31dcc..9bda065 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -27,7 +27,7 @@ jobs: registry-url: 'https://registry.npmjs.org' - name: Run npm ci - run: npm ci --no-optional + run: npm ci --omit=optional - name: Setup project run: npm run build diff --git a/.github/workflows/prettier.yaml b/.github/workflows/prettier.yaml index 3a01144..7d84d97 100644 --- a/.github/workflows/prettier.yaml +++ b/.github/workflows/prettier.yaml @@ -17,7 +17,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci --no-optional + run: npm ci --omit=optional - name: Run Prettier check run: npx prettier --check . diff --git a/.github/workflows/test-converage.yaml b/.github/workflows/test-converage.yaml index 788d39a..a7f7ebd 100644 --- a/.github/workflows/test-converage.yaml +++ b/.github/workflows/test-converage.yaml @@ -19,7 +19,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci --no-optional + run: npm ci --omit=optional - name: Setup Deno uses: denolib/setup-deno@v2 diff --git a/.github/workflows/test-package.yaml b/.github/workflows/test-package.yaml index ed5064f..51bdd2c 100644 --- a/.github/workflows/test-package.yaml +++ b/.github/workflows/test-package.yaml @@ -19,7 +19,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci --no-optional + run: npm ci --omit=optional - name: Setup Deno uses: denolib/setup-deno@v2 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 711afcb..78233c1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,7 +19,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci --no-optional + run: npm ci --omit=optional - name: Setup Deno uses: denolib/setup-deno@v2 From 7d90be07675aab838ce139e1992ead48c72867f1 Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Fri, 8 Sep 2023 14:59:45 -0500 Subject: [PATCH 08/12] fix ci --- .github/workflows/cd.yml | 2 +- .github/workflows/prettier.yaml | 2 +- .github/workflows/test-converage.yaml | 2 +- .github/workflows/test-package.yaml | 2 +- .github/workflows/test.yaml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 9bda065..7fb7f6f 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -27,7 +27,7 @@ jobs: registry-url: 'https://registry.npmjs.org' - name: Run npm ci - run: npm ci --omit=optional + run: npm install # npm install instead of npm ci is used to prevent unsupported platform errors due to the fsevents sub-dependency --no-optional - name: Setup project run: npm run build diff --git a/.github/workflows/prettier.yaml b/.github/workflows/prettier.yaml index 7d84d97..a18dfee 100644 --- a/.github/workflows/prettier.yaml +++ b/.github/workflows/prettier.yaml @@ -17,7 +17,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci --omit=optional + run: npm install # npm install instead of npm ci is used to prevent unsupported platform errors due to the fsevents sub-dependency --no-optional - name: Run Prettier check run: npx prettier --check . diff --git a/.github/workflows/test-converage.yaml b/.github/workflows/test-converage.yaml index a7f7ebd..dd46f9d 100644 --- a/.github/workflows/test-converage.yaml +++ b/.github/workflows/test-converage.yaml @@ -19,7 +19,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci --omit=optional + run: npm install # npm install instead of npm ci is used to prevent unsupported platform errors due to the fsevents sub-dependency --no-optional - name: Setup Deno uses: denolib/setup-deno@v2 diff --git a/.github/workflows/test-package.yaml b/.github/workflows/test-package.yaml index 51bdd2c..6a1600b 100644 --- a/.github/workflows/test-package.yaml +++ b/.github/workflows/test-package.yaml @@ -19,7 +19,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci --omit=optional + run: npm install # npm install instead of npm ci is used to prevent unsupported platform errors due to the fsevents sub-dependency --no-optional - name: Setup Deno uses: denolib/setup-deno@v2 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 78233c1..78127ab 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,7 +19,7 @@ jobs: node-version: 18 - name: Install dependencies - run: npm ci --omit=optional + run: npm install # npm install instead of npm ci is used to prevent unsupported platform errors due to the fsevents sub-dependency - name: Setup Deno uses: denolib/setup-deno@v2 From e31d1c6e82d7ff0e7128aea0dc024e572c7a6050 Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Fri, 8 Sep 2023 15:10:07 -0500 Subject: [PATCH 09/12] added changeset --- .changeset/honest-rockets-itch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/honest-rockets-itch.md diff --git a/.changeset/honest-rockets-itch.md b/.changeset/honest-rockets-itch.md new file mode 100644 index 0000000..8eceed8 --- /dev/null +++ b/.changeset/honest-rockets-itch.md @@ -0,0 +1,5 @@ +--- +'@chainlink/functions-toolkit': minor +--- + +Added localFunctionsTestnet From f2ace7f6e55d58796e84021be580936a0dbde9d8 Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Sat, 9 Sep 2023 16:25:40 -0500 Subject: [PATCH 10/12] simulation config file --- README.md | 16 ++++++++++++++-- src/localFunctionsTestnet.ts | 27 +++++++++++++++++++++------ src/simulateScript/Functions.ts | 1 - test/utils/index.ts | 4 +++- test/utils/testSimulationConfig.ts | 1 + 5 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 test/utils/testSimulationConfig.ts diff --git a/README.md b/README.md index 9d607a8..23dc510 100644 --- a/README.md +++ b/README.md @@ -548,13 +548,25 @@ The `localFunctionsTestnet` function takes the following values as arguments. ``` const localFunctionsTestnet = await startLocalFunctionsTestnet( - secrets?: Record, // Secrets which can be accessed by the JavaScript code during request execution + simulationConfigPath?: string // Absolute path to config file which exports simulation config parameters options?: ServerOptions, // Ganache server options port?: number, // Defaults to 8545 ) ``` -Observe that `localFunctionsTestnet` takes in a secrets object as an optional argument. This is because the local testnet does not have the ability to access or decrypt encrypted secrets provided within the request transaction. Instead, you can provide secrets as an argument here which can be accessed by the JavaScript code during request executions. Secrets specified as an argument to `localFunctionsTestnet` will be made accessible within the JavaScript code regardless of the `secretsLocation` or `encryptedSecretsReference` values sent in the request transaction. +Observe that `localFunctionsTestnet` takes in a `simulationConfigPath` string as an optional argument. The primary reason for this is because the local testnet does not have the ability to access or decrypt encrypted secrets provided within request transactions. Instead, you can export an object named `secrets` from a TypeScript or JavaScript file and provide the absolute path to that file as the `simulationConfigPath` argument. When the JavaScript code is executed during the request, secrets specified in that file will be made accessible within the JavaScript code regardless of the `secretsLocation` or `encryptedSecretsReference` values sent in the request transaction. This config file can also contain other simulation config parameters. An example of this config file is shown below. + +``` +export const secrets: { test: 'hello world' } // `secrets` object which can be accessed by the JavaScript code during request execution (can only contain string values) +export const maxOnChainResponseBytes = 256 // Maximum size of the returned value in bytes (defaults to 256) +export const maxExecutionTimeMs = 10000 // Maximum execution duration (defaults to 10_000ms) +export const maxMemoryUsageMb = 128 // Maximum RAM usage (defaults to 128mb) +export const numAllowedQueries = 5 // Maximum number of HTTP requests (defaults to 5) +export const maxQueryDurationMs = 9000// Maximum duration of each HTTP request (defaults to 9_000ms) +export const maxQueryUrlLength = 2048 // Maximum HTTP request URL length (defaults to 2048) +export const maxQueryRequestBytes = 2048 // Maximum size of outgoing HTTP request payload (defaults to 2048 == 2 KB) +export const maxQueryResponseBytes = 2097152 // Maximum size of incoming HTTP response payload (defaults to 2_097_152 == 2 MB) +``` `localFunctionsTestnet` returns a promise which resolves to the following type. diff --git a/src/localFunctionsTestnet.ts b/src/localFunctionsTestnet.ts index 13a38d9..d03935f 100644 --- a/src/localFunctionsTestnet.ts +++ b/src/localFunctionsTestnet.ts @@ -35,7 +35,7 @@ import type { } from './types' export const startLocalFunctionsTestnet = async ( - secrets?: Record, + simulationConfigPath?: string, options?: ServerOptions, port = 8545, ): Promise => { @@ -83,7 +83,12 @@ export const startLocalFunctionsTestnet = async ( callbackGasLimit, commitment, } - handleOracleRequest(requestEvent, contracts.functionsMockCoordinatorContract, admin, secrets) + handleOracleRequest( + requestEvent, + contracts.functionsMockCoordinatorContract, + admin, + simulationConfigPath, + ) }, ) @@ -137,10 +142,10 @@ const handleOracleRequest = async ( requestEventData: RequestEventData, mockCoordinator: Contract, admin: Wallet, - secrets: Record = {}, + simulationConfigPath?: string, ) => { const requestData = await buildRequestDataObject(requestEventData.data) - const response = await simulateDONExecution(requestData, secrets) + const response = await simulateDONExecution(requestData, simulationConfigPath) const errorHexstring = response.errorString ? '0x' + Buffer.from(response.errorString.toString()).toString('hex') @@ -164,15 +169,25 @@ const handleOracleRequest = async ( const simulateDONExecution = async ( requestData: FunctionsRequestParams, - secrets: Record, + simulationConfigPath?: string, ): Promise<{ responseBytesHexstring?: string; errorString?: string }> => { + const simulationConfig = simulationConfigPath ? require(simulationConfigPath) : {} + // Perform the simulation numberOfSimulatedNodeExecution times const simulations = [...Array(numberOfSimulatedNodeExecutions)].map(() => simulateScript({ source: requestData.source, - secrets, + secrets: simulationConfig.secrets, // Secrets are taken from simulationConfig, not request data included in transaction args: requestData.args, bytesArgs: requestData.bytesArgs, + maxOnChainResponseBytes: simulationConfig.maxOnChainResponseBytes, + maxExecutionTimeMs: simulationConfig.maxExecutionTimeMs, + maxMemoryUsageMb: simulationConfig.maxMemoryUsageMb, + numAllowedQueries: simulationConfig.numAllowedQueries, + maxQueryDurationMs: simulationConfig.maxQueryDurationMs, + maxQueryUrlLength: simulationConfig.maxQueryUrlLength, + maxQueryRequestBytes: simulationConfig.maxQueryRequestBytes, + maxQueryResponseBytes: simulationConfig.maxQueryResponseBytes, }), ) const responses = await Promise.all(simulations) diff --git a/src/simulateScript/Functions.ts b/src/simulateScript/Functions.ts index 654adac..2d42e90 100644 --- a/src/simulateScript/Functions.ts +++ b/src/simulateScript/Functions.ts @@ -1,4 +1,3 @@ -// This file is copied from https://github.com/smartcontractkit/universal-adapter-sandbox/blob/main/src/Functions.ts import axios from 'axios' import type { AxiosResponse, AxiosError } from 'axios' import { safePow } from './safePow' diff --git a/test/utils/index.ts b/test/utils/index.ts index 093f4f4..6854938 100644 --- a/test/utils/index.ts +++ b/test/utils/index.ts @@ -1,6 +1,8 @@ import { startLocalFunctionsTestnet } from '../../src' import { FunctionsClientExampleSource } from '../../src/v1_contract_sources' +import path from 'path' + import { Wallet, providers, ContractFactory, utils } from 'ethers' import type { GetFunds } from '../../src' @@ -25,7 +27,7 @@ export const setupLocalTestnet = async ( getFunds: GetFunds }> => { const localFunctionsTestnet = await startLocalFunctionsTestnet( - { test: 'hello world' }, + path.join(__dirname, 'testSimulationConfig.ts'), { logging: { debug: false, diff --git a/test/utils/testSimulationConfig.ts b/test/utils/testSimulationConfig.ts new file mode 100644 index 0000000..0ddd607 --- /dev/null +++ b/test/utils/testSimulationConfig.ts @@ -0,0 +1 @@ +export const secrets = { test: 'hello world' } From 06a39ddb7210d33aeecaae8a1283cd5b1f54aeda Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Mon, 11 Sep 2023 11:23:26 -0500 Subject: [PATCH 11/12] Addressed feedback & fixed test flake --- jest.config.js | 2 +- src/localFunctionsTestnet.ts | 25 +++++++++++-------- src/simulateScript/simulateScript.ts | 6 ++++- test/integration/ResponseListener.test.ts | 6 ++--- test/integration/integration.test.ts | 4 +-- .../integration/localFunctionsTestnet.test.ts | 14 +++++------ test/utils/index.ts | 2 +- 7 files changed, 33 insertions(+), 26 deletions(-) diff --git a/jest.config.js b/jest.config.js index 7f8f203..09c699e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,7 +3,7 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', testMatch: ['**/test/**/*.test.ts'], - testTimeout: 240 * 1000, + testTimeout: 60 * 1000, coverageReporters: ['html'], collectCoverageFrom: ['src/**/*.ts', '!src/test/*.ts', '!src/simulateScript/deno-sandbox/*.ts'], diff --git a/src/localFunctionsTestnet.ts b/src/localFunctionsTestnet.ts index 6fabba5..479c93e 100644 --- a/src/localFunctionsTestnet.ts +++ b/src/localFunctionsTestnet.ts @@ -144,7 +144,7 @@ const handleOracleRequest = async ( admin: Wallet, simulationConfigPath?: string, ) => { - const requestData = await buildRequestDataObject(requestEventData.data) + const requestData = await buildRequestObject(requestEventData.data) const response = await simulateDONExecution(requestData, simulationConfigPath) const errorHexstring = response.errorString @@ -284,34 +284,37 @@ const encodeReport = ( return encodedReport } -const buildRequestDataObject = async (requestData: string): Promise => { - const decodedRequestData = await cbor.decodeAll(Buffer.from(requestData.slice(2), 'hex')) +const buildRequestObject = async ( + requestDataHexString: string, +): Promise => { + const decodedRequestData = await cbor.decodeAll(Buffer.from(requestDataHexString.slice(2), 'hex')) const requestDataObject = {} as FunctionsRequestParams - + // The decoded request data is an array of alternating keys and values, therefore we can iterate over it in steps of 2 for (let i = 0; i < decodedRequestData.length - 1; i += 2) { - const elem = decodedRequestData[i] - switch (elem) { + const requestDataKey = decodedRequestData[i] + const requestDataValue = decodedRequestData[i + 1] + switch (requestDataKey) { case 'codeLocation': - requestDataObject.codeLocation = decodedRequestData[i + 1] + requestDataObject.codeLocation = requestDataValue break case 'secretsLocation': // Unused as secrets provided as an argument to startLocalFunctionsTestnet() are used instead break case 'language': - requestDataObject.codeLanguage = decodedRequestData[i + 1] + requestDataObject.codeLanguage = requestDataValue break case 'source': - requestDataObject.source = decodedRequestData[i + 1] + requestDataObject.source = requestDataValue break case 'secrets': // Unused as secrets provided as an argument to startLocalFunctionsTestnet() are used instead break case 'args': - requestDataObject.args = decodedRequestData[i + 1] + requestDataObject.args = requestDataValue break case 'bytesArgs': - requestDataObject.bytesArgs = decodedRequestData[i + 1].map((bytesArg: Buffer) => { + requestDataObject.bytesArgs = requestDataValue.map((bytesArg: Buffer) => { return '0x' + bytesArg.toString('hex') }) break diff --git a/src/simulateScript/simulateScript.ts b/src/simulateScript/simulateScript.ts index ba1bd67..c56517d 100644 --- a/src/simulateScript/simulateScript.ts +++ b/src/simulateScript/simulateScript.ts @@ -126,7 +126,11 @@ export const simulateScript = async ({ const { code, signal } = await simulationComplete - fs.rmSync(scriptPath) + try { + fs.rmSync(scriptPath) + } catch { + // The temp file may have already been deleted + } let capturedTerminalOutput: string let parsedOutput: SandboxResult = {} diff --git a/test/integration/ResponseListener.test.ts b/test/integration/ResponseListener.test.ts index 6ab1221..3e58772 100644 --- a/test/integration/ResponseListener.test.ts +++ b/test/integration/ResponseListener.test.ts @@ -6,7 +6,7 @@ import { ResponseListener, simulatedDonId, } from '../../src' -import { setupLocalTestnet } from '../utils' +import { setupLocalTestnetFixture } from '../utils' import { Contract, Wallet, utils } from 'ethers' @@ -18,7 +18,7 @@ describe('Functions toolkit classes', () => { let allowlistedUser_A: Wallet beforeAll(async () => { - const testSetup = await setupLocalTestnet(8002) + const testSetup = await setupLocalTestnetFixture(8002) linkTokenAddress = testSetup.linkTokenAddress functionsRouterAddress = testSetup.functionsRouterAddress exampleClient = testSetup.exampleConsumer @@ -72,7 +72,7 @@ describe('Functions toolkit classes', () => { utils.formatBytes32String(simulatedDonId), ) - const succReq = await succReqTx.wait(1) + const succReq = await succReqTx.wait() const succRequestId = succReq.events[0].topics[1] const succResponse = await functionsListener.listenForResponse(succRequestId) diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts index 5706aa6..8b109f8 100644 --- a/test/integration/integration.test.ts +++ b/test/integration/integration.test.ts @@ -7,7 +7,7 @@ import { simulatedSecretsKeys, } from '../../src' import { mockOffchainSecretsEndpoints, mockGatewayUrl } from './apiFixture' -import { setupLocalTestnet } from '../utils' +import { setupLocalTestnetFixture } from '../utils' import { BigNumber, Contract, Wallet, utils } from 'ethers' @@ -25,7 +25,7 @@ describe('Functions toolkit classes', () => { let subFunder_A: Wallet beforeAll(async () => { - const testSetup = await setupLocalTestnet(8001) + const testSetup = await setupLocalTestnetFixture(8001) donId = testSetup.donId linkTokenContract = testSetup.linkTokenContract linkTokenAddress = testSetup.linkTokenAddress diff --git a/test/integration/localFunctionsTestnet.test.ts b/test/integration/localFunctionsTestnet.test.ts index 7fb49e8..d0248a5 100644 --- a/test/integration/localFunctionsTestnet.test.ts +++ b/test/integration/localFunctionsTestnet.test.ts @@ -5,7 +5,7 @@ import { ResponseListener, ReturnType, } from '../../src' -import { setupLocalTestnet } from '../utils' +import { setupLocalTestnetFixture } from '../utils' import { utils } from 'ethers' @@ -22,7 +22,7 @@ describe('Local Functions Testnet', () => { let getFunds: GetFunds beforeAll(async () => { - const testSetup = await setupLocalTestnet(8003) + const testSetup = await setupLocalTestnetFixture(8003) linkTokenAddress = testSetup.linkTokenAddress functionsRouterAddress = testSetup.functionsRouterAddress exampleClient = testSetup.exampleConsumer @@ -77,7 +77,7 @@ describe('Local Functions Testnet', () => { utils.formatBytes32String(simulatedDonId), ) - const req = await reqTx.wait(1) + const req = await reqTx.wait() const requestId = req.events[0].topics[1] const response = await functionsListener.listenForResponse(requestId) @@ -116,7 +116,7 @@ describe('Local Functions Testnet', () => { codeLocation: 0, secretsLocation: 1, language: 0, - source: 'return Functions.encodeUint256(Math.floor(Math.random() * 1_000_000_000))', + source: 'return Functions.encodeUint256(Math.floor((Math.random() + 0.1) * 1_000_000_000))', encryptedSecretsReference: '0xabcd', requestSignature: [], args: ['hello', 'world'], @@ -126,7 +126,7 @@ describe('Local Functions Testnet', () => { utils.formatBytes32String(simulatedDonId), ) - const req = await reqTx.wait(1) + const req = await reqTx.wait() const requestId = req.events[0].topics[1] const response = await functionsListener.listenForResponse(requestId) @@ -164,7 +164,7 @@ describe('Local Functions Testnet', () => { codeLocation: 0, secretsLocation: 1, language: 0, - source: 'throw Error(`${Math.floor(Math.random() * 100)}`)', + source: 'throw Error(`${Math.floor((Math.random() + 0.1) * 100)}`)', encryptedSecretsReference: '0xabcd', requestSignature: [], args: ['hello', 'world'], @@ -174,7 +174,7 @@ describe('Local Functions Testnet', () => { utils.formatBytes32String(simulatedDonId), ) - const req = await reqTx.wait(1) + const req = await reqTx.wait() const requestId = req.events[0].topics[1] const response = await functionsListener.listenForResponse(requestId) diff --git a/test/utils/index.ts b/test/utils/index.ts index 6854938..762f8e2 100644 --- a/test/utils/index.ts +++ b/test/utils/index.ts @@ -10,7 +10,7 @@ import type { GetFunds } from '../../src' import type { Contract } from 'ethers' import type { Server } from 'ganache' -export const setupLocalTestnet = async ( +export const setupLocalTestnetFixture = async ( port: number, ): Promise<{ donId: string From df76e4d577c6aefc1d29aab451480980dbc342fc Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Mon, 11 Sep 2023 11:26:01 -0500 Subject: [PATCH 12/12] bump test duration --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index 09c699e..928554b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,7 +3,7 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', testMatch: ['**/test/**/*.test.ts'], - testTimeout: 60 * 1000, + testTimeout: 2 * 60 * 1000, coverageReporters: ['html'], collectCoverageFrom: ['src/**/*.ts', '!src/test/*.ts', '!src/simulateScript/deno-sandbox/*.ts'],