diff --git a/.gitignore b/.gitignore index 296b1a7..e506373 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,8 @@ node_modules/ coverage/ app/ tests/test_out/computed.json +src/config.json + +chain.json +deployed.json +orders.json diff --git a/Jenkinsfile b/Jenkinsfile index c6d0777..8cb283b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,5 +1,17 @@ @Library('global-jenkins-library@2.0.0') _ +docker.image('node:16-alpine').inside { + stage('Test') { + checkout scm + sh ''' + npm ci + cp config.prod.json src/config.json + npm run ci-test + ''' + archiveArtifacts artifacts: 'coverage/' + } +} + appName = 'generic-oracle-dapp' buildInfo = getBuildInfo() diff --git a/Readme.md b/Readme.md index 1c46ad8..df9ad42 100644 --- a/Readme.md +++ b/Readme.md @@ -58,7 +58,7 @@ TARGET_PRIVATE_KEY= ## Build native image ``` -docker image build -f docker/Dockerfile -t generic-oracle-dapp:local . +docker image build -f docker/Dockerfile -t generic-oracle-dapp:local --build-arg CONFIG_FILE=config.local.json . ``` ## Build TEE debug image @@ -81,10 +81,17 @@ npm run scone ``` iexec app deploy --chain bellecour ``` +``` +npx ts-node scripts/buildAppSecret.ts +``` ``` iexec-core-cli app push-owner-secret --secret=$MY_SECRETS --sms= --wallet-path=/tmp/wallet.json --wallet-password ``` +or +``` +iexec app push-secret --chain bellecour --secret-value $MY_SECRETS +``` ``` iexec order sign --app --chain bellecour @@ -92,11 +99,17 @@ iexec order sign --app --chain bellecour ### As requester: trigger crosschain app +Update iexec.json, make requester wallet file available and run: +``` +runTask.sh <0xrequesterAddress> +``` +or ``` iexec order sign --request --chain bellecour ``` ``` iexec orderbook workerpool --tag tee <0xworkerpool> --chain bellecour +iexec orderbook workerpool --tag tee <0xworkerpool> --chain bellecour --raw | jq -r .workerpoolOrders[0].orderHash ``` ``` iexec order fill --chain bellecour --workerpool diff --git a/config.dev.json b/config.dev.json new file mode 100644 index 0000000..c7a2d8d --- /dev/null +++ b/config.dev.json @@ -0,0 +1,15 @@ +{ + "forwarderApiUrl": "https://forwarder.dev-oracle-factory.iex.ec", + "onChainConfig": { + "5": { + "forwarder": "0x2aD6aD4F35cf7354fE703da74F459690dBcC12bf", + "oracle": "0x8dFf608952ADCDa4cF7320324Db1ef44001BE79b", + "providerUrl": "" + }, + "80001": { + "forwarder": "0xa715674ecf9D14141421190b6f8Acf20686b54d7", + "oracle": "0x330031CF7e6E2C318Dba230fe25A7f39fD3644EA", + "providerUrl": "https://rpc-mumbai.maticvigil.com" + } + } +} diff --git a/config.local.json b/config.local.json new file mode 100644 index 0000000..64c59d1 --- /dev/null +++ b/config.local.json @@ -0,0 +1,15 @@ +{ + "forwarderApiUrl": "http://localhost:5000", + "onChainConfig": { + "5": { + "forwarder": "0x2aD6aD4F35cf7354fE703da74F459690dBcC12bf", + "oracle": "0x8dFf608952ADCDa4cF7320324Db1ef44001BE79b", + "providerUrl": "" + }, + "80001": { + "forwarder": "0xa715674ecf9D14141421190b6f8Acf20686b54d7", + "oracle": "0x330031CF7e6E2C318Dba230fe25A7f39fD3644EA", + "providerUrl": "https://rpc-mumbai.maticvigil.com" + } + } +} diff --git a/config.prod.json b/config.prod.json new file mode 100644 index 0000000..3786b8e --- /dev/null +++ b/config.prod.json @@ -0,0 +1,25 @@ +{ + "forwarderApiUrl": "https://forwarder.oracle-factory.iex.ec", + "onChainConfig": { + "1": { + "forwarder": "0xc684E8645c8414812f22918146d72d1071E722AE", + "oracle": "0x36dA71ccAd7A67053f0a4d9D5f55b725C9A25A3E", + "providerUrl": "" + }, + "5": { + "forwarder": "0xc684E8645c8414812f22918146d72d1071E722AE", + "oracle": "0x36dA71ccAd7A67053f0a4d9D5f55b725C9A25A3E", + "providerUrl": "" + }, + "137": { + "forwarder": "0xc684E8645c8414812f22918146d72d1071E722AE", + "oracle": "0x36dA71ccAd7A67053f0a4d9D5f55b725C9A25A3E", + "providerUrl": "https://rpc-mainnet.maticvigil.com" + }, + "80001": { + "forwarder": "0xc684E8645c8414812f22918146d72d1071E722AE", + "oracle": "0x36dA71ccAd7A67053f0a4d9D5f55b725C9A25A3E", + "providerUrl": "https://rpc-mumbai.maticvigil.com" + } + } +} diff --git a/deployed.json b/deployed.json deleted file mode 100644 index 9d0a762..0000000 --- a/deployed.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "app": { - "134": "0xa7E233a6648d77872005E7C4689B63DFbbDd8857" - } -} diff --git a/docker/.env-build-dev b/docker/.env-build-dev new file mode 100644 index 0000000..a5e4eff --- /dev/null +++ b/docker/.env-build-dev @@ -0,0 +1 @@ +ARG CONFIG_FILE=config.dev.json diff --git a/docker/.env-build-local b/docker/.env-build-local new file mode 100644 index 0000000..e0d08a7 --- /dev/null +++ b/docker/.env-build-local @@ -0,0 +1 @@ +ARG CONFIG_FILE=config.local.json diff --git a/docker/.env-build-prod b/docker/.env-build-prod new file mode 100644 index 0000000..70ec990 --- /dev/null +++ b/docker/.env-build-prod @@ -0,0 +1 @@ +ARG CONFIG_FILE=config.prod.json diff --git a/docker/Dockerfile b/docker/Dockerfile index 5a2b810..dcfd402 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,12 +1,17 @@ FROM node:14-alpine3.10 AS builder -WORKDIR /app COPY package*.json tsconfig.json ./ -COPY src/ . +COPY src/ /src/ +# region-config-file +ARG CONFIG_FILE +RUN echo "CONFIG_FILE : ${CONFIG_FILE}" +RUN test -n "$CONFIG_FILE" +COPY $CONFIG_FILE src/config.json +RUN cat src/config.json +# endregion-config-file RUN npm ci -# Future note: Run unit tests automatically RUN npm run build # Copy generated *.js to /app so we can use them -RUN cp -R app/* ./ +RUN cp -R app/src/* ./app FROM node:14-alpine3.10 WORKDIR /app diff --git a/docker/sconify.sh b/docker/sconify.sh index d7374a8..fbd2373 100644 --- a/docker/sconify.sh +++ b/docker/sconify.sh @@ -9,5 +9,5 @@ ARGS=$(sed -e "s'\${IMG_FROM}'${IMG_FROM}'" -e "s'\${IMG_TO}'${IMG_TO}'" sconify echo $ARGS /bin/bash -c "docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ - registry.scontain.com:5050/sconecuratedimages/iexec-sconify-image:5.3.6 \ + registry.scontain.com:5050/scone-production/iexec-sconify-image:5.3.15 \ sconify_iexec $ARGS" diff --git a/iexec.json b/iexec.json index c5bd067..44d5b32 100644 --- a/iexec.json +++ b/iexec.json @@ -16,27 +16,27 @@ "callback": "0x0000000000000000000000000000000000000000" }, "app": { - "owner": "0x15Bd06807eF0F2284a9C5baeAA2EF4d5a88eB72A", - "name": "generic-oracle-dapp-2", + "owner": "<0xAppDeveloper>", + "name": "generic-oracle-dapp", "type": "DOCKER", - "multiaddr": "docker.io/iexechub/generic-oracle-dapp:feature-99feb9ab-sconify-5.3.15-debug", - "checksum": "0xd0d4c6828115d01ec5f4fd8350bd485f9f4b904050fdce7be77ab6655af11df4", + "multiaddr": "docker.io/iexechub/generic-oracle-dapp:", + "checksum": "0xf7485b254816519e4e49aaaa66e31a99877f3fd19feb2dfe880513b44bfe4fde", "mrenclave": { "provider": "SCONE", "version": "v5", "entrypoint": "node /app/app.js", "heapSize": 1073741824, - "fingerprint": "820fef5ec06be4bf1b49e463a0f4a6531254807d2d34b33c8e454f5ac46c8b2c" + "fingerprint": "0e485080518310768ebab8eff3600bb188fe9b669ce19092dde984ab7c900fad" } }, "order": { "requestorder": { - "app": "0xa7E233a6648d77872005E7C4689B63DFbbDd8857", + "app": "<0xApp>", "appmaxprice": "0", "dataset": "0x0000000000000000000000000000000000000000", "datasetmaxprice": "0", - "workerpool": "0x09bc1b06A695Fa9d2A98AC336331872EA81F307D", - "workerpoolmaxprice": "10000000", + "workerpool": "0xEb14Dc854A8873e419183c81a657d025EC70276b", + "workerpoolmaxprice": "0", "volume": "1", "category": "0", "trust": "0", @@ -44,22 +44,22 @@ "beneficiary": "0x15Bd06807eF0F2284a9C5baeAA2EF4d5a88eB72A", "callback": "0x8ecEDdd1377E52d23A46E2bd3dF0aFE35B526D5F", "params": { - "iexec_args": "", + "iexec_args": "5,80001", "iexec_input_files": [ - "https://raw.githubusercontent.com/iExecBlockchainComputing/generic-oracle-dapp/feature/goerli-crosschain/tests/test_files/input_file_no_dataset.json" + "https://raw.githubusercontent.com/iExecBlockchainComputing/generic-oracle-dapp/develop/tests/test_files/input_file_no_dataset.json" ], "iexec_result_encryption": false, "iexec_developer_logger": true }, - "requester": "0x15Bd06807eF0F2284a9C5baeAA2EF4d5a88eB72A" + "requester": "<0xRequester>" }, "apporder": { - "app": "0xa7E233a6648d77872005E7C4689B63DFbbDd8857", + "app": "<0xApp>", "appprice": "0", - "volume": "10", + "volume": "1000", "tag": "0x0000000000000000000000000000000000000000000000000000000000000001", "datasetrestrict": "0x0000000000000000000000000000000000000000", - "workerpoolrestrict": "0x0000000000000000000000000000000000000000", + "workerpoolrestrict": "0xEb14Dc854A8873e419183c81a657d025EC70276b", "requesterrestrict": "0x0000000000000000000000000000000000000000" } } diff --git a/package-lock.json b/package-lock.json index 6eff936..9f32ef0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.1.0", "license": "Apache-2.0", "dependencies": { - "@iexec/generic-oracle-contracts": "^2.0.0", + "@iexec/generic-oracle-contracts": "^2.2.0", "big.js": "^6.0.3", "ethers": "^5.6.8", "jsonpath": "^1.1.0", @@ -1341,9 +1341,12 @@ "dev": true }, "node_modules/@iexec/generic-oracle-contracts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@iexec/generic-oracle-contracts/-/generic-oracle-contracts-2.0.0.tgz", - "integrity": "sha512-Sigwmcm0VESaVijZwrsXq7ntjPMeDCXnL9QhYmjo/S0atcGSYIeVZaahDOZth4zzPs5gB7Eih7WzVgyV6NJ/tg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@iexec/generic-oracle-contracts/-/generic-oracle-contracts-2.2.0.tgz", + "integrity": "sha512-1uGIabzRmN9X0Y3VIpEFOC4/sfiz2SCJsY5qLiO0nGi6ZkuS5B/JGhDCRuOMTjXojO/vTimeWaxVbNgxU96G+A==", + "dependencies": { + "openzeppelin-contracts-solc-0.8": "npm:@openzeppelin/contracts@^4.7.0" + }, "optionalDependencies": { "fsevents": "^2.3.2" } @@ -6277,6 +6280,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openzeppelin-contracts-solc-0.8": { + "name": "@openzeppelin/contracts", + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.7.3.tgz", + "integrity": "sha512-dGRS0agJzu8ybo44pCIf3xBaPQN/65AIXNgK8+4gzKd5kbvlqyxryUYVLJv7fK98Seyd2hDZzVEHSWAh0Bt1Yw==" + }, "node_modules/optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", @@ -8800,11 +8809,12 @@ "dev": true }, "@iexec/generic-oracle-contracts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@iexec/generic-oracle-contracts/-/generic-oracle-contracts-2.0.0.tgz", - "integrity": "sha512-Sigwmcm0VESaVijZwrsXq7ntjPMeDCXnL9QhYmjo/S0atcGSYIeVZaahDOZth4zzPs5gB7Eih7WzVgyV6NJ/tg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@iexec/generic-oracle-contracts/-/generic-oracle-contracts-2.2.0.tgz", + "integrity": "sha512-1uGIabzRmN9X0Y3VIpEFOC4/sfiz2SCJsY5qLiO0nGi6ZkuS5B/JGhDCRuOMTjXojO/vTimeWaxVbNgxU96G+A==", "requires": { - "fsevents": "^2.3.2" + "fsevents": "^2.3.2", + "openzeppelin-contracts-solc-0.8": "npm:@openzeppelin/contracts@^4.7.0" } }, "@istanbuljs/load-nyc-config": { @@ -12568,6 +12578,11 @@ "mimic-fn": "^2.1.0" } }, + "openzeppelin-contracts-solc-0.8": { + "version": "npm:@openzeppelin/contracts@4.7.3", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.7.3.tgz", + "integrity": "sha512-dGRS0agJzu8ybo44pCIf3xBaPQN/65AIXNgK8+4gzKd5kbvlqyxryUYVLJv7fK98Seyd2hDZzVEHSWAh0Bt1Yw==" + }, "optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", diff --git a/package.json b/package.json index c83d380..0b2b229 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "This application is meant to build a docker container usable in SGX iexec tasks. The dapp take an input file containing a param set in a JSON format. The param set describe the request that should be done to the target API in order to get the wanted data.", "main": "src/app.js", "dependencies": { - "@iexec/generic-oracle-contracts": "^2.0.0", + "@iexec/generic-oracle-contracts": "^2.2.0", "big.js": "^6.0.3", "ethers": "^5.6.8", "jsonpath": "^1.1.0", @@ -38,8 +38,9 @@ "format": "prettier --ignore-path .gitignore --write \"**/*.+(js|ts|json)\"", "test": "jest --coverage", "itest": "jest --coverage tests/dapp.integ.test.ts", + "ci-test": "jest --coverage --testPathIgnorePatterns=\"dapp.integ\"", "build": "tsc", - "scone": "bash sconify.sh" + "scone": "bash docker/sconify.sh" }, "repository": { "type": "git", diff --git a/runTask.sh b/runTask.sh new file mode 100755 index 0000000..59df1bc --- /dev/null +++ b/runTask.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# Usage: ./runTask.sh <0xrequesterAddress> + +REQUESTER=$1 +# Bellecour prod pool +WORKERPOOL=0xEb14Dc854A8873e419183c81a657d025EC70276b +WALLET="--wallet-address $REQUESTER" +CHAIN="--chain bellecour" + +iexec order sign --request $CHAIN +WORKERPOOL_ORDER=$(iexec orderbook workerpool --tag tee $WORKERPOOL $CHAIN --raw | jq -r .workerpoolOrders[0].orderHash) +echo "WORKERPOOL_ORDER: $WORKERPOOL_ORDER" +iexec order fill --workerpool $WORKERPOOL_ORDER $CHAIN $WALLET diff --git a/scripts/buildAppSecret.ts b/scripts/buildAppSecret.ts new file mode 100644 index 0000000..f3865bc --- /dev/null +++ b/scripts/buildAppSecret.ts @@ -0,0 +1,9 @@ +import { buildAppSecret } from "../tests/utils"; + +process.env.INFURA_PROJECT_ID = ""; +process.env.INFURA_PROJECT_SECRET = ""; +const encodedAppSecret = buildAppSecret(process.argv[2]); +console.log(encodedAppSecret); + +// Enable it to verify final app secret +//console.log("App secret recovered [decodedAppSecret:%s]", Buffer.from(encodedAppSecret, "base64").toString()); diff --git a/src/dapp.ts b/src/dapp.ts index 9ce0b2c..f0ef4c0 100644 --- a/src/dapp.ts +++ b/src/dapp.ts @@ -1,8 +1,7 @@ import fsPromises from "fs/promises"; import utils from "./utils"; -import { loadClassicOracle } from "../src/contractLoader"; import { apiCall } from "./caller"; -import { jsonParamSetSchema } from "./validators"; +import { jsonParamSetSchema, targetChainsSchema } from "./validators"; import { getInputFilePath, extractDataset, @@ -10,16 +9,9 @@ import { } from "./requestConsistency"; import { encodeValue } from "./resultEncoder"; import { ethers } from "ethers"; +import { triggerForwardRequests } from "./forward/forwardHandler"; const start = async () => { - let classicOracle; - try { - // validate args or exit before going further - classicOracle = loadClassicOracle(process.env.IEXEC_APP_DEVELOPER_SECRET); - } catch (e) { - console.error("Failed to load ClassicOracle from encoded args [e:%s]", e); - return undefined; - } const inputFolder = process.env.IEXEC_IN; let inputFilePath; try { @@ -82,32 +74,27 @@ const start = async () => { return undefined; } - try { - const tx = await classicOracle.receiveResult(oracleId, encodedValue); + // Native command line arguments - 0:node, 1:app.ts, 2:arg1 + const requestedChainIds = await targetChainsSchema().validate( + process.argv[2] + ); + if (requestedChainIds) { console.log( - "Sent transaction to targeted oracle [tx:%s, oracleId:%s, encodedValue:%s]", - tx.hash, + "User requesting updates on foreign blockchains [chains:%s]", + requestedChainIds + ); + const allForwardRequestsAccepted = await triggerForwardRequests( + requestedChainIds, oracleId, encodedValue ); - return tx - .wait() - .then((receipt) => { - console.log( - "Mined transaction for targeted oracle [tx:%s, blockNumber:%s, oracleId:%s]", - tx.hash, - receipt.blockNumber, - oracleId - ); - return encodedValue; - }) - .catch((e) => { - console.error("Failed transaction on targeted oracle [e:%s]", e); - return undefined; - }); - } catch (e) { - console.error("Failed to send transaction [e:%s]", e); - return undefined; + // Status is logged for information purpose only (app must go on on failure) + if (allForwardRequestsAccepted) { + console.log("All forward requests accepted by Forwarder API"); + } else { + console.error("At least one forward request rejected by Forwarder API"); + } } + return encodedValue; }; export default start; diff --git a/src/forward/forwardEnvironment.ts b/src/forward/forwardEnvironment.ts new file mode 100644 index 0000000..95c697a --- /dev/null +++ b/src/forward/forwardEnvironment.ts @@ -0,0 +1,24 @@ +import config from "../config.json"; + +function getConfig(): Config { + return config; +} + +export function getForwarderApiUrl() { + return getConfig().forwarderApiUrl; +} + +export function getOnChainConfig(chainId: number) { + return getConfig().onChainConfig[chainId]; +} + +export interface Config { + forwarderApiUrl: string; + onChainConfig: Record; +} + +export interface OnChainConfig { + forwarder: string; + oracle: string; + providerUrl: string | undefined; +} diff --git a/src/forward/forwardHandler.ts b/src/forward/forwardHandler.ts new file mode 100644 index 0000000..8df9b4c --- /dev/null +++ b/src/forward/forwardHandler.ts @@ -0,0 +1,70 @@ +import { loadWallet } from "./walletLoader"; +import { Wallet } from "ethers"; +import { getOnChainConfig } from "./forwardEnvironment"; +import { signForwardRequest } from "./forwardSigner"; +import { postForwardRequest } from "./forwardSender"; + +export async function triggerForwardRequests( + requestedChainIds: number[], + oracleId: string, + encodedValue: string +) { + const taskId = process.env.IEXEC_TASK_ID; + if (!taskId) { + console.error( + "`IEXEC_TASK_ID` environnement variable is missing [oracleId:%s, requestedChainIds:%s]", + oracleId, + requestedChainIds + ); + return false; + } + + let wallet: Wallet; + try { + // validate args or exit before going further + wallet = loadWallet(process.env.IEXEC_APP_DEVELOPER_SECRET); + } catch (error) { + console.error( + "Failed to load wallet from `IEXEC_APP_DEVELOPER_SECRET` [oracleId:%s, taskId:%s, error:%s]", + oracleId, + taskId, + error + ); + return false; + } + + const successes = await Promise.all( + requestedChainIds.map(async (chainId) => { + const onChainConfig = getOnChainConfig(chainId); + if (!onChainConfig) { + console.error( + "Foreign blockchain requested is not supported [chainId:%s, oracleId:%s, taskId:%s]", + chainId, + oracleId, + taskId + ); + return false; + } + + const signedForwardRequest = await signForwardRequest( + chainId, + wallet, + taskId, + oracleId, + encodedValue, + onChainConfig + // catch promise rejection and convert it to false to prevent Promise.all rejection + ).catch((e) => false); + + return await postForwardRequest( + signedForwardRequest, + oracleId, + taskId + ).catch((e) => false); + }) + ); + + return ( + successes.filter((success) => success).length == requestedChainIds.length + ); +} diff --git a/src/forward/forwardSender.ts b/src/forward/forwardSender.ts new file mode 100644 index 0000000..4e8e8a0 --- /dev/null +++ b/src/forward/forwardSender.ts @@ -0,0 +1,34 @@ +import fetch from "node-fetch"; +import { getForwarderApiUrl } from "./forwardEnvironment"; + +export async function postForwardRequest( + signedForwardRequest: any, + oracleId: string, + taskId: string +): Promise { + const response = await fetch(getForwarderApiUrl() + "/forward", { + method: "post", + body: JSON.stringify(signedForwardRequest), + headers: { "Content-Type": "application/json" }, + }); + + if (response.ok) { + console.log( + "Forward request accepted by Forwarder API [oracleId:%s, taskId:%s]", + oracleId, + taskId + ); + return true; + } + console.error( + "Forward request rejected by Forwarder API [oracleId:%s, taskId:%s, httpStatus:%s, messageBody:%s]", + oracleId, + taskId, + response.status, + await response + .json() + .then((body) => body.message) + .then(console.log) + ); + return false; +} diff --git a/src/forward/forwardSigner.ts b/src/forward/forwardSigner.ts new file mode 100644 index 0000000..8dbfc8c --- /dev/null +++ b/src/forward/forwardSigner.ts @@ -0,0 +1,71 @@ +import { ethers } from "ethers"; +import { OnChainConfig } from "./forwardEnvironment"; +import { ReceiveResultContractFunction } from "./oracleContractWrapper"; + +export async function signForwardRequest( + chainId: number, + wallet: ethers.Wallet, + taskId: string, + oracleId: string, + encodedValue: string, + onChainConfig: OnChainConfig +) { + const reporterAddress = await wallet.getAddress(); + const domain = { + name: "SaltyForwarder", + version: "0.0.1", + chainId: chainId.toString(), + verifyingContract: onChainConfig.forwarder, + }; + const types = { + ForwardRequest: [ + { name: "from", type: "address" }, + { name: "to", type: "address" }, + { name: "value", type: "uint256" }, + { name: "gas", type: "uint256" }, + { name: "salt", type: "bytes32" }, + { name: "data", type: "bytes" }, + ], + }; + + const oracleAddress = onChainConfig.oracle; + const providerUrl = onChainConfig.providerUrl; + + const provider = ethers.getDefaultProvider(providerUrl || chainId); + + const receiveResult = new ReceiveResultContractFunction( + oracleAddress, + provider + ); + + const forwardRequest = { + from: reporterAddress, + to: oracleAddress, + value: "0", + gas: ( + await receiveResult.getGasEstimate(taskId, encodedValue, reporterAddress) + ).toString(), + salt: ethers.utils.hexlify(ethers.utils.randomBytes(32)), + data: receiveResult.getData(taskId, encodedValue), + }; + + const signature = await wallet._signTypedData(domain, types, forwardRequest); + + const signedForwardRequest = { + eip712: { + types: types, + domain: domain, + message: forwardRequest, + }, + sign: signature, + }; + console.log( + "Signed forward request [chainId:%s, oracleId:%s, taskId:%s, encodedValue:%s, signedForwardRequest:%s]", + chainId, + oracleId, + taskId, + encodedValue, + JSON.stringify(signedForwardRequest) + ); + return signedForwardRequest; +} diff --git a/src/forward/oracleContractWrapper.ts b/src/forward/oracleContractWrapper.ts new file mode 100644 index 0000000..8847af4 --- /dev/null +++ b/src/forward/oracleContractWrapper.ts @@ -0,0 +1,30 @@ +import { ethers } from "ethers"; +import { IOracleConsumer__factory } from "@iexec/generic-oracle-contracts/typechain"; + +export class ReceiveResultContractFunction { + oracleContract; //public for tests + + constructor(oracleAddress: string, provider: ethers.providers.BaseProvider) { + this.oracleContract = IOracleConsumer__factory.connect( + oracleAddress, + provider + ); + } + + getGasEstimate( + taskId: string, + encodedValue: string, + reporterAddress: string + ) { + return this.oracleContract.estimateGas.receiveResult(taskId, encodedValue, { + from: reporterAddress, + }); + } + + getData(taskId: string, encodedValue: string) { + return this.oracleContract.interface.encodeFunctionData("receiveResult", [ + taskId, + encodedValue, + ]); + } +} diff --git a/src/contractLoader.ts b/src/forward/walletLoader.ts similarity index 59% rename from src/contractLoader.ts rename to src/forward/walletLoader.ts index 5327181..986a632 100644 --- a/src/contractLoader.ts +++ b/src/forward/walletLoader.ts @@ -1,18 +1,6 @@ -import { ethers } from "ethers"; -import { - ClassicOracle, - ClassicOracle__factory, -} from "@iexec/generic-oracle-contracts/typechain"; - -const chain = "goerli"; -const oracleReceiver = "0x28291E6A81aC30cE6099144E68D8aEeE2b64052b"; - -export function loadClassicOracle( - encodedArgs: string | undefined -): ClassicOracle { - console.log("Target chain: " + chain); - console.log("Target oracle address: " + oracleReceiver); +import { ethers, Wallet } from "ethers"; +export function loadWallet(encodedArgs: string | undefined): Wallet { if (encodedArgs == undefined) { throw Error("Encoded args are required"); } @@ -41,15 +29,16 @@ export function loadClassicOracle( throw Error("Failed to parse `targetPrivateKey` from decoded secret JSON"); } - const provider = new ethers.providers.InfuraProvider(chain, { - projectId: infuraProjectId, - projectSecret: infuraProjectSecret, - }); + //TODO: Remove infura keys at some point + //const provider = getProvider(chainId, infuraProjectId, infuraProjectSecret); - const wallet = new ethers.Wallet(targetPrivateKey, provider); - console.log("Target reporter wallet address: " + wallet.address); + const wallet = new ethers.Wallet(targetPrivateKey); + console.log( + "Recovered authorized reporter wallet for foreign oracle contract [address:%s]", + wallet.address + ); - return new ClassicOracle__factory().attach(oracleReceiver).connect(wallet); + return wallet; } interface OracleArgs { @@ -57,3 +46,14 @@ interface OracleArgs { infuraProjectSecret?: string; targetPrivateKey?: string; } + +// function getProvider( +// chainId: number, +// infuraProjectId: string, +// infuraProjectSecret: string +// ) { +// return new ethers.providers.InfuraProvider(chainId, { +// projectId: infuraProjectId, +// projectSecret: infuraProjectSecret, +// }); +// } diff --git a/src/validators.js b/src/validators.js index 7846a64..2c18da5 100644 --- a/src/validators.js +++ b/src/validators.js @@ -1,4 +1,4 @@ -const { string, object, array } = require("yup"); +const { string, number, object, array } = require("yup"); const { getAddress } = require("ethers").utils; const jp = require("jsonpath"); const { API_KEY_PLACEHOLDER } = require("./conf"); @@ -306,9 +306,19 @@ const jsonParamSetSchema = () => } ); +const chainIdSchema = () => number().integer(); + +// Parse chainIds, sort them, remove duplicates, cast them to number +const targetChainsSchema = () => + array() + .transform((value, originalValue) => + Array.from(new Set(originalValue.split(','))).sort((a, b) => a - b)) + .of(chainIdSchema().required()); + module.exports = { rawParamsSchema, strictCallParamsSchema, strictParamSetSchema, jsonParamSetSchema, + targetChainsSchema }; diff --git a/tests/dapp.integ.test.ts b/tests/dapp.integ.test.ts index 1298713..9350104 100644 --- a/tests/dapp.integ.test.ts +++ b/tests/dapp.integ.test.ts @@ -1,39 +1,44 @@ import { config as dotEnvConfig } from "dotenv"; import Dapp from "../src/dapp"; +import { buildAppSecret } from "./utils"; const somePrivateKey = "0x0000000000000000000000000000000000000000000000000000000000000001"; describe("dapp", () => { - test("should fail since no app developer secret", async () => { - const callbackData = await Dapp(); - expect(callbackData).toBeUndefined(); - }); - - test("should fail since no inputfile", async () => { - process.env.IEXEC_APP_DEVELOPER_SECRET = - buildAppSecretWithValidInfuraProcessEnv(somePrivateKey); - - const callbackData = await Dapp(); - expect(callbackData).toBeUndefined(); - }); - - test("should fail since tx failed", async () => { - process.env.IEXEC_APP_DEVELOPER_SECRET = - buildAppSecretWithValidInfuraProcessEnv(somePrivateKey); - process.env.IEXEC_INPUT_FILES_NUMBER = "1"; - process.env.IEXEC_INPUT_FILES_FOLDER = "./tests/test_files"; - process.env.IEXEC_INPUT_FILE_NAME_1 = "input_file_no_dataset.json"; - process.env.IEXEC_OUT = "./tests/test_out"; - process.env.IEXEC_IN = "./tests/test_files"; - - const callbackData = await Dapp(); - expect(callbackData).toBeUndefined(); - }); + // test("should fail since no app developer secret", async () => { + // const callbackData = await Dapp(); + // expect(callbackData).toBeUndefined(); + // }); + + // test("should fail since no inputfile", async () => { + // process.env.IEXEC_APP_DEVELOPER_SECRET = + // buildAppSecretWithValidInfuraProcessEnv(somePrivateKey); + + // const callbackData = await Dapp(); + // expect(callbackData).toBeUndefined(); + // }); + + // test("should fail since tx failed", async () => { + // process.env.IEXEC_APP_DEVELOPER_SECRET = + // buildAppSecretWithValidInfuraProcessEnv(somePrivateKey); + // process.env.IEXEC_INPUT_FILES_NUMBER = "1"; + // process.env.IEXEC_INPUT_FILES_FOLDER = "./tests/test_files"; + // process.env.IEXEC_INPUT_FILE_NAME_1 = "input_file_no_dataset.json"; + // process.env.IEXEC_OUT = "./tests/test_out"; + // process.env.IEXEC_IN = "./tests/test_files"; + + // const callbackData = await Dapp(); + // expect(callbackData).toBeUndefined(); + // }); test("a full successful dapp IT run without dataset", async () => { dotEnvConfig(); - process.env.IEXEC_APP_DEVELOPER_SECRET = - buildAppSecretWithValidInfuraProcessEnv(process.env.TARGET_PRIVATE_KEY); + process.argv = ["", "", "5,80001"]; // Goerli & Mumbai Polygon + process.env.IEXEC_TASK_ID = + "0x0000000000000000000000000000000000000000000000000000000000000abc"; + process.env.IEXEC_APP_DEVELOPER_SECRET = buildAppSecret( + process.env.TARGET_PRIVATE_KEY + ); process.env.IEXEC_INPUT_FILES_NUMBER = "1"; process.env.IEXEC_INPUT_FILES_FOLDER = "./tests/test_files"; process.env.IEXEC_INPUT_FILE_NAME_1 = "input_file_no_dataset.json"; @@ -51,19 +56,3 @@ describe("dapp", () => { */ }, 60000); //sending tx takes some time }); - -function buildAppSecretWithValidInfuraProcessEnv( - targetPrivateKey: string | undefined -) { - dotEnvConfig(); - const infuraProjectId = process.env.INFURA_PROJECT_ID; - const infuraProjectSecret = process.env.INFURA_PROJECT_SECRET; - const appDeveloperSecretJsonString = JSON.stringify({ - infuraProjectId: infuraProjectId, - infuraProjectSecret: infuraProjectSecret, - targetPrivateKey: targetPrivateKey, - }); - const buff = Buffer.from(appDeveloperSecretJsonString, "utf-8"); - const encodedAppDeveloperSecret = buff.toString("base64"); - return encodedAppDeveloperSecret; -} diff --git a/tests/dapp.test.ts b/tests/dapp.test.ts index 65b839c..faaf449 100644 --- a/tests/dapp.test.ts +++ b/tests/dapp.test.ts @@ -1,14 +1,17 @@ import fetch from "node-fetch"; import Dapp from "../src/dapp"; +import { buildAppSecret } from "./utils"; +import { triggerForwardRequests } from "../src/forward/forwardHandler"; jest.mock("node-fetch"); +jest.mock("../src/forward/forwardHandler"); afterEach(() => { jest.resetAllMocks(); }); describe("dapp", () => { - test.skip("a full successful dapp run", async () => { + test("a full successful dapp run", async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (fetch as any).mockImplementation(async () => ({ json: () => @@ -22,6 +25,15 @@ describe("dapp", () => { get: () => "Thu, 10 Jun 2021 09:58:20 GMT", }, })); + + jest.mocked(triggerForwardRequests).mockReturnValue(Promise.resolve(true)); + + process.argv = ["", "", "80001,5"]; // Goerli & Mumbai Polygon + process.env.IEXEC_TASK_ID = + "0x0000000000000000000000000000000000000000000000000000000000000abc"; + process.env.IEXEC_APP_DEVELOPER_SECRET = buildAppSecret( + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); process.env.IEXEC_INPUT_FILES_FOLDER = "./tests/test_files"; process.env.IEXEC_INPUT_FILE_NAME_1 = "input_file.json"; process.env.IEXEC_OUT = "./tests/test_out"; diff --git a/tests/forward/forwardEnvironment.test.ts b/tests/forward/forwardEnvironment.test.ts new file mode 100644 index 0000000..1ee98ee --- /dev/null +++ b/tests/forward/forwardEnvironment.test.ts @@ -0,0 +1,12 @@ +import { + getForwarderApiUrl, + getOnChainConfig, +} from "../../src/forward/forwardEnvironment"; + +describe("Environment", () => { + test("should get environment", async () => { + expect(getForwarderApiUrl()).toContain("http"); + expect(getOnChainConfig(5)).toBeDefined(); + expect(getOnChainConfig(80001)).toBeDefined(); + }); +}); diff --git a/tests/forward/forwardHandler.test.ts b/tests/forward/forwardHandler.test.ts new file mode 100644 index 0000000..3d5c4c2 --- /dev/null +++ b/tests/forward/forwardHandler.test.ts @@ -0,0 +1,95 @@ +import { Wallet } from "ethers"; +import { triggerForwardRequests } from "../../src/forward/forwardHandler"; +import { loadWallet } from "../../src/forward/walletLoader"; +import { signForwardRequest } from "../../src/forward/forwardSigner"; +import { postForwardRequest } from "../../src/forward/forwardSender"; + +jest.mock("../../src/forward/walletLoader"); +jest.mock("../../src/forward/forwardSigner"); +jest.mock("../../src/forward/forwardSender"); + +const loadWalletMock = loadWallet as jest.MockedFunction; +const getSignedForwardRequestMock = + signForwardRequest as jest.MockedFunction< + typeof signForwardRequest + >; +const postMultiForwardRequestMock = + postForwardRequest as jest.MockedFunction< + typeof postForwardRequest + >; + +const TASK_ID = + "0x0000000000000000000000000000000000000000000000000000000000000abc"; +const CHAIN_ID = 5; +const ORACLE_ID = "0x1"; +const VALUE = "0x2"; +const wallet = new Wallet( + "0x0000000000000000000000000000000000000000000000000000000000000001" +); + +describe("Forward handler", () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test("should not trigger since task ID missing", async () => { + process.env.IEXEC_TASK_ID = ""; + + const success = await triggerForwardRequests([CHAIN_ID], ORACLE_ID, VALUE); + expect(success).toBe(false); + }); + + test("should not trigger since chain not supported", async () => { + process.env.IEXEC_TASK_ID = TASK_ID; + + const success = await triggerForwardRequests( + [12345], // chain not supported + ORACLE_ID, + VALUE + ); + expect(success).toBe(false); + }); + + test("should not trigger since wallet not found", async () => { + process.env.IEXEC_TASK_ID = TASK_ID; + + loadWalletMock.mockImplementation(() => { + throw new Error("Wallet error"); + }); + + const success = await triggerForwardRequests([CHAIN_ID], ORACLE_ID, VALUE); + expect(success).toBe(false); + }); + + test("should trigger", async () => { + process.env.IEXEC_TASK_ID = TASK_ID; + + loadWalletMock.mockReturnValue(wallet); + getSignedForwardRequestMock.mockReturnValue( + Promise.resolve({ + eip712: { + types: { ForwardRequest: [] }, + domain: { + name: "string", + version: "string", + chainId: "string", + verifyingContract: "string", + }, + message: { + from: "string", + to: "string", + value: "string", + gas: "string", + salt: "string", + data: "string", + }, + }, + sign: "string", + }) + ); + postMultiForwardRequestMock.mockReturnValue(Promise.resolve(true)); + + const success = await triggerForwardRequests([CHAIN_ID], ORACLE_ID, VALUE); + expect(success).toBe(true); + }); +}); diff --git a/tests/forward/forwardSender.test.ts b/tests/forward/forwardSender.test.ts new file mode 100644 index 0000000..6a09159 --- /dev/null +++ b/tests/forward/forwardSender.test.ts @@ -0,0 +1,48 @@ +import fetch from "node-fetch"; +import { postForwardRequest } from "../../src/forward/forwardSender"; +import { getForwarderApiUrl } from "../../src/forward/forwardEnvironment"; + +jest.mock("node-fetch"); +jest.mock("../../src/forward/forwardEnvironment"); + +const TASK_ID = + "0x0000000000000000000000000000000000000000000000000000000000000abc"; +const ORACLE_ID = "0x1"; + +describe("Forward signer", () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test("should successfully forward", async () => { + (fetch as any).mockImplementation(async () => ({ + ok: true, + })); + jest + .mocked(getForwarderApiUrl) + .mockImplementation(() => "http://forwarder"); + + await postForwardRequest({ some: "request" }, ORACLE_ID, TASK_ID); + + expect(fetch).toHaveBeenCalledWith("http://forwarder/forward", { + method: "post", + body: '{"some":"request"}', + headers: { "Content-Type": "application/json" }, + }); + }); + + test("should not forward", async () => { + (fetch as any).mockImplementation(async () => ({ + ok: false, + status: 500, + json: () => + Promise.resolve({ + message: "error message", + }), + })); + + expect( + await postForwardRequest({ some: "request" }, ORACLE_ID, TASK_ID) + ).toBe(false); + }); +}); diff --git a/tests/forward/forwardSigner.test.ts b/tests/forward/forwardSigner.test.ts new file mode 100644 index 0000000..7b232bf --- /dev/null +++ b/tests/forward/forwardSigner.test.ts @@ -0,0 +1,79 @@ +import { IOracleConsumer } from "@iexec/generic-oracle-contracts/typechain"; +import { Wallet, BigNumber, utils } from "ethers"; +import { signForwardRequest } from "../../src/forward/forwardSigner"; +import { ReceiveResultContractFunction } from "../../src/forward/oracleContractWrapper"; + +jest.mock("../../src/forward/oracleContractWrapper"); + +const TASK_ID = + "0x0000000000000000000000000000000000000000000000000000000000000abc"; +const CHAIN_ID = 5; +const ORACLE_ID = "0x1"; +const VALUE = "0x2"; +const wallet = new Wallet( + "0x0000000000000000000000000000000000000000000000000000000000000001" +); + +describe("Forward signer", () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test("should", async () => { + const receiveResultMock = { + oracleContract: {} as IOracleConsumer, + getGasEstimate: () => Promise.resolve(BigNumber.from("100000")), + getData: () => "0xabcd", + } as ReceiveResultContractFunction; + + jest + .mocked(ReceiveResultContractFunction) + .mockImplementation(() => receiveResultMock); + + jest.spyOn(utils, "randomBytes").mockReturnValue( + utils.toUtf8Bytes("01234567890123456789012345678901") //size 32 + ); + + const signedRequest = await signForwardRequest( + CHAIN_ID, + wallet, + TASK_ID, + ORACLE_ID, + VALUE, + { + forwarder: "0x0000000000000000000000000000000000000001", + oracle: "0x0000000000000000000000000000000000000002", + providerUrl: undefined, + } + ); + expect(signedRequest).toEqual({ + eip712: { + types: { + ForwardRequest: [ + { name: "from", type: "address" }, + { name: "to", type: "address" }, + { name: "value", type: "uint256" }, + { name: "gas", type: "uint256" }, + { name: "salt", type: "bytes32" }, + { name: "data", type: "bytes" }, + ], + }, + domain: { + name: "SaltyForwarder", + version: "0.0.1", + chainId: "5", + verifyingContract: "0x0000000000000000000000000000000000000001", + }, + message: { + from: "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf", + to: "0x0000000000000000000000000000000000000002", + value: "0", + gas: "100000", + salt: "0x3031323334353637383930313233343536373839303132333435363738393031", + data: "0xabcd", + }, + }, + sign: "0xfa561b74cd3fc10cdee22beed8ede242d8da31c9e203e2ee1c34fe015cadc50603d93681ad71cd682ac4f8049633a78ceaafd92c69595d9c60402f54ad442fe91b", + }); + }); +}); diff --git a/tests/contractLoader.test.ts b/tests/forward/walletLoader.test.ts similarity index 70% rename from tests/contractLoader.test.ts rename to tests/forward/walletLoader.test.ts index 5dbcff5..054b255 100644 --- a/tests/contractLoader.test.ts +++ b/tests/forward/walletLoader.test.ts @@ -1,27 +1,27 @@ -import { loadClassicOracle } from "../src/contractLoader"; +import { loadWallet } from "../../src/forward/walletLoader"; describe("contract loader", () => { test("should fail since no args", () => { expect(() => { - loadClassicOracle(undefined); + loadWallet(undefined); }).toThrowError("Encoded args are required"); }); test("should fail since empty args", () => { expect(() => { - loadClassicOracle(""); + loadWallet(""); }).toThrowError("Failed to parse appDeveloperSecret JSON"); }); test("should fail since parse payload failed", () => { expect(() => { - loadClassicOracle(JSON.stringify({ some: "data" })); + loadWallet(JSON.stringify({ some: "data" })); }).toThrowError("Failed to parse appDeveloperSecret JSON"); }); test("should fail since no infuraProjectId", () => { expect(() => { - loadClassicOracle(encode({})); + loadWallet(encode({})); }).toThrowError( "Failed to parse `infuraProjectId` from decoded secret JSON" ); @@ -29,7 +29,7 @@ describe("contract loader", () => { test("should fail since no infuraProjectSecret", () => { expect(() => { - loadClassicOracle( + loadWallet( encode({ infuraProjectId: "id", }) @@ -41,7 +41,7 @@ describe("contract loader", () => { test("should fail since no targetPrivateKey", () => { expect(() => { - loadClassicOracle( + loadWallet( encode({ infuraProjectId: "id", infuraProjectSecret: "secret", @@ -52,17 +52,18 @@ describe("contract loader", () => { ); }); - test("should return something", () => { - expect( - loadClassicOracle( - encode({ - infuraProjectId: "some", - infuraProjectSecret: "secret", - targetPrivateKey: - "0x0000000000000000000000000000000000000000000000000000000000000001", - }) - ) - ).not.toBeNull(); + test("should return wallet", async () => { + const wallet = loadWallet( + encode({ + infuraProjectId: "some", + infuraProjectSecret: "secret", + targetPrivateKey: + "0x0000000000000000000000000000000000000000000000000000000000000001", + }) + ); + expect(await wallet.getAddress()).toEqual( + "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf" + ); }); }); diff --git a/tests/utils.ts b/tests/utils.ts new file mode 100644 index 0000000..c199523 --- /dev/null +++ b/tests/utils.ts @@ -0,0 +1,15 @@ +import { config as dotEnvConfig } from "dotenv"; + +export function buildAppSecret(targetPrivateKey: string | undefined) { + dotEnvConfig(); + const infuraProjectId = process.env.INFURA_PROJECT_ID; + const infuraProjectSecret = process.env.INFURA_PROJECT_SECRET; + const appDeveloperSecretJsonString = JSON.stringify({ + infuraProjectId: infuraProjectId, + infuraProjectSecret: infuraProjectSecret, + targetPrivateKey: targetPrivateKey, + }); + const buff = Buffer.from(appDeveloperSecretJsonString, "utf-8"); + const encodedAppDeveloperSecret = buff.toString("base64"); + return encodedAppDeveloperSecret; +} diff --git a/tsconfig.json b/tsconfig.json index ba9fd12..4661ac0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,5 +21,5 @@ "src", "tests", "node_modules/@iexec/generic-oracle-contracts/typechain" - ] +, "scripts/buildAppSecret.ts" ] }