From 4c6d8df4b12991249a1bc11c7d2b9af999cd5e94 Mon Sep 17 00:00:00 2001 From: Wenfeng Wang Date: Fri, 12 Jan 2024 20:06:58 +0800 Subject: [PATCH] Closed beta (#96) * remove rollup storage * remove cache * remove recover check * remote cloud storage r/w * add comments * check tx with off-chin indexer * fix clippy * simply graph setup * delete unused code * remove graph * update * 1) simplify executor setup - remove rollup setup - remove graph setup - introduce remote storage setup 2) update console.js 3) update play document * update * fix storage writing * reduce storage reading * fix * comment gas estimation in uniswap executor * update console.js * bring back gas estimation * task retry support * ensure executor running when retry * fix task destroy * claim during task init * merge various steps into one * update * introduce Call * impl UniswapV2 action * more actions * create actions * remove index * impl Tokenizable for Call * upgrade dependencies * step execution * call update * step update * fix balance settlement * step merge * assign call_index when merge * merge claim and first step * update * fix action lookup * split task logic to multiple file * fix spend amount when claim * fix recipient apply and test * fix step merge and test * add astar sub > evm transactor * upgrade to XCM v3 * update * fix deployment * fix khala endpoint * fix call build * update * fix batch call failure * fix bridge balance check * fix xtransfer weight * asset registry * update * get running task * fix error InvalidKey * add support of UniswapV3 based swap router * fix build * add command drop-task * upgrade dependencies * integrate scheduler into console cli * remove breakline char * trival * WIP: upgrade phala sdk * upgrade phala sdk * dependencies: let @phala/sdk decide the polkadot-js's libraries versions * dependencies: update @phala/sdk to latest version in beta channel for improved InkResponse error handling * handler network exception * ingore InkResponse error * keep task data in db when finished * check remote db before init task * save transaction hash of each step * update status check * update moonbeam native asset * update * add action to support native asset wrap & unwrap * adjust call builder according to handler update * replace step json with scale (#91) * replace step json with scale * remove exe_type * update handler contracts * double retry limit * update * phala sygma * feat: evm sygma bridge * 1) support call along with native asset in engine 2) fix & test sygma bridge from goerli to rhala * fix update offset * ignore transfer test * remove approval in sygma * fix clippy * add astar native wrapper action * update handler contracts * pack call * adapt new task creation * bring back recipient * bring back ABI codec for batch call * optimize storage access * fix fee configuration of sygma bridge action * action: khala to ethereum bridge * specify fixed version to =1.23.1 * fix sub claim * action: moonbeam_bridge_to_assethub * support moonbeam bridge to polkadot * import worker key * update ethereum rpc endpoint * update polkadot rpc endpoint * remove merge steps * remove steps * astar bridge to phala * polkadot bridges * phala to polkadot bridge * polkadot to astar * sub native transactor * support AccountKey20 in astar xtokens * feat: Fee calculation & solution simulation (#93) * action extra info * task simulation, including fee calculation * test solution simulation * expose more details in simulation result * update * more complicate solution simulation * rename * fix clippy * add new extra info * fix merge * collect fee when claim evm task * update * support claim with fee from pallet-index * update * set simulation spned amount according to decimals * update * serveal fixes * fix task finality check * fix evm tx option settting * add test * fix clippy and CI --------- Co-authored-by: Leechael Yim Co-authored-by: Kingsley <10992364+kingsleydon@users.noreply.github.com> --- .github/actions/install_toolchain/action.yml | 2 +- .github/workflows/cargo-contract-build.yml | 2 +- chain | 1 - contracts/index_executor/Cargo.toml | 2 +- .../src/abi/SygmaBasicFeeHandler.json | 507 +++++++ .../index_executor/src/abi/SygmaBridge.json | 723 ++++++++++ contracts/index_executor/src/abi/WETH9.json | 286 ++++ contracts/index_executor/src/abi/handler.json | 162 ++- .../src/abi/uniswapV3Router.json | 634 +++++++++ .../index_executor/src/actions/acala/dex.rs | 8 +- .../index_executor/src/actions/acala/mod.rs | 24 + .../src/actions/acala/transfer.rs | 13 +- .../index_executor/src/actions/astar/asset.rs | 26 +- .../index_executor/src/actions/astar/mod.rs | 89 +- .../index_executor/src/actions/astar/sub.rs | 50 +- .../src/actions/astar/xtokens.rs | 99 ++ .../index_executor/src/actions/base/mod.rs | 3 + .../src/actions/base/native_wrapper.rs | 224 +++ .../src/actions/base/sub_transactor.rs | 82 ++ .../src/actions/base/uniswapv2.rs | 70 +- .../src/actions/base/uniswapv3.rs | 167 +++ .../src/actions/ethereum/mod.rs | 127 +- .../src/actions/ethereum/sygma.rs | 434 ++++++ contracts/index_executor/src/actions/mod.rs | 20 + .../src/actions/moonbeam/mod.rs | 93 +- .../src/actions/moonbeam/xtoken.rs | 258 ++-- .../index_executor/src/actions/phala/mod.rs | 111 +- .../index_executor/src/actions/phala/sygma.rs | 124 ++ .../src/actions/phala/xtransfer.rs | 74 +- .../src/actions/polkadot/mod.rs | 48 + .../src/actions/polkadot/xcm_v2.rs | 113 ++ .../src/actions/polkadot/xcm_v3.rs | 144 ++ contracts/index_executor/src/call.rs | 35 +- contracts/index_executor/src/chain.rs | 18 +- contracts/index_executor/src/constants.rs | 8 +- contracts/index_executor/src/context.rs | 5 + contracts/index_executor/src/gov.rs | 4 +- contracts/index_executor/src/lib.rs | 437 ++++-- contracts/index_executor/src/price.rs | 35 + contracts/index_executor/src/registry.rs | 120 +- contracts/index_executor/src/step.rs | 332 ++++- contracts/index_executor/src/storage.rs | 4 +- contracts/index_executor/src/task.rs | 1229 +++++------------ contracts/index_executor/src/task_deposit.rs | 107 +- contracts/index_executor/src/task_fetcher.rs | 118 +- contracts/index_executor/src/utils.rs | 5 + doc/play-with-index.md | 25 +- rust-toolchain | 2 +- scripts/package.json | 9 +- scripts/src/HandlerABI.json | 657 +++++++++ scripts/src/config.poc5.json | 19 +- scripts/src/console.js | 175 ++- scripts/src/registry.json | 565 ++++++++ scripts/src/scheduler.js | 82 -- scripts/src/utils.js | 21 +- 55 files changed, 7090 insertions(+), 1642 deletions(-) delete mode 160000 chain create mode 100644 contracts/index_executor/src/abi/SygmaBasicFeeHandler.json create mode 100644 contracts/index_executor/src/abi/SygmaBridge.json create mode 100644 contracts/index_executor/src/abi/WETH9.json create mode 100644 contracts/index_executor/src/abi/uniswapV3Router.json create mode 100644 contracts/index_executor/src/actions/astar/xtokens.rs create mode 100644 contracts/index_executor/src/actions/base/native_wrapper.rs create mode 100644 contracts/index_executor/src/actions/base/sub_transactor.rs create mode 100644 contracts/index_executor/src/actions/base/uniswapv3.rs create mode 100644 contracts/index_executor/src/actions/ethereum/sygma.rs create mode 100644 contracts/index_executor/src/actions/phala/sygma.rs create mode 100644 contracts/index_executor/src/actions/polkadot/mod.rs create mode 100644 contracts/index_executor/src/actions/polkadot/xcm_v2.rs create mode 100644 contracts/index_executor/src/actions/polkadot/xcm_v3.rs create mode 100644 contracts/index_executor/src/price.rs create mode 100644 scripts/src/HandlerABI.json create mode 100644 scripts/src/registry.json delete mode 100644 scripts/src/scheduler.js diff --git a/.github/actions/install_toolchain/action.yml b/.github/actions/install_toolchain/action.yml index a80b3e18..f258e12e 100644 --- a/.github/actions/install_toolchain/action.yml +++ b/.github/actions/install_toolchain/action.yml @@ -6,7 +6,7 @@ runs: - name: Install latest nightly uses: actions-rs/toolchain@v1 with: - toolchain: nightly-2023-02-03 + toolchain: nightly-2023-12-26 override: true target: wasm32-unknown-unknown components: rustfmt diff --git a/.github/workflows/cargo-contract-build.yml b/.github/workflows/cargo-contract-build.yml index 8fbf7bfb..5154a61e 100644 --- a/.github/workflows/cargo-contract-build.yml +++ b/.github/workflows/cargo-contract-build.yml @@ -18,7 +18,7 @@ jobs: submodules: "true" - uses: ./.github/actions/install_toolchain - name: Install cargo-contract - run: rustup component add rust-src && cargo install cargo-contract --version ^2 --force --locked + run: rustup component add rust-src && cargo install cargo-contract --version ^3.2 --force --locked - name: Install Binaryen run: "cd /tmp \ && curl -OL https://github.com/WebAssembly/binaryen/releases/download/version_110/binaryen-version_110-x86_64-linux.tar.gz \ diff --git a/chain b/chain deleted file mode 160000 index 8139271b..00000000 --- a/chain +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8139271bddb2f5a5d337947caf291401b1e2fdcb diff --git a/contracts/index_executor/Cargo.toml b/contracts/index_executor/Cargo.toml index 80b29f79..8506ed73 100644 --- a/contracts/index_executor/Cargo.toml +++ b/contracts/index_executor/Cargo.toml @@ -24,6 +24,7 @@ primitive-types = { version = "0.12.1", default-features = false, features = ["c scale = { package = "parity-scale-codec", version = "3.6.4", default-features = false, features = ["derive"] } scale-info = { version = "2.9.0", default-features = false, features = ["derive"], optional = true } xcm = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.43", default-features = false } + pink-kv-session = { version = "0.2.0", default-features = false } pink-extension = { version = "0.4", default-features = false, features = ["dlmalloc"] } pink-web3 = { version = "0.20.2", git = "https://github.com/Phala-Network/pink-web3.git", branch = "pink", default-features = false, features = ["pink"]} @@ -38,7 +39,6 @@ hex-literal = "0.4.1" pink-extension-runtime = "0.4.4" dotenv = "0.15.0" hex = "0.4.3" - sp-runtime = { version = "7.0.0", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.43", default-features = false } [profile.release] diff --git a/contracts/index_executor/src/abi/SygmaBasicFeeHandler.json b/contracts/index_executor/src/abi/SygmaBasicFeeHandler.json new file mode 100644 index 00000000..8e49ef66 --- /dev/null +++ b/contracts/index_executor/src/abi/SygmaBasicFeeHandler.json @@ -0,0 +1,507 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "bridgeAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "feeHandlerRouterAddress", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "newFee", + "type": "uint256" + } + ], + "name": "FeeChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "fromDomainID", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "destinationDomainID", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "resourceID", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "tokenAddress", + "type": "address" + } + ], + "name": "FeeCollected", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "FeeDistributed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "_bridgeAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "_fee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "_feeHandlerRouterAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleAdmin", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "getRoleMember", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleMemberCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "getRoleMemberIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "hasRole", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint8", + "name": "fromDomainID", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "destinationDomainID", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "resourceID", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "depositData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "feeData", + "type": "bytes" + } + ], + "name": "collectFee", + "outputs": [], + "stateMutability": "payable", + "type": "function", + "payable": true + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint8", + "name": "fromDomainID", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "destinationDomainID", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "resourceID", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "depositData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "feeData", + "type": "bytes" + } + ], + "name": "calculateFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newFee", + "type": "uint256" + } + ], + "name": "changeFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable[]", + "name": "addrs", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "name": "transferFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/contracts/index_executor/src/abi/SygmaBridge.json b/contracts/index_executor/src/abi/SygmaBridge.json new file mode 100644 index 00000000..677cad34 --- /dev/null +++ b/contracts/index_executor/src/abi/SygmaBridge.json @@ -0,0 +1,723 @@ +[ + { + "inputs": [ + { + "internalType": "uint8", + "name": "domainID", + "type": "uint8" + }, + { + "internalType": "address", + "name": "accessControl", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "newAccessControl", + "type": "address" + } + ], + "name": "AccessControlChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "destinationDomainID", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "resourceID", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "depositNonce", + "type": "uint64" + }, + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "handlerResponse", + "type": "bytes" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "EndKeygen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes", + "name": "lowLevelData", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "originDomainID", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "depositNonce", + "type": "uint64" + } + ], + "name": "FailedHandlerExecution", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "newFeeHandler", + "type": "address" + } + ], + "name": "FeeHandlerChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "hash", + "type": "string" + } + ], + "name": "KeyRefresh", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "originDomainID", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "depositNonce", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "dataHash", + "type": "bytes32" + } + ], + "name": "ProposalExecution", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "txHash", + "type": "string" + } + ], + "name": "Retry", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "StartKeygen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [], + "name": "_MPCAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "_accessControl", + "outputs": [ + { + "internalType": "contract IAccessControlSegregator", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "name": "_depositCounts", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "_domainID", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "_feeHandler", + "outputs": [ + { + "internalType": "contract IFeeHandler", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "_resourceIDToHandlerAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isValidForwarder", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "usedNonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "adminPauseTransfers", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "adminUnpauseTransfers", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "handlerAddress", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "resourceID", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "internalType": "bytes", + "name": "args", + "type": "bytes" + } + ], + "name": "adminSetResource", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "handlerAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + } + ], + "name": "adminSetBurnable", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "domainID", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + } + ], + "name": "adminSetDepositNonce", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "forwarder", + "type": "address" + }, + { + "internalType": "bool", + "name": "valid", + "type": "bool" + } + ], + "name": "adminSetForwarder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newAccessControl", + "type": "address" + } + ], + "name": "adminChangeAccessControl", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newFeeHandler", + "type": "address" + } + ], + "name": "adminChangeFeeHandler", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "handlerAddress", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "adminWithdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "destinationDomainID", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "resourceID", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "depositData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "feeData", + "type": "bytes" + } + ], + "name": "deposit", + "outputs": [], + "stateMutability": "payable", + "type": "function", + "payable": true + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint8", + "name": "originDomainID", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "depositNonce", + "type": "uint64" + }, + { + "internalType": "bytes32", + "name": "resourceID", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct Bridge.Proposal", + "name": "proposal", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "executeProposal", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint8", + "name": "originDomainID", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "depositNonce", + "type": "uint64" + }, + { + "internalType": "bytes32", + "name": "resourceID", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct Bridge.Proposal[]", + "name": "proposals", + "type": "tuple[]" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "executeProposals", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "startKeygen", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "MPCAddress", + "type": "address" + } + ], + "name": "endKeygen", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "hash", + "type": "string" + } + ], + "name": "refreshKey", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "txHash", + "type": "string" + } + ], + "name": "retry", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "domainID", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "depositNonce", + "type": "uint256" + } + ], + "name": "isProposalExecuted", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint8", + "name": "originDomainID", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "depositNonce", + "type": "uint64" + }, + { + "internalType": "bytes32", + "name": "resourceID", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct Bridge.Proposal[]", + "name": "proposals", + "type": "tuple[]" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "verify", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + } +] diff --git a/contracts/index_executor/src/abi/WETH9.json b/contracts/index_executor/src/abi/WETH9.json new file mode 100644 index 00000000..b30860b1 --- /dev/null +++ b/contracts/index_executor/src/abi/WETH9.json @@ -0,0 +1,286 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "guy", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Withdrawal", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "guy", + "type": "address" + }, + { + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "deposit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ] \ No newline at end of file diff --git a/contracts/index_executor/src/abi/handler.json b/contracts/index_executor/src/abi/handler.json index e20e620f..38549b91 100644 --- a/contracts/index_executor/src/abi/handler.json +++ b/contracts/index_executor/src/abi/handler.json @@ -68,12 +68,6 @@ "internalType": "bytes", "name": "recipient", "type": "bytes" - }, - { - "indexed": false, - "internalType": "string", - "name": "task", - "type": "string" } ], "name": "Deposited", @@ -143,6 +137,25 @@ "name": "Unpaused", "type": "event" }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "_activedTaskIndexs", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -183,24 +196,19 @@ "type": "address" }, { - "internalType": "contract IERC20", + "internalType": "address", "name": "token", "type": "address" }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, { "internalType": "bytes", "name": "recipient", "type": "bytes" }, { - "internalType": "string", - "name": "task", - "type": "string" + "internalType": "uint256", + "name": "amount", + "type": "uint256" } ], "stateMutability": "view", @@ -259,6 +267,11 @@ "name": "updateLen", "type": "uint256" }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, { "internalType": "address", "name": "spendAsset", @@ -292,7 +305,7 @@ ], "name": "batchCall", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -315,6 +328,11 @@ "name": "taskId", "type": "bytes32" }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, { "components": [ { @@ -347,6 +365,11 @@ "name": "updateLen", "type": "uint256" }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, { "internalType": "address", "name": "spendAsset", @@ -380,7 +403,7 @@ ], "name": "claimAndBatchCall", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -409,16 +432,11 @@ "internalType": "bytes32", "name": "taskId", "type": "bytes32" - }, - { - "internalType": "string", - "name": "task", - "type": "string" } ], "name": "deposit", "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -434,6 +452,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "feeAccount", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -457,24 +488,19 @@ "type": "address" }, { - "internalType": "contract IERC20", + "internalType": "address", "name": "token", "type": "address" }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, { "internalType": "bytes", "name": "recipient", "type": "bytes" }, { - "internalType": "string", - "name": "task", - "type": "string" + "internalType": "uint256", + "name": "amount", + "type": "uint256" } ], "internalType": "struct Handler.DepositInfo", @@ -512,7 +538,7 @@ "type": "address" } ], - "name": "getLastActivedTask", + "name": "getNextActivedTask", "outputs": [ { "internalType": "bytes32", @@ -541,24 +567,19 @@ "type": "address" }, { - "internalType": "contract IERC20", + "internalType": "address", "name": "token", "type": "address" }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, { "internalType": "bytes", "name": "recipient", "type": "bytes" }, { - "internalType": "string", - "name": "task", - "type": "string" + "internalType": "uint256", + "name": "amount", + "type": "uint256" } ], "internalType": "struct Handler.DepositInfo", @@ -569,6 +590,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "nativeAsset", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "owner", @@ -582,6 +616,13 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "paused", @@ -615,6 +656,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_feeAccount", + "type": "address" + } + ], + "name": "setFeeAccount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -628,6 +682,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "native", + "type": "address" + } + ], + "name": "setNative", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -653,5 +720,16 @@ "outputs": [], "stateMutability": "nonpayable", "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" } ] \ No newline at end of file diff --git a/contracts/index_executor/src/abi/uniswapV3Router.json b/contracts/index_executor/src/abi/uniswapV3Router.json new file mode 100644 index 00000000..62aff359 --- /dev/null +++ b/contracts/index_executor/src/abi/uniswapV3Router.json @@ -0,0 +1,634 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_factory", + "type": "address" + }, + { + "internalType": "address", + "name": "_WNativeToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_poolDeployer", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "WNativeToken", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes", + "name": "path", + "type": "bytes" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMinimum", + "type": "uint256" + } + ], + "internalType": "struct ISwapRouter.ExactInputParams", + "name": "params", + "type": "tuple" + } + ], + "name": "exactInput", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMinimum", + "type": "uint256" + }, + { + "internalType": "uint160", + "name": "sqrtPriceLimitX96", + "type": "uint160" + } + ], + "internalType": "struct ISwapRouter.ExactInputSingleParams", + "name": "params", + "type": "tuple" + } + ], + "name": "exactInputSingle", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMinimum", + "type": "uint256" + }, + { + "internalType": "uint160", + "name": "sqrtPriceLimitX96", + "type": "uint160" + } + ], + "internalType": "struct ISwapRouter.ExactInputSingleParams", + "name": "params", + "type": "tuple" + } + ], + "name": "exactInputSingleSupportingFeeOnTransferTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes", + "name": "path", + "type": "bytes" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountInMaximum", + "type": "uint256" + } + ], + "internalType": "struct ISwapRouter.ExactOutputParams", + "name": "params", + "type": "tuple" + } + ], + "name": "exactOutput", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountInMaximum", + "type": "uint256" + }, + { + "internalType": "uint160", + "name": "sqrtPriceLimitX96", + "type": "uint160" + } + ], + "internalType": "struct ISwapRouter.ExactOutputSingleParams", + "name": "params", + "type": "tuple" + } + ], + "name": "exactOutputSingle", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int256", + "name": "amount0Delta", + "type": "int256" + }, + { + "internalType": "int256", + "name": "amount1Delta", + "type": "int256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "AlgebraSwapCallback", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "data", + "type": "bytes[]" + } + ], + "name": "multicall", + "outputs": [ + { + "internalType": "bytes[]", + "name": "results", + "type": "bytes[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "poolDeployer", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "refundNativeToken", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "selfPermit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "expiry", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "selfPermitAllowed", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "expiry", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "selfPermitAllowedIfNecessary", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "selfPermitIfNecessary", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountMinimum", + "type": "uint256" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "sweepToken", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountMinimum", + "type": "uint256" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "feeBips", + "type": "uint256" + }, + { + "internalType": "address", + "name": "feeRecipient", + "type": "address" + } + ], + "name": "sweepTokenWithFee", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountMinimum", + "type": "uint256" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "unwrapWNativeToken", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountMinimum", + "type": "uint256" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "feeBips", + "type": "uint256" + }, + { + "internalType": "address", + "name": "feeRecipient", + "type": "address" + } + ], + "name": "unwrapWNativeTokenWithFee", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ] \ No newline at end of file diff --git a/contracts/index_executor/src/actions/acala/dex.rs b/contracts/index_executor/src/actions/acala/dex.rs index 0386bde5..dcb08592 100644 --- a/contracts/index_executor/src/actions/acala/dex.rs +++ b/contracts/index_executor/src/actions/acala/dex.rs @@ -1,5 +1,5 @@ use super::asset::{AcalaAssets, AggregatedSwapPath, CurrencyId, TokenSymbol}; -use alloc::{format, vec, vec::Vec}; +use alloc::{format, vec}; use pink_extension::ResultExt; use scale::{Compact, Decode, Encode}; use xcm::v3::prelude::*; @@ -21,7 +21,7 @@ impl AcalaSwap { } impl CallBuilder for AcalaSwap { - fn build_call(&self, step: Step) -> Result, &'static str> { + fn build_call(&self, step: Step) -> Result { let amount_out = Compact(1_u8); let amount_in = Compact(step.spend_amount.ok_or("MissingSpendAmount")?); @@ -58,7 +58,7 @@ impl CallBuilder for AcalaSwap { ]); let path = vec![taiga_path, dex_path]; - Ok(vec![Call { + Ok(Call { params: CallParams::Sub(SubCall { calldata: SubExtrinsic { // the call index of acala dex module @@ -72,6 +72,6 @@ impl CallBuilder for AcalaSwap { }), input_call: None, call_index: None, - }]) + }) } } diff --git a/contracts/index_executor/src/actions/acala/mod.rs b/contracts/index_executor/src/actions/acala/mod.rs index 03c2364b..a18c60dd 100644 --- a/contracts/index_executor/src/actions/acala/mod.rs +++ b/contracts/index_executor/src/actions/acala/mod.rs @@ -2,9 +2,12 @@ pub mod asset; pub mod dex; pub mod transfer; +use crate::actions::ActionExtraInfo; use crate::call::CallBuilder; use crate::chain::Chain; +use crate::constants::PARACHAIN_BLOCK_TIME; use alloc::{boxed::Box, string::String, vec, vec::Vec}; +use sp_runtime::Permill; pub fn create_actions(_chain: &Chain) -> Vec<(String, Box)> { vec![ @@ -15,3 +18,24 @@ pub fn create_actions(_chain: &Chain) -> Vec<(String, Box)> { ), ] } + +pub fn get_extra_info(chain: &str, action: &str) -> Option { + assert!(chain == "Acala"); + if action == "acala_dex" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + const_proto_fee_in_usd: 0, + percentage_proto_fee: Permill::from_perthousand(3), + confirm_time_in_sec: PARACHAIN_BLOCK_TIME, + }) + } else if action == "acala_transactor" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + const_proto_fee_in_usd: 0, + percentage_proto_fee: Permill::zero(), + confirm_time_in_sec: PARACHAIN_BLOCK_TIME, + }) + } else { + None + } +} diff --git a/contracts/index_executor/src/actions/acala/transfer.rs b/contracts/index_executor/src/actions/acala/transfer.rs index d3a8012b..f81dfc26 100644 --- a/contracts/index_executor/src/actions/acala/transfer.rs +++ b/contracts/index_executor/src/actions/acala/transfer.rs @@ -5,7 +5,6 @@ use super::asset::{AcalaAssets, CurrencyId, TokenType as AcalaTokenType}; use crate::call::{Call, CallBuilder, CallParams, SubCall, SubExtrinsic}; use crate::step::Step; use crate::utils::ToArray; -use alloc::{vec, vec::Vec}; use scale::{Compact, Decode, Encode}; type MultiAddress = sp_runtime::MultiAddress; @@ -23,10 +22,10 @@ impl AcalaTransactor { } impl CallBuilder for AcalaTransactor { - fn build_call(&self, step: Step) -> Result, &'static str> { + fn build_call(&self, step: Step) -> Result { let asset_location = MultiLocation::decode(&mut step.spend_asset.as_slice()) .map_err(|_| "FailedToScaleDecode")?; - let bytes: [u8; 32] = step.recipient.ok_or("MissingRecipient")?.to_array(); + let bytes: [u8; 32] = step.recipient.to_array(); let recipient = MultiAddress::Id(AccountId::from(bytes)); let asset_attrs = AcalaAssets::get_asset_attrs(&asset_location).ok_or("BadAsset")?; let currency_id = CurrencyId::Token(asset_attrs.0); @@ -35,7 +34,7 @@ impl CallBuilder for AcalaTransactor { match asset_type { AcalaTokenType::Utility => { - Ok(vec![Call { + Ok(Call { params: CallParams::Sub(SubCall { calldata: SubExtrinsic { // Balance @@ -47,7 +46,7 @@ impl CallBuilder for AcalaTransactor { }), input_call: None, call_index: None, - }]) + }) } _ => { let currency_id = match asset_type { @@ -57,7 +56,7 @@ impl CallBuilder for AcalaTransactor { } _ => currency_id, }; - Ok(vec![Call { + Ok(Call { params: CallParams::Sub(SubCall { calldata: SubExtrinsic { // Currencies @@ -69,7 +68,7 @@ impl CallBuilder for AcalaTransactor { }), input_call: None, call_index: None, - }]) + }) } } } diff --git a/contracts/index_executor/src/actions/astar/asset.rs b/contracts/index_executor/src/actions/astar/asset.rs index c14a4742..c246bda3 100644 --- a/contracts/index_executor/src/actions/astar/asset.rs +++ b/contracts/index_executor/src/actions/astar/asset.rs @@ -24,7 +24,18 @@ impl AstarAssets { // PHA ( 18446744073709551622_u128, - MultiLocation::new(1, X1(Parachain(2035))), + MultiLocation::new(1, X1(Parachain(crate::constants::PHALA_PARACHAIN_ID))), + ), + // GLMR + ( + 18446744073709551619_u128, + MultiLocation::new( + 1, + X2( + Parachain(crate::constants::MOONBEAM_PARACHAIN_ID), + PalletInstance(10), + ), + ), ), ], )], @@ -33,9 +44,20 @@ impl AstarAssets { vec![ // PHA ( - MultiLocation::new(1, X1(Parachain(2035))), + MultiLocation::new(1, X1(Parachain(crate::constants::PHALA_PARACHAIN_ID))), 18446744073709551622_u128, ), + // GLMR + ( + MultiLocation::new( + 1, + X2( + Parachain(crate::constants::MOONBEAM_PARACHAIN_ID), + PalletInstance(10), + ), + ), + 18446744073709551619_u128, + ), ], )], } diff --git a/contracts/index_executor/src/actions/astar/mod.rs b/contracts/index_executor/src/actions/astar/mod.rs index 7d785919..e5a65ec4 100644 --- a/contracts/index_executor/src/actions/astar/mod.rs +++ b/contracts/index_executor/src/actions/astar/mod.rs @@ -1,24 +1,45 @@ pub mod asset; mod sub; +mod xtokens; -use crate::actions::base::uniswapv2; +use crate::actions::base::{native_wrapper, uniswapv2}; +use crate::actions::ActionExtraInfo; +use sp_runtime::Permill; pub type AstarArthSwap = uniswapv2::UniswapV2; +pub type AstarNativeWrapper = native_wrapper::NativeWrapper; use crate::call::CallBuilder; use crate::chain::Chain; +use crate::constants::PARACHAIN_BLOCK_TIME; +use crate::constants::PHALA_PARACHAIN_ID; +use crate::utils::ToArray; use alloc::{boxed::Box, string::String, vec, vec::Vec}; pub fn evm_create_actions(chain: &Chain) -> Vec<(String, Box)> { - let arthswap_pancake_router: [u8; 20] = - hex_literal::hex!("E915D2393a08a00c5A463053edD31bAe2199b9e7"); - vec![( - String::from("astar_evm_arthswap"), - Box::new(AstarArthSwap::new( - &chain.endpoint, - arthswap_pancake_router.into(), - )), - )] + let arthswap_pancake_router: [u8; 20] = hex::decode("E915D2393a08a00c5A463053edD31bAe2199b9e7") + .unwrap() + .to_array(); + let astar_evm_wastr: [u8; 20] = hex_literal::hex!("Aeaaf0e2c81Af264101B9129C00F4440cCF0F720"); + let astar_evm_astr: [u8; 20] = hex_literal::hex!("0000000000000000000000000000000000000000"); + + vec![ + ( + String::from("astar_evm_nativewrapper"), + Box::new(AstarNativeWrapper::new( + &chain.endpoint, + astar_evm_wastr.into(), + astar_evm_astr.into(), + )), + ), + ( + String::from("astar_evm_arthswap"), + Box::new(AstarArthSwap::new( + &chain.endpoint, + arthswap_pancake_router.into(), + )), + ), + ] } pub fn sub_create_actions(chain: &Chain) -> Vec<(String, Box)> { @@ -33,5 +54,53 @@ pub fn sub_create_actions(chain: &Chain) -> Vec<(String, Box)> chain.native_asset.clone(), )), ), + ( + String::from("astar_bridge_to_phala"), + Box::new(xtokens::AstarXtokens::new(PHALA_PARACHAIN_ID)), + ), ] } + +#[allow(clippy::if_same_then_else)] +pub fn get_extra_info(chain: &str, action: &str) -> Option { + assert!(chain == "Astar" || chain == "AstarEvm"); + if action == "astar_evm_nativewrapper" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + const_proto_fee_in_usd: 0, + percentage_proto_fee: Permill::zero(), + confirm_time_in_sec: PARACHAIN_BLOCK_TIME, + }) + } else if action == "astar_evm_arthswap" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + const_proto_fee_in_usd: 0, + percentage_proto_fee: Permill::from_perthousand(3), + confirm_time_in_sec: PARACHAIN_BLOCK_TIME, + }) + } else if action == "astar_transactor" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + const_proto_fee_in_usd: 0, + percentage_proto_fee: Permill::zero(), + confirm_time_in_sec: PARACHAIN_BLOCK_TIME, + }) + } else if action == "astar_bridge_to_astarevm" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + const_proto_fee_in_usd: 0, + percentage_proto_fee: Permill::zero(), + confirm_time_in_sec: PARACHAIN_BLOCK_TIME, + }) + } else if action == "astar_bridge_to_phala" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + // 0.0005 USD + const_proto_fee_in_usd: 5, + percentage_proto_fee: Permill::zero(), + confirm_time_in_sec: PARACHAIN_BLOCK_TIME * 2, + }) + } else { + None + } +} diff --git a/contracts/index_executor/src/actions/astar/sub.rs b/contracts/index_executor/src/actions/astar/sub.rs index 948f19fb..b7bc1980 100644 --- a/contracts/index_executor/src/actions/astar/sub.rs +++ b/contracts/index_executor/src/actions/astar/sub.rs @@ -4,9 +4,8 @@ use xcm::v3::prelude::*; use super::asset::AstarAssets; use crate::call::{Call, CallBuilder, CallParams, SubCall, SubExtrinsic}; use crate::step::Step; -use crate::utils::ToArray; -use alloc::{string::String, vec, vec::Vec}; -use pink_subrpc::hasher::{Blake2_256, Hasher}; +use crate::utils::{h160_to_sr25519_pub, ToArray}; +use alloc::{string::String, vec::Vec}; use scale::{Compact, Decode, Encode}; type MultiAddress = sp_runtime::MultiAddress; @@ -27,17 +26,11 @@ impl AstarSubToEvmTransactor { } } -impl AstarSubToEvmTransactor { - fn h160_to_sr25519_pub(&self, addr: &[u8]) -> [u8; 32] { - Blake2_256::hash(&[b"evm:", addr].concat()) - } -} - impl CallBuilder for AstarSubToEvmTransactor { - fn build_call(&self, step: Step) -> Result, &'static str> { - let bytes: [u8; 20] = step.recipient.clone().ok_or("MissingRecipient")?.to_array(); + fn build_call(&self, step: Step) -> Result { + let bytes: [u8; 20] = step.recipient.to_array(); let mut new_step = step; - new_step.recipient = Some(self.h160_to_sr25519_pub(&bytes).to_vec()); + new_step.recipient = h160_to_sr25519_pub(&bytes).to_vec(); self.transactor.build_call(new_step) } } @@ -57,15 +50,15 @@ impl AstarTransactor { } impl CallBuilder for AstarTransactor { - fn build_call(&self, step: Step) -> Result, &'static str> { + fn build_call(&self, step: Step) -> Result { let asset_location = MultiLocation::decode(&mut step.spend_asset.as_slice()) .map_err(|_| "FailedToScaleDecode")?; - let bytes: [u8; 32] = step.recipient.ok_or("MissingRecipient")?.to_array(); + let bytes: [u8; 32] = step.recipient.to_array(); let recipient = MultiAddress::Id(AccountId::from(bytes)); let amount = Compact(step.spend_amount.ok_or("MissingSpendAmount")?); if step.spend_asset == self.native { - Ok(vec![Call { + Ok(Call { params: CallParams::Sub(SubCall { calldata: SubExtrinsic { // Balance @@ -77,12 +70,12 @@ impl CallBuilder for AstarTransactor { }), input_call: None, call_index: None, - }]) + }) } else { let asset_id = AstarAssets::new() .get_assetid(&String::from("Astar"), &asset_location) .ok_or("AssetNotFound")?; - Ok(vec![Call { + Ok(Call { params: CallParams::Sub(SubCall { calldata: SubExtrinsic { // palletAsset @@ -94,7 +87,7 @@ impl CallBuilder for AstarTransactor { }), input_call: None, call_index: None, - }]) + }) } } } @@ -123,23 +116,22 @@ mod tests { let secret_bytes = hex::decode(secret_key).unwrap(); let signer: [u8; 32] = secret_bytes.to_array(); - let calls = transactor + let call = transactor .build_call(Step { - exe_type: String::from(""), exe: String::from(""), source_chain: String::from("Astar"), dest_chain: String::from("Astar"), spend_asset: astr_location.encode(), receive_asset: astr_location.encode(), sender: None, - recipient: Some(recipient), + recipient, // 0.1 ASTR spend_amount: Some(1_00_000_000_000_000_000 as u128), origin_balance: None, nonce: None, }) .unwrap(); - match &calls[0].params { + match &call.params { CallParams::Sub(sub_call) => { let signed_tx = create_transaction_with_calldata( &signer, @@ -182,23 +174,22 @@ mod tests { let secret_bytes = hex::decode(secret_key).unwrap(); let signer: [u8; 32] = secret_bytes.to_array(); - let calls = transactor + let call = transactor .build_call(Step { - exe_type: String::from(""), exe: String::from(""), source_chain: String::from("Astar"), dest_chain: String::from("Astar"), spend_asset: pha_location.encode(), receive_asset: pha_location.encode(), sender: None, - recipient: Some(recipient), + recipient, // 0.1 PHA spend_amount: Some(1_00_000_000_000 as u128), origin_balance: None, nonce: None, }) .unwrap(); - match &calls[0].params { + match &call.params { CallParams::Sub(sub_call) => { let signed_tx = create_transaction_with_calldata( &signer, @@ -239,23 +230,22 @@ mod tests { let secret_bytes = hex::decode(secret_key).unwrap(); let signer: [u8; 32] = secret_bytes.to_array(); - let calls = transactor + let call = transactor .build_call(Step { - exe_type: String::from("bridge"), exe: String::from(""), source_chain: String::from("Astar"), dest_chain: String::from("AstarEvm"), spend_asset: pha_location.encode(), receive_asset: pha_location.encode(), sender: None, - recipient: Some(h160_recipient), + recipient: h160_recipient, // 0.1 PHA spend_amount: Some(1_00_000_000_000 as u128), origin_balance: None, nonce: None, }) .unwrap(); - match &calls[0].params { + match &call.params { CallParams::Sub(sub_call) => { let signed_tx = create_transaction_with_calldata( &signer, diff --git a/contracts/index_executor/src/actions/astar/xtokens.rs b/contracts/index_executor/src/actions/astar/xtokens.rs new file mode 100644 index 00000000..7a28548c --- /dev/null +++ b/contracts/index_executor/src/actions/astar/xtokens.rs @@ -0,0 +1,99 @@ +use crate::call::{Call, CallBuilder, CallParams, SubCall, SubExtrinsic}; +use crate::step::Step; +use crate::utils::ToArray; +use scale::{Decode, Encode}; +use xcm::{v3::prelude::*, VersionedMultiAsset, VersionedMultiLocation}; + +#[derive(Clone)] +pub struct AstarXtokens { + dest_chain_id: u32, +} + +impl AstarXtokens { + pub fn new(dest_chain_id: u32) -> Self + where + Self: Sized, + { + Self { dest_chain_id } + } +} + +impl CallBuilder for AstarXtokens { + fn build_call(&self, step: Step) -> Result { + let asset_location: MultiLocation = + Decode::decode(&mut step.spend_asset.as_slice()).map_err(|_| "InvalidMultilocation")?; + let multi_asset = VersionedMultiAsset::V3(MultiAsset { + id: AssetId::Concrete(asset_location), + fun: Fungibility::Fungible(step.spend_amount.ok_or("MissingSpendAmount")?), + }); + let dest = VersionedMultiLocation::V3(MultiLocation::new( + 1, + Junctions::X2( + Parachain(self.dest_chain_id), + match step.recipient.len() { + 20 => AccountKey20 { + network: None, + key: step.recipient.to_array(), + }, + 32 => AccountId32 { + network: None, + id: step.recipient.to_array(), + }, + _ => return Err("InvalidRecipient"), + }, + ), + )); + let dest_weight: WeightLimit = WeightLimit::Unlimited; + + Ok(Call { + params: CallParams::Sub(SubCall { + calldata: SubExtrinsic { + pallet_id: 0x37u8, + call_id: 0x01u8, + call: (multi_asset, dest, dest_weight), + } + .encode(), + }), + input_call: None, + call_index: None, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::constants::PHALA_PARACHAIN_ID; + + #[test] + fn test_bridge_to_phala() { + let xtokens = AstarXtokens { + dest_chain_id: PHALA_PARACHAIN_ID, + }; + let call = xtokens + .build_call(Step { + exe: String::from(""), + source_chain: String::from("Astar"), + dest_chain: String::from("Phala"), + spend_asset: hex::decode("010100cd1f").unwrap(), + receive_asset: hex::decode("0000").unwrap(), + sender: None, + recipient: hex::decode( + "04dba0677fc274ffaccc0fa1030a66b171d1da9226d2bb9d152654e6a746f276", + ) + .unwrap(), + // 2 PHA + spend_amount: Some(2_000_000_000_000 as u128), + origin_balance: None, + nonce: None, + }) + .unwrap(); + + match &call.params { + CallParams::Sub(sub_call) => { + println!("calldata: {:?}", hex::encode(&sub_call.calldata)) + } + _ => assert!(false), + } + } +} diff --git a/contracts/index_executor/src/actions/base/mod.rs b/contracts/index_executor/src/actions/base/mod.rs index 5d9ff811..2c6041b3 100644 --- a/contracts/index_executor/src/actions/base/mod.rs +++ b/contracts/index_executor/src/actions/base/mod.rs @@ -1,2 +1,5 @@ pub mod account; +pub mod native_wrapper; +pub mod sub_transactor; pub mod uniswapv2; +pub mod uniswapv3; diff --git a/contracts/index_executor/src/actions/base/native_wrapper.rs b/contracts/index_executor/src/actions/base/native_wrapper.rs new file mode 100644 index 00000000..8f779f1d --- /dev/null +++ b/contracts/index_executor/src/actions/base/native_wrapper.rs @@ -0,0 +1,224 @@ +use pink_web3::{ + api::{Eth, Namespace}, + contract::{tokens::Tokenize, Contract}, + ethabi::Address, + transports::PinkHttp, + types::U256, +}; + +use crate::call::{Call, CallBuilder, CallParams, EvmCall}; +use crate::step::Step; + +#[derive(Clone)] +pub struct NativeWrapper { + pub eth: Eth, + pub weth9: Contract, + pub native: Address, +} + +impl NativeWrapper { + pub fn new(rpc: &str, weth9: Address, native: Address) -> Self { + let eth = Eth::new(PinkHttp::new(rpc)); + let weth9 = Contract::from_json(eth.clone(), weth9, include_bytes!("../../abi/WETH9.json")) + .expect("Bad abi data"); + + Self { eth, weth9, native } + } +} + +impl CallBuilder for NativeWrapper { + fn build_call(&self, step: Step) -> Result { + let spend_asset = Address::from_slice(&step.spend_asset); + let receive_asset = Address::from_slice(&step.receive_asset); + let spend_amount = U256::from(step.spend_amount.ok_or("MissingSpendAmount")?); + + if spend_asset == self.native && receive_asset == self.weth9.address() { + // Deposit + let deposit_func = self + .weth9 + .abi() + .function("deposit") + .map_err(|_| "NoFunctionFound")?; + let deposit_calldata = deposit_func + .encode_input(&[]) + .map_err(|_| "EncodeParamError")?; + Ok(Call { + params: CallParams::Evm(EvmCall { + target: self.weth9.address(), + calldata: deposit_calldata, + value: spend_amount, + + need_settle: true, + update_offset: U256::from(0), + update_len: U256::from(0), + // No spender + spender: Address::from(&[0; 20]), + spend_asset, + spend_amount, + receive_asset, + }), + input_call: None, + call_index: None, + }) + } else if spend_asset == self.weth9.address() && receive_asset == self.native { + // Withdraw + let withdraw_func = self + .weth9 + .abi() + .function("withdraw") + .map_err(|_| "NoFunctionFound")?; + let withdraw_calldata = withdraw_func + .encode_input(&spend_amount.into_tokens()) + .map_err(|_| "EncodeParamError")?; + Ok(Call { + params: CallParams::Evm(EvmCall { + target: self.weth9.address(), + calldata: withdraw_calldata, + value: U256::from(0), + + need_settle: true, + update_offset: U256::from(4), + update_len: U256::from(32), + // No spender + spender: Address::from(&[0; 20]), + spend_asset, + spend_amount, + receive_asset, + }), + input_call: None, + call_index: None, + }) + } else { + Err("UnrecognizedArguments") + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::ToArray; + use dotenv::dotenv; + use primitive_types::H160; + + use pink_web3::{ + api::{Eth, Namespace}, + contract::{Contract, Options}, + keys::pink::KeyPair, + transports::{resolve_ready, PinkHttp}, + }; + + #[test] + #[ignore] + fn should_work() { + dotenv().ok(); + pink_extension_runtime::mock_ext::mock_all_ext(); + use pink_web3::types::Address; + + let secret_key = std::env::vars().find(|x| x.0 == "SECRET_KEY"); + let secret_key = secret_key.unwrap().1; + let secret_bytes = hex::decode(secret_key).unwrap(); + let signer: [u8; 32] = secret_bytes.to_array(); + + // Handler on Moonbeam + let handler_address: H160 = + H160::from_slice(&hex::decode("50a0D445E2Df1255e01B27F932D90421305a8eCA").unwrap()); + let transport = Eth::new(PinkHttp::new("https://rpc.api.moonbeam.network")); + let handler = Contract::from_json( + transport, + handler_address, + include_bytes!("../../abi/handler.json"), + ) + .unwrap(); + + let wglmr: [u8; 20] = hex::decode("Acc15dC74880C9944775448304B263D191c6077F") + .unwrap() + .to_array(); + let glmr: [u8; 20] = hex::decode("0000000000000000000000000000000000000802") + .unwrap() + .to_array(); + let native_wrapper: NativeWrapper = NativeWrapper::new( + "https://rpc.api.moonbeam.network", + wglmr.into(), + glmr.into(), + ); + let mut deposit_call = native_wrapper + .build_call(Step { + exe: String::from(""), + source_chain: String::from("Moonbeam"), + dest_chain: String::from("Moonbeam"), + // glmr + spend_asset: glmr.into(), + // wglmr + receive_asset: wglmr.into(), + sender: None, + recipient: hex::decode("bf526928373748b00763875448ee905367d97f96").unwrap(), + // 0.1 glmr + spend_amount: Some(100_000_000_000_000_000 as u128), + origin_balance: None, + nonce: None, + }) + .unwrap(); + + let mut withdraw_call = native_wrapper + .build_call(Step { + exe: String::from(""), + source_chain: String::from("Moonbeam"), + dest_chain: String::from("Moonbeam"), + // wglmr + spend_asset: wglmr.into(), + // glmr + receive_asset: glmr.into(), + sender: None, + recipient: hex::decode("bf526928373748b00763875448ee905367d97f96").unwrap(), + // 0.05 wglmr, will be updated to 0.1 wglmr bc we set deposit call as input call where we got 0.1 wglmr + spend_amount: Some(50_000_000_000_000_000 as u128), + origin_balance: None, + nonce: None, + }) + .unwrap(); + + // Apply index mannually + deposit_call.input_call = Some(0); + deposit_call.call_index = Some(0); + withdraw_call.input_call = Some(0); + withdraw_call.call_index = Some(1); + + let calls = [deposit_call, withdraw_call].to_vec(); + + // Estiamte gas before submission + let gas = resolve_ready(handler.estimate_gas( + "batchCall", + calls.clone(), + Address::from_slice(&hex::decode("bf526928373748b00763875448ee905367d97f96").unwrap()), + Options::with(|opt| { + // 0.1 GLMR + opt.value = Some(U256::from(100_000_000_000_000_000_u128)); + }), + )) + .map_err(|e| { + println!("Failed to estimated step gas cost with error: {:?}", e); + "FailedToEstimateGas" + }) + .unwrap(); + + // test tx: https://moonscan.io/tx/0x5d10ef2d1232d8047a9f8ec3fba4048f4b262079ea8de761fb7d3f0a966a0e7e + // Uncomment if wanna send it to blockchain + let tx_id = resolve_ready(handler.signed_call( + "batchCall", + calls, + Options::with(|opt| { + opt.gas = Some(gas); + // 0.1 GLMR + opt.value = Some(U256::from(100_000_000_000_000_000_u128)); + }), + KeyPair::from(signer), + )) + .map_err(|e| { + println!("Failed to submit step execution tx with error: {:?}", e); + "FailedToSubmitTransaction" + }) + .unwrap(); + println!("native warpper test tx: {:?}", tx_id); + } +} diff --git a/contracts/index_executor/src/actions/base/sub_transactor.rs b/contracts/index_executor/src/actions/base/sub_transactor.rs new file mode 100644 index 00000000..2a77eae6 --- /dev/null +++ b/contracts/index_executor/src/actions/base/sub_transactor.rs @@ -0,0 +1,82 @@ +use pink_extension::AccountId; +use scale::{Compact, Encode}; + +use crate::call::{Call, CallBuilder, CallParams, SubCall, SubExtrinsic}; +use crate::step::Step; +use crate::utils::ToArray; + +type MultiAddress = sp_runtime::MultiAddress; + +#[derive(Clone)] +pub struct Transactor { + pallet_id: u8, + call_id: u8, +} + +impl Transactor { + pub fn new(pallet_id: u8, call_id: u8) -> Self + where + Self: Sized, + { + Self { pallet_id, call_id } + } +} + +impl CallBuilder for Transactor { + fn build_call(&self, step: Step) -> Result { + let dest = match step.recipient.len() { + 20 => MultiAddress::Address20(step.recipient.to_array()), + 32 => MultiAddress::Id(AccountId::from(step.recipient.to_array())), + _ => return Err("InvalidRecipient"), + }; + let value = Compact(step.spend_amount.ok_or("MissingSpendAmount")?); + + Ok(Call { + params: CallParams::Sub(SubCall { + calldata: SubExtrinsic { + pallet_id: self.pallet_id, + call_id: self.call_id, + call: (dest, value), + } + .encode(), + }), + input_call: None, + call_index: None, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn transfer_on_phala() { + let transactor = Transactor::new(0x28, 0x07); + let call = transactor + .build_call(Step { + exe: String::from(""), + source_chain: String::from("Phala"), + dest_chain: String::from("Phala"), + spend_asset: hex::decode("0000").unwrap(), + receive_asset: hex::decode("0000").unwrap(), + sender: None, + recipient: hex::decode( + "04dba0677fc274ffaccc0fa1030a66b171d1da9226d2bb9d152654e6a746f276", + ) + .unwrap(), + // 2 PHA + spend_amount: Some(2_000_000_000_000 as u128), + origin_balance: None, + nonce: None, + }) + .unwrap(); + + match &call.params { + CallParams::Sub(sub_call) => { + println!("calldata: {:?}", hex::encode(&sub_call.calldata)) + } + _ => assert!(false), + } + } +} diff --git a/contracts/index_executor/src/actions/base/uniswapv2.rs b/contracts/index_executor/src/actions/base/uniswapv2.rs index 9b886670..4aaf6706 100644 --- a/contracts/index_executor/src/actions/base/uniswapv2.rs +++ b/contracts/index_executor/src/actions/base/uniswapv2.rs @@ -1,4 +1,4 @@ -use alloc::{vec, vec::Vec}; +use alloc::vec; use pink_web3::{ api::{Eth, Namespace}, contract::{tokens::Tokenize, Contract}, @@ -31,10 +31,10 @@ impl UniswapV2 { } impl CallBuilder for UniswapV2 { - fn build_call(&self, step: Step) -> Result, &'static str> { + fn build_call(&self, step: Step) -> Result { let asset0 = Address::from_slice(&step.spend_asset); let asset1 = Address::from_slice(&step.receive_asset); - let to = Address::from_slice(&step.recipient.ok_or("MissingRecipient")?); + let to = Address::from_slice(&step.recipient); let path = vec![asset0, asset1]; let amount_out = U256::from(1); let amount_in = U256::from(step.spend_amount.ok_or("MissingSpendAmount")?); @@ -51,54 +51,22 @@ impl CallBuilder for UniswapV2 { .encode_input(&swap_params.into_tokens()) .map_err(|_| "EncodeParamError")?; - let token = Contract::from_json( - self.eth.clone(), - asset0, - include_bytes!("../../abi/erc20.json"), - ) - .expect("Bad abi data"); - let approve_params = (self.router.address(), amount_in); - let approve_func = token - .abi() - .function("approve") - .map_err(|_| "NoFunctionFound")?; - let approve_calldata = approve_func - .encode_input(&approve_params.into_tokens()) - .map_err(|_| "EncodeParamError")?; - - Ok(vec![ - Call { - params: CallParams::Evm(EvmCall { - target: asset0, - calldata: approve_calldata, - value: U256::from(0), - - need_settle: false, - update_offset: U256::from(36), - update_len: U256::from(32), - spend_asset: asset0, - spend_amount: amount_in, - receive_asset: asset0, - }), - input_call: None, - call_index: None, - }, - Call { - params: CallParams::Evm(EvmCall { - target: self.router.address(), - calldata: swap_calldata, - value: U256::from(0), + Ok(Call { + params: CallParams::Evm(EvmCall { + target: self.router.address(), + calldata: swap_calldata, + value: U256::from(0), - need_settle: true, - update_offset: U256::from(4), - update_len: U256::from(32), - spend_asset: asset0, - spend_amount: amount_in, - receive_asset: asset1, - }), - input_call: None, - call_index: None, - }, - ]) + need_settle: true, + update_offset: U256::from(4), + update_len: U256::from(32), + spender: self.router.address(), + spend_asset: asset0, + spend_amount: amount_in, + receive_asset: asset1, + }), + input_call: None, + call_index: None, + }) } } diff --git a/contracts/index_executor/src/actions/base/uniswapv3.rs b/contracts/index_executor/src/actions/base/uniswapv3.rs new file mode 100644 index 00000000..5825a7e8 --- /dev/null +++ b/contracts/index_executor/src/actions/base/uniswapv3.rs @@ -0,0 +1,167 @@ +use pink_web3::{ + api::{Eth, Namespace}, + contract::{tokens::Tokenize, Contract}, + ethabi::{Address, Token}, + transports::PinkHttp, + types::U256, +}; + +use crate::call::{Call, CallBuilder, CallParams, EvmCall}; +use crate::step::Step; + +#[derive(Clone)] +pub struct UniswapV3 { + pub eth: Eth, + pub router: Contract, +} + +impl UniswapV3 { + pub fn new(rpc: &str, router: Address) -> Self { + let eth = Eth::new(PinkHttp::new(rpc)); + let router = Contract::from_json( + eth.clone(), + router, + include_bytes!("../../abi/uniswapV3Router.json"), + ) + .expect("Bad abi data"); + + Self { eth, router } + } +} + +impl CallBuilder for UniswapV3 { + fn build_call(&self, step: Step) -> Result { + let asset0 = Address::from_slice(&step.spend_asset); + let asset1 = Address::from_slice(&step.receive_asset); + let to = Address::from_slice(&step.recipient); + let amount_out = U256::from(1); + let amount_in = U256::from(step.spend_amount.ok_or("MissingSpendAmount")?); + let time = pink_extension::ext().untrusted_millis_since_unix_epoch() / 1000; + // 1 month + let deadline = U256::from(time + 60 * 60 * 24 * 30); + let swap_params = (asset0, asset1, to, deadline, amount_in, amount_out, 0_u128); + // https://github.com/Uniswap/v3-periphery/blob/6cce88e63e176af1ddb6cc56e029110289622317/contracts/SwapRouter.sol#L115 + let swap_func = self + .router + .abi() + .function("exactInputSingle") + .map_err(|_| "NoFunctionFound")?; + let swap_calldata = swap_func + .encode_input(&[Token::Tuple(swap_params.into_tokens())]) + .map_err(|_| "EncodeParamError")?; + + Ok(Call { + params: CallParams::Evm(EvmCall { + target: self.router.address(), + calldata: swap_calldata, + value: U256::from(0), + + need_settle: true, + update_offset: U256::from(132), + update_len: U256::from(32), + spender: self.router.address(), + spend_asset: asset0, + spend_amount: amount_in, + receive_asset: asset1, + }), + input_call: None, + call_index: None, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::ToArray; + use dotenv::dotenv; + use primitive_types::H160; + + use pink_web3::{ + api::{Eth, Namespace}, + contract::{Contract, Options}, + keys::pink::KeyPair, + transports::{resolve_ready, PinkHttp}, + }; + + #[test] + #[ignore] + fn should_work() { + dotenv().ok(); + pink_extension_runtime::mock_ext::mock_all_ext(); + + use pink_web3::types::Address; + + let secret_key = std::env::vars().find(|x| x.0 == "SECRET_KEY"); + let secret_key = secret_key.unwrap().1; + let secret_bytes = hex::decode(secret_key).unwrap(); + let signer: [u8; 32] = secret_bytes.to_array(); + + // Handler on Moonbeam + let handler_address: H160 = + H160::from_slice(&hex::decode("635eA86804200F80C16ea8EdDc3c749a54a9C37D").unwrap()); + let transport = Eth::new(PinkHttp::new("https://rpc.api.moonbeam.network")); + let handler = Contract::from_json( + transport, + handler_address, + include_bytes!("../../abi/handler.json"), + ) + .unwrap(); + let stellaswap_routerv3: [u8; 20] = hex::decode("e6d0ED3759709b743707DcfeCAe39BC180C981fe") + .unwrap() + .to_array(); + let stellaswap_v3 = UniswapV3::new( + "https://rpc.api.moonbeam.network", + stellaswap_routerv3.into(), + ); + let mut call = stellaswap_v3 + .build_call(Step { + exe: String::from(""), + source_chain: String::from("Moonbeam"), + dest_chain: String::from("Moonbeam"), + // xcDOT + spend_asset: hex::decode("FfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080").unwrap(), + // xcPHA + receive_asset: hex::decode("FFFfFfFf63d24eCc8eB8a7b5D0803e900F7b6cED").unwrap(), + sender: None, + recipient: hex::decode("A29D4E0F035cb50C0d78c8CeBb56Ca292616Ab20").unwrap(), + // 0.002 xcDOT + spend_amount: Some(2_0_000_000 as u128), + origin_balance: None, + nonce: None, + }) + .unwrap(); + + // Apply index mannually + call.input_call = Some(0); + call.call_index = Some(0); + + // Estiamte gas before submission + let gas = resolve_ready(handler.estimate_gas( + "batchCall", + vec![call.clone()], + Address::from_slice(&hex::decode("bf526928373748b00763875448ee905367d97f96").unwrap()), + Options::default(), + )) + .map_err(|e| { + println!("Failed to estimated step gas cost with error: {:?}", e); + "FailedToEstimateGas" + }) + .unwrap(); + + // Uncomment if wanna send it to blockchain + let _tx_id = resolve_ready(handler.signed_call( + "batchCall", + vec![call], + Options::with(|opt| { + opt.gas = Some(gas); + }), + KeyPair::from(signer), + )) + .map_err(|e| { + println!("Failed to submit step execution tx with error: {:?}", e); + "FailedToSubmitTransaction" + }) + .unwrap(); + } +} diff --git a/contracts/index_executor/src/actions/ethereum/mod.rs b/contracts/index_executor/src/actions/ethereum/mod.rs index 5cd379ee..07e5f99f 100644 --- a/contracts/index_executor/src/actions/ethereum/mod.rs +++ b/contracts/index_executor/src/actions/ethereum/mod.rs @@ -1,18 +1,127 @@ -use crate::actions::base::uniswapv2; +use crate::actions::base::{native_wrapper, uniswapv2, uniswapv3}; +pub mod sygma; + use alloc::{boxed::Box, string::String, vec, vec::Vec}; +use sp_runtime::Permill; pub type EthereumUniswapV2 = uniswapv2::UniswapV2; +pub type EthereumUniswapV3 = uniswapv3::UniswapV3; +pub type EthereumNativeWrapper = native_wrapper::NativeWrapper; +use crate::actions::ActionExtraInfo; use crate::call::CallBuilder; use crate::chain::Chain; +use crate::constants::{ETHEREUM_BLOCK_TIME, PARACHAIN_BLOCK_TIME}; +use crate::utils::ToArray; +use core::str::FromStr; +use pink_web3::ethabi::Address; pub fn create_actions(chain: &Chain) -> Vec<(String, Box)> { - let uniswapv2_router: [u8; 20] = hex_literal::hex!("7a250d5630B4cF539739dF2C5dAcb4c659F2488D"); + let uniswapv2_router: [u8; 20] = hex::decode("7a250d5630B4cF539739dF2C5dAcb4c659F2488D") + .unwrap() + .to_array(); + let uniswapv3_router: [u8; 20] = hex::decode("E592427A0AEce92De3Edee1F18E0157C05861564") + .unwrap() + .to_array(); + let ethereum_weth: [u8; 20] = hex_literal::hex!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); + let ethereum_eth: [u8; 20] = hex_literal::hex!("0000000000000000000000000000000000000000"); + + vec![ + ( + String::from("ethereum_nativewrapper"), + Box::new(EthereumNativeWrapper::new( + &chain.endpoint, + ethereum_weth.into(), + ethereum_eth.into(), + )), + ), + ( + String::from("ethereum_uniswapv2"), + Box::new(EthereumUniswapV2::new( + &chain.endpoint, + uniswapv2_router.into(), + )), + ), + ( + String::from("ethereum_uniswapv3"), + Box::new(EthereumUniswapV3::new( + &chain.endpoint, + uniswapv3_router.into(), + )), + ), + ( + String::from("ethereum_sygmabridge_to_phala"), + Box::new(sygma::EvmSygmaBridge::new( + &chain.endpoint, + Address::from_str("4D878E8Fb90178588Cda4cf1DCcdC9a6d2757089").unwrap(), + Address::from_str("C832588193cd5ED2185daDA4A531e0B26eC5B830").unwrap(), + Address::from_str("e43F8245249d7fAF46408723Ab36D071dD85D7BB").unwrap(), + // 0.001 ETH + 100_000_000_000_000u128, + 1, + 3, + None, + )), + ), + ( + String::from("ethereum_sygmabridge_to_khala"), + Box::new(sygma::EvmSygmaBridge::new( + &chain.endpoint, + Address::from_str("4D878E8Fb90178588Cda4cf1DCcdC9a6d2757089").unwrap(), + Address::from_str("C832588193cd5ED2185daDA4A531e0B26eC5B830").unwrap(), + Address::from_str("e43F8245249d7fAF46408723Ab36D071dD85D7BB").unwrap(), + // 0.001 ETH + 100_000_000_000_000u128, + 1, + 2, + None, + )), + ), + ] +} - vec![( - String::from("ethereum_uniswapv2"), - Box::new(EthereumUniswapV2::new( - &chain.endpoint, - uniswapv2_router.into(), - )), - )] +#[allow(clippy::if_same_then_else)] +pub fn get_extra_info(chain: &str, action: &str) -> Option { + assert!(chain == "Ethereum"); + if action == "ethereum_nativewrapper" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + const_proto_fee_in_usd: 0, + percentage_proto_fee: Permill::zero(), + confirm_time_in_sec: ETHEREUM_BLOCK_TIME, + }) + } else if action == "ethereum_uniswapv2" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + const_proto_fee_in_usd: 0, + percentage_proto_fee: Permill::from_perthousand(3), + confirm_time_in_sec: ETHEREUM_BLOCK_TIME, + }) + } else if action == "ethereum_uniswapv3" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + const_proto_fee_in_usd: 0, + percentage_proto_fee: Permill::zero(), + confirm_time_in_sec: ETHEREUM_BLOCK_TIME, + }) + } else if action == "ethereum_sygmabridge_to_phala" { + Some(ActionExtraInfo { + // 0.2 USD + extra_proto_fee_in_usd: 2000, + const_proto_fee_in_usd: 0, + percentage_proto_fee: Permill::zero(), + // Sygma relayer wait 15 blocks to forward and 1 block on Phala to confirm + confirm_time_in_sec: ETHEREUM_BLOCK_TIME * 15 + PARACHAIN_BLOCK_TIME, + }) + } else if action == "ethereum_sygmabridge_to_khala" { + Some(ActionExtraInfo { + // 0.2 USD + extra_proto_fee_in_usd: 2000, + const_proto_fee_in_usd: 0, + percentage_proto_fee: Permill::zero(), + // Sygma relayer wait 15 blocks to forward and 1 block on Khala to confirm + confirm_time_in_sec: ETHEREUM_BLOCK_TIME * 15 + PARACHAIN_BLOCK_TIME, + }) + } else { + None + } } diff --git a/contracts/index_executor/src/actions/ethereum/sygma.rs b/contracts/index_executor/src/actions/ethereum/sygma.rs new file mode 100644 index 00000000..c66642e1 --- /dev/null +++ b/contracts/index_executor/src/actions/ethereum/sygma.rs @@ -0,0 +1,434 @@ +use alloc::{collections::BTreeMap, vec, vec::Vec}; +use core::str::FromStr; +use hex_literal::hex; +use pink_web3::{ + api::{Eth, Namespace}, + contract::{tokens::Tokenize, Contract, Options}, + ethabi::{Address, Uint}, + transports::{resolve_ready, PinkHttp}, + types::U256, +}; +use scale::Encode; +use xcm::v3::{ + Junction::{AccountId32, Parachain}, + Junctions::{X1, X2}, + MultiLocation, +}; + +use crate::step::Step; +use crate::{ + call::{Call, CallBuilder, CallParams, EvmCall}, + utils::ToArray, +}; + +#[derive(Clone)] +pub struct EvmSygmaBridge { + eth: Eth, + contract: Contract, + erc20_handler_address: Address, + fee_handler_address: Address, + fee_amount: u128, + from_domain_id: u8, + to_domain_id: u8, + maybe_parachain_id: Option, + resource_id_map: BTreeMap, +} + +#[allow(clippy::too_many_arguments)] +impl EvmSygmaBridge { + pub fn new( + rpc: &str, + contract_address: Address, + erc20_handler_address: Address, + fee_handler_address: Address, + fee_amount: u128, + from_domain_id: u8, + to_domain_id: u8, + maybe_parachain_id: Option, + ) -> Self { + let eth = Eth::new(PinkHttp::new(rpc)); + let contract = Contract::from_json( + eth.clone(), + contract_address, + include_bytes!("../../abi/SygmaBridge.json"), + ) + .expect("Bad abi data"); + + let mut resource_id_map = BTreeMap::new(); + // Goerli GPHA + resource_id_map.insert( + Address::from_str("B376b0Ee6d8202721838e76376e81eEc0e2FE864").unwrap(), + hex!("0000000000000000000000000000000000000000000000000000000000001000"), + ); + // Ethereum PHA + resource_id_map.insert( + Address::from_str("6c5bA91642F10282b576d91922Ae6448C9d52f4E").unwrap(), + hex!("0000000000000000000000000000000000000000000000000000000000000001"), + ); + + Self { + eth, + contract, + erc20_handler_address, + fee_handler_address, + fee_amount, + from_domain_id, + to_domain_id, + maybe_parachain_id, + resource_id_map, + } + } +} + +impl CallBuilder for EvmSygmaBridge { + fn build_call(&self, step: Step) -> Result { + let sender = Address::from_slice(&step.sender.ok_or("MissingSender")?); + let spend_asset = Address::from_slice(&step.spend_asset); + let resource_id = *self + .resource_id_map + .get(&spend_asset) + .ok_or("NoResourceId")?; + let spend_amount = U256::from(step.spend_amount.ok_or("MissingSpendAmount")?); + let mut recipient = step.recipient; + if recipient.len() == 32 { + let account_id = AccountId32 { + network: None, + id: recipient.to_array(), + }; + let rec = match self.maybe_parachain_id { + Some(parachain_id) => { + MultiLocation::new(1, X2(Parachain(parachain_id), account_id)) + } + None => MultiLocation::new(0, X1(account_id)), + }; + recipient = rec.encode() + } + let mut deposit_data: Vec = vec![]; + let token_stats: [u8; 32] = spend_amount.into(); + deposit_data.extend(token_stats); + deposit_data.extend_from_slice(&{ + let mut res = Vec::new(); + for b in U256::from(recipient.len()).0.iter().rev() { + let bytes = b.to_be_bytes(); + res.extend(bytes); + } + res + }); + deposit_data.extend(recipient); + + let fee_handler = Contract::from_json( + self.eth.clone(), + self.fee_handler_address, + include_bytes!("../../abi/SygmaBasicFeeHandler.json"), + ) + .expect("Bad abi data"); + + let fee: (Uint, Address) = resolve_ready(fee_handler.query( + "calculateFee", + ( + sender, + self.from_domain_id, + self.to_domain_id, + resource_id, + hex!("").to_vec(), + hex!("").to_vec(), + ), + None, + Options::default(), + None, + )) + .unwrap(); + + let mut fee_data = vec![0u8; 32]; + fee.0.to_big_endian(&mut fee_data); + let nonzero_index = fee_data + .iter() + .position(|&x| x != 0) + .unwrap_or(fee_data.len() - 1); + let fee_data: Vec = fee_data[nonzero_index..].to_vec(); + + let bridge_params = (self.to_domain_id, resource_id, deposit_data, fee_data); + + let bridge_func = self + .contract + .abi() + .function("deposit") + .map_err(|_| "NoFunctionFound")?; + let bridge_calldata = bridge_func + .encode_input(&bridge_params.into_tokens()) + .map_err(|_| "EncodeParamError")?; + + Ok(Call { + params: CallParams::Evm(EvmCall { + target: self.contract.address(), + calldata: bridge_calldata, + value: U256::from(self.fee_amount), + spender: self.erc20_handler_address, + need_settle: false, + update_offset: U256::from(164), + update_len: U256::from(32), + spend_asset, + spend_amount, + receive_asset: spend_asset, + }), + input_call: None, + call_index: None, + }) + } +} + +#[cfg(test)] +mod tests { + use crate::utils::ToArray; + + use super::*; + use pink_web3::keys::pink::KeyPair; + use pink_web3::types::H160; + + #[test] + #[ignore] + fn test_pha_from_goerli_to_rhala() { + pink_extension_runtime::mock_ext::mock_all_ext(); + + let rpc = "https://rpc.ankr.com/eth_goerli"; + + // Handler on Goerli + let handler_address: H160 = + H160::from_slice(&hex::decode("0B674CC89F54a47Be4Eb6C1A125bB8f04A529181").unwrap()); + let transport = Eth::new(PinkHttp::new(rpc.clone())); + let handler = Contract::from_json( + transport, + handler_address, + include_bytes!("../../abi/handler.json"), + ) + .unwrap(); + let sygma_bridge = EvmSygmaBridge::new( + rpc, + Address::from_str("c26335a9f16398b5fDA4bC05b62C1429D8a4d755").unwrap(), + Address::from_str("7Ed4B14a82B2F2C4DfB13DC4Eac00205EDEff6C2").unwrap(), + Address::from_str("e6CE0ea4eC6ECbdC23eEF9f4fB165aCc979C56b5").unwrap(), + // 0.001 ETH on Goerli + 1_000_000_000_000_000u128, + 1, + 3, + None, + ); + + let mut call = sygma_bridge + .build_call(Step { + exe: String::from(""), + source_chain: String::from("Goerli"), + dest_chain: String::from("Rhala"), + spend_asset: hex::decode("B376b0Ee6d8202721838e76376e81eEc0e2FE864").unwrap(), + // MuliLocation: (0, Here) + receive_asset: hex::decode("0000").unwrap(), + sender: Some(hex::decode("53e4C6611D3C92232bCBdd20D1073ce892D34594").unwrap()), + recipient: hex::decode( + "04dba0677fc274ffaccc0fa1030a66b171d1da9226d2bb9d152654e6a746f276", + ) + .unwrap(), + spend_amount: Some(1_000_000_000_000_000_000 as u128), + origin_balance: None, + nonce: None, + }) + .unwrap(); + + // Apply index mannually + call.input_call = Some(0); + call.call_index = Some(0); + + // Make sure handler address hold enough spend asset and native asset (e.g. ETH). + // Because handler is the account who spend and pay fee on behalf + + // Estiamte gas before submission + let gas = resolve_ready(handler.estimate_gas( + "batchCall", + vec![call.clone()], + // Worker address + Address::from_slice(&hex::decode("bf526928373748b00763875448ee905367d97f96").unwrap()), + Options::with(|opt| { + // 0.001 ETH + opt.value = Some(U256::from(1_000_000_000_000_000u128)) + }), + )) + .map_err(|e| { + println!("Failed to estimated step gas cost with error: {:?}", e); + "FailedToEstimateGas" + }) + .unwrap(); + + // Tested on Georli: + // Goerli: https://goerli.etherscan.io/tx/0xe64402af2a358c155a15d26bd547ab6beb51790a4ffb3f0eee248b5b67e09dab + // Rhala: + + let secret_key = std::env::vars().find(|x| x.0 == "SECRET_KEY"); + let secret_key = secret_key.unwrap().1; + let secret_bytes = hex::decode(secret_key).unwrap(); + let signer: [u8; 32] = secret_bytes.to_array(); + + let _tx_id: primitive_types::H256 = resolve_ready(handler.signed_call( + "batchCall", + vec![call], + Options::with(|opt| { + opt.gas = Some(gas); + // 0.001 ETH + opt.value = Some(U256::from(1_000_000_000_000_000u128)) + }), + KeyPair::from(signer), + )) + .map_err(|e| { + println!("Failed to submit step execution tx with error: {:?}", e); + "FailedToSubmitTransaction" + }) + .unwrap(); + } + + // cargo test --package index_executor --lib -- actions::ethereum::sygma::tests::test_batch_call_on_ethereum --exact --nocapture + #[test] + #[ignore] + fn test_batch_call_on_ethereum() { + pink_extension_runtime::mock_ext::mock_all_ext(); + + let rpc = "https://mainnet.infura.io/v3/e5f4c95222934613bbde028ba5dc526b"; + + // Handler on Ethereum + let handler_address: H160 = + H160::from_slice(&hex::decode("d693bDC5cb0cF2a31F08744A0Ec135a68C26FE1c").unwrap()); + let transport = Eth::new(PinkHttp::new(rpc.clone())); + let handler = Contract::from_json( + transport, + handler_address, + include_bytes!("../../abi/handler.json"), + ) + .unwrap(); + + let wrap_call = Call { + params: CallParams::Evm(EvmCall { + target: hex::decode("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") + .unwrap() + .to_array() + .into(), + calldata: vec![208, 227, 13, 176], + value: U256::from(300000000000000_u128), + need_settle: true, + update_offset: U256::from(0), + update_len: U256::from(0), + spender: hex::decode("0000000000000000000000000000000000000000") + .unwrap() + .to_array() + .into(), + spend_asset: hex::decode("0000000000000000000000000000000000000000") + .unwrap() + .to_array() + .into(), + spend_amount: U256::from(300000000000000_u128), + receive_asset: hex::decode("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") + .unwrap() + .to_array() + .into(), + }), + input_call: Some(0), + call_index: Some(0), + }; + + let swap_call = Call { + params: CallParams::Evm(EvmCall { + target: hex::decode("7a250d5630b4cf539739df2c5dacb4c659f2488d") + .unwrap() + .to_array() + .into(), + calldata: vec![ + 56, 237, 23, 57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 160, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 214, 147, 189, 197, 203, 12, 242, 163, 31, 8, 116, + 74, 14, 193, 53, 166, 140, 38, 254, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 49, 28, 40, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 42, 170, 57, 178, 35, 254, 141, 10, + 14, 92, 79, 39, 234, 217, 8, 60, 117, 108, 194, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 108, 91, 169, 22, 66, 241, 2, 130, 181, 118, 217, 25, 34, 174, 100, 72, + 201, 213, 47, 78, + ], + value: U256::from(0), + need_settle: true, + update_offset: U256::from(4), + update_len: U256::from(32), + spender: hex::decode("7a250d5630b4cf539739df2c5dacb4c659f2488d") + .unwrap() + .to_array() + .into(), + spend_asset: hex::decode("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") + .unwrap() + .to_array() + .into(), + spend_amount: U256::from(0), + receive_asset: hex::decode("6c5ba91642f10282b576d91922ae6448c9d52f4e") + .unwrap() + .to_array() + .into(), + }), + input_call: Some(0), + call_index: Some(1), + }; + + let bridge_call = Call { + params: CallParams::Evm(EvmCall { + target: hex::decode("4d878e8fb90178588cda4cf1dccdc9a6d2757089") + .unwrap() + .to_array() + .into(), + calldata: vec![ + 115, 196, 92, 152, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 0, 1, 1, 0, + 4, 219, 160, 103, 127, 194, 116, 255, 172, 204, 15, 161, 3, 10, 102, 177, 113, + 209, 218, 146, 38, 210, 187, 157, 21, 38, 84, 230, 167, 70, 242, 118, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 6, 90, 243, 16, 122, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + value: U256::from(100000000000000_u128), + need_settle: false, + update_offset: U256::from(164), + update_len: U256::from(32), + spender: hex::decode("c832588193cd5ed2185dada4a531e0b26ec5b830") + .unwrap() + .to_array() + .into(), + spend_asset: hex::decode("6c5ba91642f10282b576d91922ae6448c9d52f4e") + .unwrap() + .to_array() + .into(), + spend_amount: U256::from(0), + receive_asset: hex::decode("6c5ba91642f10282b576d91922ae6448c9d52f4e") + .unwrap() + .to_array() + .into(), + }), + input_call: Some(1), + call_index: Some(2), + }; + + let _gas = resolve_ready(handler.estimate_gas( + "batchCall", + vec![wrap_call, swap_call, bridge_call], + // Worker address + Address::from_slice(&hex::decode("bf526928373748b00763875448ee905367d97f96").unwrap()), + Options::default(), + )) + .map_err(|e| { + println!("Failed to estimated step gas cost with error: {:?}", e); + "FailedToEstimateGas" + }) + .unwrap(); + } +} diff --git a/contracts/index_executor/src/actions/mod.rs b/contracts/index_executor/src/actions/mod.rs index 7af14845..9e2dd733 100644 --- a/contracts/index_executor/src/actions/mod.rs +++ b/contracts/index_executor/src/actions/mod.rs @@ -1,6 +1,26 @@ +use sp_runtime::Permill; pub mod acala; pub mod astar; pub mod base; pub mod ethereum; pub mod moonbeam; pub mod phala; +pub mod polkadot; + +#[derive(Clone, Debug, Default, scale::Decode, scale::Encode, Eq, PartialEq, Ord, PartialOrd)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct ActionExtraInfo { + // The fee is a constant amount that will NOT deducted from user spend asset or receive asset + // That means our worker will pay for this during execution, so it should be treat like tx fee + // that should be deducted from user spend separately + // Represent USD: extra_proto_fee_in_usd / 10000 + // We can potentially use Fixed crates here + pub extra_proto_fee_in_usd: u32, + // The fee is a constant amount that will deducted from user spend asset or receive asset + // Represent USD: const_proto_fee_in_usd / 10000 + // We can potentially use Fixed crates here + pub const_proto_fee_in_usd: u32, + // The fee that calculated by a percentage scale, will deducted from user spend asset or receive asset + pub percentage_proto_fee: Permill, + pub confirm_time_in_sec: u16, +} diff --git a/contracts/index_executor/src/actions/moonbeam/mod.rs b/contracts/index_executor/src/actions/moonbeam/mod.rs index 2ef43342..7d722eab 100644 --- a/contracts/index_executor/src/actions/moonbeam/mod.rs +++ b/contracts/index_executor/src/actions/moonbeam/mod.rs @@ -1,24 +1,41 @@ -use crate::actions::base::uniswapv2; +use crate::actions::base::{native_wrapper, uniswapv3}; pub mod xtoken; -pub type MoonbeamStellaSwap = uniswapv2::UniswapV2; +pub type MoonbeamStellaSwap = uniswapv3::UniswapV3; +pub type MoonbeamNativeWrapper = native_wrapper::NativeWrapper; +use crate::actions::ActionExtraInfo; use crate::call::CallBuilder; use crate::chain::Chain; +use crate::constants::PARACHAIN_BLOCK_TIME; use crate::constants::*; +use crate::utils::ToArray; use alloc::{boxed::Box, string::String, vec, vec::Vec}; +use sp_runtime::Permill; pub fn create_actions(chain: &Chain) -> Vec<(String, Box)> { - let stellaswap_router: [u8; 20] = hex_literal::hex!("70085a09D30D6f8C4ecF6eE10120d1847383BB57"); + let stellaswap_routerv3: [u8; 20] = hex::decode("e6d0ED3759709b743707DcfeCAe39BC180C981fe") + .unwrap() + .to_array(); let moonbeam_xtoken: [u8; 20] = hex_literal::hex!("0000000000000000000000000000000000000804"); + let moonbeam_wglmr: [u8; 20] = hex_literal::hex!("Acc15dC74880C9944775448304B263D191c6077F"); + let moonbeam_glmr: [u8; 20] = hex_literal::hex!("0000000000000000000000000000000000000802"); vec![ + ( + String::from("moonbeam_nativewrapper"), + Box::new(MoonbeamNativeWrapper::new( + &chain.endpoint, + moonbeam_wglmr.into(), + moonbeam_glmr.into(), + )), + ), ( String::from("moonbeam_stellaswap"), Box::new(MoonbeamStellaSwap::new( &chain.endpoint, - stellaswap_router.into(), + stellaswap_routerv3.into(), )), ), ( @@ -26,7 +43,7 @@ pub fn create_actions(chain: &Chain) -> Vec<(String, Box)> { Box::new(xtoken::XTokenBridge::new( &chain.endpoint, moonbeam_xtoken.into(), - ACALA_PARACHAIN_ID, + xtoken::XTokenDestChain::Parachain(ACALA_PARACHAIN_ID), )), ), ( @@ -34,7 +51,7 @@ pub fn create_actions(chain: &Chain) -> Vec<(String, Box)> { Box::new(xtoken::XTokenBridge::new( &chain.endpoint, moonbeam_xtoken.into(), - ASTAR_PARACHAIN_ID, + xtoken::XTokenDestChain::Parachain(ASTAR_PARACHAIN_ID), )), ), ( @@ -42,8 +59,70 @@ pub fn create_actions(chain: &Chain) -> Vec<(String, Box)> { Box::new(xtoken::XTokenBridge::new( &chain.endpoint, moonbeam_xtoken.into(), - PHALA_PARACHAIN_ID, + xtoken::XTokenDestChain::Parachain(PHALA_PARACHAIN_ID), + )), + ), + ( + String::from("moonbeam_bridge_to_polkadot"), + Box::new(xtoken::XTokenBridge::new( + &chain.endpoint, + moonbeam_xtoken.into(), + xtoken::XTokenDestChain::Relaychain, )), ), ] } + +#[allow(clippy::if_same_then_else)] +pub fn get_extra_info(chain: &str, action: &str) -> Option { + assert!(chain == "Moonbeam"); + if action == "moonbeam_nativewrapper" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + const_proto_fee_in_usd: 0, + percentage_proto_fee: Permill::zero(), + confirm_time_in_sec: PARACHAIN_BLOCK_TIME, + }) + } else if action == "moonbeam_stellaswap" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + const_proto_fee_in_usd: 0, + percentage_proto_fee: Permill::from_perthousand(3), + confirm_time_in_sec: PARACHAIN_BLOCK_TIME, + }) + } else if action == "moonbeam_bridge_to_acala" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + // 0.0005 USD + const_proto_fee_in_usd: 5, + percentage_proto_fee: Permill::zero(), + confirm_time_in_sec: PARACHAIN_BLOCK_TIME * 2, + }) + } else if action == "moonbeam_bridge_to_astar" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + // 0.0005 USD + const_proto_fee_in_usd: 5, + percentage_proto_fee: Permill::zero(), + confirm_time_in_sec: PARACHAIN_BLOCK_TIME * 2, + }) + } else if action == "moonbeam_bridge_to_phala" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + // 0.0005 USD + const_proto_fee_in_usd: 5, + percentage_proto_fee: Permill::zero(), + confirm_time_in_sec: PARACHAIN_BLOCK_TIME * 2, + }) + } else if action == "moonbeam_bridge_to_polkadot" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + // 0.0005 USD + const_proto_fee_in_usd: 5, + percentage_proto_fee: Permill::zero(), + confirm_time_in_sec: PARACHAIN_BLOCK_TIME * 2, + }) + } else { + None + } +} diff --git a/contracts/index_executor/src/actions/moonbeam/xtoken.rs b/contracts/index_executor/src/actions/moonbeam/xtoken.rs index 3e950e61..9d9adde1 100644 --- a/contracts/index_executor/src/actions/moonbeam/xtoken.rs +++ b/contracts/index_executor/src/actions/moonbeam/xtoken.rs @@ -10,15 +10,21 @@ use pink_web3::{ use crate::call::{Call, CallBuilder, CallParams, EvmCall}; use crate::step::Step; +#[derive(Clone)] +pub enum XTokenDestChain { + Relaychain, + Parachain(u32), +} + #[derive(Clone)] pub struct XTokenBridge { - eth: Eth, + _eth: Eth, xtoken: Contract, - dest_chain_id: u32, + dest_chain: XTokenDestChain, } impl XTokenBridge { - pub fn new(rpc: &str, xtoken_address: Address, dest_chain_id: u32) -> Self { + pub fn new(rpc: &str, xtoken_address: Address, dest_chain: XTokenDestChain) -> Self { let eth = Eth::new(PinkHttp::new(rpc)); let xtoken = Contract::from_json( eth.clone(), @@ -28,52 +34,72 @@ impl XTokenBridge { .expect("Bad abi data"); Self { - eth, + _eth: eth, xtoken, - dest_chain_id, + dest_chain, } } } impl CallBuilder for XTokenBridge { - fn build_call(&self, step: Step) -> Result, &'static str> { + fn build_call(&self, step: Step) -> Result { let spend_asset = Address::from_slice(&step.spend_asset); // We don't use it let receive_asset = Address::from_slice(&[0; 20]); - let mut recipient = step.recipient.ok_or("MissingRecipient")?; + let mut recipient = step.recipient; let spend_amount = U256::from(step.spend_amount.ok_or("MissingSpendAmount")?); let weight: u64 = 6000000000; - let location = Token::Tuple(vec![ - Token::Uint(1_u8.into()), - Token::Array(vec![ - Token::Bytes( - // Parachain(#[codec(compact)] u32), - { - let mut bytes: Vec = vec![]; - let mut enum_id = 0_u8.to_be_bytes().to_vec(); - let mut chain_id = self.dest_chain_id.to_be_bytes().to_vec(); - bytes.append(&mut enum_id); - bytes.append(&mut chain_id); - bytes - }, - ), - Token::Bytes( - // AccountId32 { network: NetworkId, id: [u8; 32] }, - { - let mut bytes: Vec = vec![]; - let mut enum_id = 1_u8.to_be_bytes().to_vec(); - let mut network_vec = 0_u8.to_be_bytes().to_vec(); - bytes.append(&mut enum_id); - bytes.append(&mut recipient); - bytes.append(&mut network_vec); - bytes - }, - ), - ]), - ]); + let location = match self.dest_chain { + XTokenDestChain::Relaychain => { + Token::Tuple(vec![ + Token::Uint(1_u8.into()), + Token::Array(vec![Token::Bytes( + // AccountId32 { network: NetworkId, id: [u8; 32] }, + { + let mut bytes: Vec = vec![]; + let mut enum_id = 1_u8.to_be_bytes().to_vec(); + let mut network_vec = 0_u8.to_be_bytes().to_vec(); + bytes.append(&mut enum_id); + bytes.append(&mut recipient); + bytes.append(&mut network_vec); + bytes + }, + )]), + ]) + } + XTokenDestChain::Parachain(parachain_id) => { + Token::Tuple(vec![ + Token::Uint(1_u8.into()), + Token::Array(vec![ + Token::Bytes( + // Parachain(#[codec(compact)] u32), + { + let mut bytes: Vec = vec![]; + let mut enum_id = 0_u8.to_be_bytes().to_vec(); + let mut chain_id = parachain_id.to_be_bytes().to_vec(); + bytes.append(&mut enum_id); + bytes.append(&mut chain_id); + bytes + }, + ), + Token::Bytes( + // AccountId32 { network: NetworkId, id: [u8; 32] }, + { + let mut bytes: Vec = vec![]; + let mut enum_id = 1_u8.to_be_bytes().to_vec(); + let mut network_vec = 0_u8.to_be_bytes().to_vec(); + bytes.append(&mut enum_id); + bytes.append(&mut recipient); + bytes.append(&mut network_vec); + bytes + }, + ), + ]), + ]) + } + }; let bridge_params = (spend_asset, spend_amount, location, weight); - let bridge_func = self .xtoken .abi() @@ -83,56 +109,118 @@ impl CallBuilder for XTokenBridge { .encode_input(&bridge_params.into_tokens()) .map_err(|_| "EncodeParamError")?; - let token = Contract::from_json( - self.eth.clone(), - spend_asset, - include_bytes!("../../abi/erc20.json"), + Ok(Call { + params: CallParams::Evm(EvmCall { + target: self.xtoken.address(), + calldata: bridge_calldata, + value: U256::from(0), + + // Bridge operation do not need do settlement on source chain, because it must be the + // last step on source chain + need_settle: false, + update_offset: U256::from(36), + update_len: U256::from(32), + spender: self.xtoken.address(), + spend_asset, + spend_amount, + receive_asset, + }), + input_call: None, + call_index: None, + }) + } +} + +#[cfg(test)] +mod tests { + use crate::utils::ToArray; + + use super::*; + use pink_web3::contract::Options; + use pink_web3::keys::pink::KeyPair; + use pink_web3::transports::{resolve_ready, PinkHttp}; + use pink_web3::types::H160; + + #[test] + #[ignore] + fn test_transfer_dot_to_polkadot() { + pink_extension_runtime::mock_ext::mock_all_ext(); + + let rpc = "https://moonbeam.api.onfinality.io/public"; + // Handler on Moonbeam + let handler_address: H160 = + H160::from_slice(&hex::decode("B8D20dfb8c3006AA17579887ABF719DA8bDf005B").unwrap()); + let transport = Eth::new(PinkHttp::new(rpc.clone())); + let handler = Contract::from_json( + transport, + handler_address, + include_bytes!("../../abi/handler.json"), ) - .expect("Bad abi data"); - let approve_params = (self.xtoken.address(), spend_amount); - let approve_func = token - .abi() - .function("approve") - .map_err(|_| "NoFunctionFound")?; - let approve_calldata = approve_func - .encode_input(&approve_params.into_tokens()) - .map_err(|_| "EncodeParamError")?; + .unwrap(); + let moonbeam_xtoken: [u8; 20] = + hex_literal::hex!("0000000000000000000000000000000000000804"); + let xtoken = XTokenBridge::new(&rpc, moonbeam_xtoken.into(), XTokenDestChain::Relaychain); + + let mut call = xtoken + .build_call(Step { + exe: String::from(""), + source_chain: String::from("Moonbeam"), + dest_chain: String::from("Polkadot"), + // xcDOT + spend_asset: hex::decode("FfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080").unwrap(), + // MuliLocation: (0, Here) + receive_asset: hex::decode("0000").unwrap(), + sender: Some(hex::decode("A29D4E0F035cb50C0d78c8CeBb56Ca292616Ab20").unwrap()), + recipient: hex::decode( + "7804e66ec9eea3d8daf6273ffbe0a8af25a8879cf43f14d0ebbb30941f578242", + ) + .unwrap(), + + // 0.05 xcDOT + spend_amount: Some(500_000_000 as u128), + origin_balance: None, + nonce: None, + }) + .unwrap(); + + // Apply index mannually + call.input_call = Some(0); + call.call_index = Some(0); + + // Make sure handler address hold enough spend asset and native asset (e.g. ETH). + // Because handler is the account who spend and pay fee on behalf + + // Estiamte gas before submission + let gas = resolve_ready(handler.estimate_gas( + "batchCall", + vec![call.clone()], + // Worker address + Address::from_slice(&hex::decode("bf526928373748b00763875448ee905367d97f96").unwrap()), + Options::default(), + )) + .map_err(|e| { + println!("Failed to estimated step gas cost with error: {:?}", e); + "FailedToEstimateGas" + }) + .unwrap(); + + // Tested on Moonbeam: https://moonscan.io/tx/0xe3c3e3e41cc742575f3b5dc75d8954c427e430ac63c022bc8af46ed544f0782e + // Received on Polkadot: https://polkadot.subscan.io/xcm_message/polkadot-db80cb5e14a2f2be83caffddd5c5267a8bef0b1a + let secret_key = std::env::vars().find(|x| x.0 == "SECRET_KEY"); + let secret_key = secret_key.unwrap().1; + let secret_bytes = hex::decode(secret_key).unwrap(); + let signer: [u8; 32] = secret_bytes.to_array(); - Ok(vec![ - Call { - params: CallParams::Evm(EvmCall { - target: spend_asset, - calldata: approve_calldata, - value: U256::from(0), - - need_settle: false, - update_offset: U256::from(36), - update_len: U256::from(32), - spend_asset, - spend_amount, - receive_asset, - }), - input_call: None, - call_index: None, - }, - Call { - params: CallParams::Evm(EvmCall { - target: self.xtoken.address(), - calldata: bridge_calldata, - value: U256::from(0), - - // Bridge operation do not need do settlement on source chain, because it must be the - // last step on source chain - need_settle: false, - update_offset: U256::from(36), - update_len: U256::from(32), - spend_asset, - spend_amount, - receive_asset, - }), - input_call: None, - call_index: None, - }, - ]) + let _tx_id: primitive_types::H256 = resolve_ready(handler.signed_call( + "batchCall", + vec![call], + Options::with(|opt| opt.gas = Some(gas * 15 / 10)), + KeyPair::from(signer), + )) + .map_err(|e| { + println!("Failed to submit step execution tx with error: {:?}", e); + "FailedToSubmitTransaction" + }) + .unwrap(); } } diff --git a/contracts/index_executor/src/actions/phala/mod.rs b/contracts/index_executor/src/actions/phala/mod.rs index 244a0bd7..848c8378 100644 --- a/contracts/index_executor/src/actions/phala/mod.rs +++ b/contracts/index_executor/src/actions/phala/mod.rs @@ -1,34 +1,137 @@ pub mod asset; +pub mod sygma; pub mod xtransfer; use crate::account::AccountType; +use crate::actions::base::sub_transactor; +use crate::actions::phala::xtransfer::XTransferDestChain; +use crate::actions::ActionExtraInfo; use crate::call::CallBuilder; use crate::chain::Chain; -use crate::constants::{ACALA_PARACHAIN_ID, ASTAR_PARACHAIN_ID, MOONBEAM_PARACHAIN_ID}; +use crate::constants::{ + ACALA_PARACHAIN_ID, ASTAR_PARACHAIN_ID, ETHEREUM_BLOCK_TIME, MOONBEAM_PARACHAIN_ID, + PARACHAIN_BLOCK_TIME, SYGMA_ETHEREUM_DOMAIN_ID, +}; use alloc::{boxed::Box, string::String, vec, vec::Vec}; +use sp_runtime::Permill; pub fn create_actions(_chain: &Chain) -> Vec<(String, Box)> { vec![ ( String::from("phala_bridge_to_acala"), Box::new(xtransfer::XTransferXcm::new( - ACALA_PARACHAIN_ID, + XTransferDestChain::ParaChain(ACALA_PARACHAIN_ID), AccountType::Account32, )), ), ( String::from("phala_bridge_to_astar"), Box::new(xtransfer::XTransferXcm::new( - ASTAR_PARACHAIN_ID, + XTransferDestChain::ParaChain(ASTAR_PARACHAIN_ID), AccountType::Account32, )), ), ( String::from("phala_bridge_to_moonbeam"), Box::new(xtransfer::XTransferXcm::new( - MOONBEAM_PARACHAIN_ID, + XTransferDestChain::ParaChain(MOONBEAM_PARACHAIN_ID), AccountType::Account20, )), ), + ( + String::from("phala_bridge_to_polkadot"), + Box::new(xtransfer::XTransferXcm::new( + XTransferDestChain::RelayChain, + AccountType::Account20, + )), + ), + ( + String::from("phala_bridge_to_ethereum"), + Box::new(sygma::XTransferSygma::new(SYGMA_ETHEREUM_DOMAIN_ID)), + ), + ( + String::from("khala_bridge_to_ethereum"), + Box::new(sygma::XTransferSygma::new(SYGMA_ETHEREUM_DOMAIN_ID)), + ), + ( + String::from("phala_native_transactor"), + Box::new(sub_transactor::Transactor::new(0x28, 0x07)), + ), + ( + String::from("khala_native_transactor"), + Box::new(sub_transactor::Transactor::new(0x28, 0x07)), + ), ] } + +#[allow(clippy::if_same_then_else)] +pub fn get_extra_info(chain: &str, action: &str) -> Option { + assert!(chain == "Phala" || chain == "Khala"); + if action == "phala_bridge_to_acala" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + // 0.0005 USD + const_proto_fee_in_usd: 5, + percentage_proto_fee: Permill::zero(), + confirm_time_in_sec: PARACHAIN_BLOCK_TIME * 2, + }) + } else if action == "phala_bridge_to_astar" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + // 0.0005 USD + const_proto_fee_in_usd: 5, + percentage_proto_fee: Permill::zero(), + confirm_time_in_sec: PARACHAIN_BLOCK_TIME * 2, + }) + } else if action == "phala_bridge_to_moonbeam" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + // 0.0005 USD + const_proto_fee_in_usd: 5, + percentage_proto_fee: Permill::zero(), + confirm_time_in_sec: PARACHAIN_BLOCK_TIME * 2, + }) + } else if action == "phala_bridge_to_polkadot" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + // 5 USD + const_proto_fee_in_usd: 5, + percentage_proto_fee: Permill::zero(), + confirm_time_in_sec: PARACHAIN_BLOCK_TIME, + }) + } else if action == "phala_bridge_to_ethereum" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + // 5 USD + const_proto_fee_in_usd: 50000, + percentage_proto_fee: Permill::zero(), + // Sygma relayer wait 2 blocks to finialize and 1 block on Phala to confirm + confirm_time_in_sec: PARACHAIN_BLOCK_TIME * 2 + ETHEREUM_BLOCK_TIME, + }) + } else if action == "phala_native_transactor" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + const_proto_fee_in_usd: 0, + percentage_proto_fee: Permill::zero(), + confirm_time_in_sec: PARACHAIN_BLOCK_TIME, + }) + } else if action == "khala_bridge_to_ethereum" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + // 5 USD + const_proto_fee_in_usd: 5000, + percentage_proto_fee: Permill::zero(), + // Sygma relayer wait 2 blocks to finialize and 1 block on Ethereum to confirm + confirm_time_in_sec: PARACHAIN_BLOCK_TIME * 2 + ETHEREUM_BLOCK_TIME, + }) + } else if action == "khala_native_transactor" { + Some(ActionExtraInfo { + extra_proto_fee_in_usd: 0, + const_proto_fee_in_usd: 0, + percentage_proto_fee: Permill::zero(), + confirm_time_in_sec: PARACHAIN_BLOCK_TIME, + }) + } else { + None + } +} diff --git a/contracts/index_executor/src/actions/phala/sygma.rs b/contracts/index_executor/src/actions/phala/sygma.rs new file mode 100644 index 00000000..bfe73a8c --- /dev/null +++ b/contracts/index_executor/src/actions/phala/sygma.rs @@ -0,0 +1,124 @@ +use scale::{Decode, Encode}; + +use crate::call::{Call, CallBuilder, CallParams, SubCall, SubExtrinsic}; +use crate::step::Step; + +use crate::utils::ToArray; +use xcm::v3::{prelude::*, Weight}; + +use crate::utils::slice_to_generalkey; + +#[derive(Clone)] +pub struct XTransferSygma { + evm_domain_id: u8, +} + +impl XTransferSygma { + pub fn new(evm_domain_id: u8) -> Self + where + Self: Sized, + { + Self { evm_domain_id } + } +} + +impl CallBuilder for XTransferSygma { + fn build_call(&self, step: Step) -> Result { + let recipient: [u8; 32] = step.recipient.to_array(); + let asset_location: MultiLocation = + Decode::decode(&mut step.spend_asset.as_slice()).map_err(|_| "InvalidMultilocation")?; + let multi_asset = MultiAsset { + id: AssetId::Concrete(asset_location), + fun: Fungibility::Fungible(step.spend_amount.ok_or("MissingSpendAmount")?), + }; + let dest = MultiLocation::new( + 0, + Junctions::X3( + slice_to_generalkey("sygma".as_bytes()), + GeneralIndex(self.evm_domain_id as u128), + slice_to_generalkey(&recipient), + ), + ); + let dest_weight: Option = None; + + Ok(Call { + params: CallParams::Sub(SubCall { + calldata: SubExtrinsic { + pallet_id: 0x52u8, + call_id: 0x0u8, + call: (multi_asset, dest, dest_weight), + } + .encode(), + }), + input_call: None, + call_index: None, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{call::CallBuilder, constants::SYGMA_ETHEREUM_DOMAIN_ID, step::Step}; + use dotenv::dotenv; + use pink_subrpc::{create_transaction_with_calldata, send_transaction, ExtraParam}; + use xcm::v3::{Junctions, MultiLocation}; + + #[test] + #[ignore] + fn test_pha_from_phala_to_ethereum() { + dotenv().ok(); + pink_extension_runtime::mock_ext::mock_all_ext(); + + let xtransfer_sygma_ethereum = XTransferSygma::new(SYGMA_ETHEREUM_DOMAIN_ID); + let pha_location = MultiLocation::new(0, Junctions::Here); + let recipient: Vec = + hex_literal::hex!("A29D4E0F035cb50C0d78c8CeBb56Ca292616Ab20").into(); + let endpoint = "https://subbridge-test.phala.network/rhala/ws"; + + let call = xtransfer_sygma_ethereum + .build_call(Step { + exe: String::from(""), + source_chain: String::from("Phala"), + dest_chain: String::from("Ethereum"), + spend_asset: pha_location.encode(), + receive_asset: pha_location.encode(), + sender: None, + recipient, + // Spend 1.1 PHA, 0.1 as fee, expect to receive 1 PHA + spend_amount: Some(1_100_000_000_000 as u128), + origin_balance: None, + nonce: None, + }) + .unwrap(); + + let secret_key = std::env::vars().find(|x| x.0 == "SECRET_KEY"); + let secret_key = secret_key.unwrap().1; + let secret_bytes = hex::decode(secret_key).unwrap(); + let signer: [u8; 32] = secret_bytes.to_array(); + + match &call.params { + CallParams::Sub(sub_call) => { + let signed_tx = create_transaction_with_calldata( + &signer, + &"Phala", + endpoint, + &sub_call.calldata, + ExtraParam::default(), + ) + .map_err(|_| "InvalidSignature") + .unwrap(); + + println!("{:?}", hex::encode(&sub_call.calldata)); + + let tx_id = send_transaction(&endpoint, &signed_tx) + .map_err(|_| "FailedToSubmitTransaction") + .unwrap(); + println!("Phala asset transfer: {}", &hex::encode(&tx_id)); + } + _ => { + assert!(false); + } + } + } +} diff --git a/contracts/index_executor/src/actions/phala/xtransfer.rs b/contracts/index_executor/src/actions/phala/xtransfer.rs index e7a2a53b..2af57ca0 100644 --- a/contracts/index_executor/src/actions/phala/xtransfer.rs +++ b/contracts/index_executor/src/actions/phala/xtransfer.rs @@ -1,4 +1,3 @@ -use alloc::{vec, vec::Vec}; use scale::{Decode, Encode}; use crate::call::{Call, CallBuilder, CallParams, SubCall, SubExtrinsic}; @@ -9,15 +8,21 @@ use xcm::v3::{prelude::*, AssetId, Fungibility, Junctions, MultiAsset, MultiLoca use crate::account::AccountType; +#[derive(Clone)] +pub enum XTransferDestChain { + RelayChain, + ParaChain(u32), +} + #[derive(Clone)] pub struct XTransferXcm { - dest_chain_id: u32, + dest_chain_id: XTransferDestChain, // dest chain account type account_type: AccountType, } impl XTransferXcm { - pub fn new(dest_chain_id: u32, account_type: AccountType) -> Self + pub fn new(dest_chain_id: XTransferDestChain, account_type: AccountType) -> Self where Self: Sized, { @@ -29,39 +34,40 @@ impl XTransferXcm { } impl CallBuilder for XTransferXcm { - fn build_call(&self, step: Step) -> Result, &'static str> { - let recipient = step.recipient.ok_or("MissingRecipient")?; + fn build_call(&self, step: Step) -> Result { + let recipient = step.recipient; let asset_location: MultiLocation = Decode::decode(&mut step.spend_asset.as_slice()).map_err(|_| "InvalidMultilocation")?; let multi_asset = MultiAsset { id: AssetId::Concrete(asset_location), fun: Fungibility::Fungible(step.spend_amount.ok_or("MissingSpendAmount")?), }; + let account = match &self.account_type { + AccountType::Account20 => { + let recipient: [u8; 20] = recipient.to_array(); + AccountKey20 { + network: None, + key: recipient, + } + } + AccountType::Account32 => { + let recipient: [u8; 32] = recipient.to_array(); + AccountId32 { + network: None, + id: recipient, + } + } + }; let dest = MultiLocation::new( 1, - Junctions::X2( - Parachain(self.dest_chain_id), - match &self.account_type { - AccountType::Account20 => { - let recipient: [u8; 20] = recipient.to_array(); - AccountKey20 { - network: None, - key: recipient, - } - } - AccountType::Account32 => { - let recipient: [u8; 32] = recipient.to_array(); - AccountId32 { - network: None, - id: recipient, - } - } - }, - ), + match &self.dest_chain_id { + XTransferDestChain::RelayChain => Junctions::X1(account), + XTransferDestChain::ParaChain(id) => Junctions::X2(Parachain(*id), account), + }, ); let dest_weight: Weight = Weight::from_parts(6000000000_u64, 1000000_u64); - Ok(vec![Call { + Ok(Call { params: CallParams::Sub(SubCall { calldata: SubExtrinsic { pallet_id: 0x52u8, @@ -72,34 +78,34 @@ impl CallBuilder for XTransferXcm { }), input_call: None, call_index: None, - }]) + }) } } #[cfg(test)] mod tests { use super::*; + use crate::constants::ASTAR_PARACHAIN_ID; #[test] fn test_bridge_to_astar() { let xtransfer = XTransferXcm { - dest_chain_id: 2006, + dest_chain_id: XTransferDestChain::ParaChain(ASTAR_PARACHAIN_ID), // dest chain account type account_type: AccountType::Account32, }; - let calls = xtransfer + let call = xtransfer .build_call(Step { - exe_type: String::from(""), exe: String::from(""), source_chain: String::from("Phala"), dest_chain: String::from("Astar"), spend_asset: hex::decode("0000").unwrap(), receive_asset: hex::decode("010100cd1f").unwrap(), sender: None, - recipient: Some( - hex::decode("04dba0677fc274ffaccc0fa1030a66b171d1da9226d2bb9d152654e6a746f276") - .unwrap(), - ), + recipient: hex::decode( + "04dba0677fc274ffaccc0fa1030a66b171d1da9226d2bb9d152654e6a746f276", + ) + .unwrap(), // 2 PHA spend_amount: Some(2_000_000_000_000 as u128), origin_balance: None, @@ -107,7 +113,7 @@ mod tests { }) .unwrap(); - match &calls[0].params { + match &call.params { CallParams::Sub(sub_call) => { println!("calldata: {:?}", hex::encode(&sub_call.calldata)) } diff --git a/contracts/index_executor/src/actions/polkadot/mod.rs b/contracts/index_executor/src/actions/polkadot/mod.rs new file mode 100644 index 00000000..45e8e513 --- /dev/null +++ b/contracts/index_executor/src/actions/polkadot/mod.rs @@ -0,0 +1,48 @@ +mod xcm_v2; +mod xcm_v3; + +use crate::actions::base::sub_transactor; +use crate::chain::Chain; +use crate::constants::{ASTAR_PARACHAIN_ID, MOONBEAM_PARACHAIN_ID, PHALA_PARACHAIN_ID}; +use crate::{account::AccountType, call::CallBuilder}; +use alloc::{boxed::Box, string::String, vec, vec::Vec}; + +pub fn create_actions(_chain: &Chain) -> Vec<(String, Box)> { + vec![ + ( + String::from("polkadot_bridge_to_phala"), + Box::new(xcm_v3::PolkadotXcm::new( + PHALA_PARACHAIN_ID, + AccountType::Account32, + false, + )), + ), + ( + String::from("polkadot_bridge_to_moonbeam"), + Box::new(xcm_v2::PolkadotXcm::new( + MOONBEAM_PARACHAIN_ID, + AccountType::Account20, + )), + ), + ( + String::from("polkadot_bridge_to_astar_evm"), + Box::new(xcm_v3::PolkadotXcm::new( + ASTAR_PARACHAIN_ID, + AccountType::Account32, + true, + )), + ), + ( + String::from("polkadot_bridge_to_astar"), + Box::new(xcm_v3::PolkadotXcm::new( + ASTAR_PARACHAIN_ID, + AccountType::Account32, + false, + )), + ), + ( + String::from("polkadot_native_transactor"), + Box::new(sub_transactor::Transactor::new(0x05, 0x07)), + ), + ] +} diff --git a/contracts/index_executor/src/actions/polkadot/xcm_v2.rs b/contracts/index_executor/src/actions/polkadot/xcm_v2.rs new file mode 100644 index 00000000..38abbe50 --- /dev/null +++ b/contracts/index_executor/src/actions/polkadot/xcm_v2.rs @@ -0,0 +1,113 @@ +use crate::account::AccountType; +use crate::call::{Call, CallBuilder, CallParams, SubCall, SubExtrinsic}; +use crate::step::Step; +use crate::utils::ToArray; +use alloc::vec; +use scale::{Decode, Encode}; +use xcm::{v2::prelude::*, VersionedMultiAssets, VersionedMultiLocation}; + +#[derive(Clone)] +pub struct PolkadotXcm { + dest_chain_id: u32, + account_type: AccountType, +} + +impl PolkadotXcm { + pub fn new(dest_chain_id: u32, account_type: AccountType) -> Self + where + Self: Sized, + { + Self { + dest_chain_id, + account_type, + } + } +} + +impl CallBuilder for PolkadotXcm { + fn build_call(&self, step: Step) -> Result { + let recipient = step.recipient; + let asset_location: MultiLocation = + Decode::decode(&mut step.spend_asset.as_slice()).map_err(|_| "InvalidMultilocation")?; + let dest = VersionedMultiLocation::V2(MultiLocation::new( + 0, + Junctions::X1(Parachain(self.dest_chain_id)), + )); + let beneficiary = VersionedMultiLocation::V2(MultiLocation::new( + 0, + Junctions::X1(match &self.account_type { + AccountType::Account20 => { + let recipient: [u8; 20] = recipient.to_array(); + AccountKey20 { + network: NetworkId::Any, + key: recipient, + } + } + AccountType::Account32 => { + let recipient: [u8; 32] = recipient.to_array(); + AccountId32 { + network: NetworkId::Any, + id: recipient, + } + } + }), + )); + let assets = VersionedMultiAssets::V2(MultiAssets::from(vec![MultiAsset { + id: AssetId::Concrete(asset_location), + fun: Fungibility::Fungible(step.spend_amount.ok_or("MissingSpendAmount")?), + }])); + + let fee_asset_item: u32 = 0; + + let weight_limit = WeightLimit::Unlimited; + + Ok(Call { + params: CallParams::Sub(SubCall { + calldata: SubExtrinsic { + pallet_id: 0x63u8, + call_id: 0x08u8, + call: (dest, beneficiary, assets, fee_asset_item, weight_limit), + } + .encode(), + }), + input_call: None, + call_index: None, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::constants::MOONBEAM_PARACHAIN_ID; + + #[test] + fn test_bridge_to_moonbeam() { + let xcm = PolkadotXcm { + dest_chain_id: MOONBEAM_PARACHAIN_ID, + account_type: AccountType::Account20, + }; + let call = xcm + .build_call(Step { + exe: String::from(""), + source_chain: String::from("Polkadot"), + dest_chain: String::from("Moonbeam"), + spend_asset: hex::decode("0000").unwrap(), + receive_asset: hex::decode("FfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080").unwrap(), + sender: None, + recipient: hex::decode("bEA1C40ecf9c4603ec25264860B9b6623Ff733F5").unwrap(), + // 1.1 DOT + spend_amount: Some(11_000_000_000 as u128), + origin_balance: None, + nonce: None, + }) + .unwrap(); + + match &call.params { + CallParams::Sub(sub_call) => { + println!("calldata: {:?}", hex::encode(&sub_call.calldata)) + } + _ => assert!(false), + } + } +} diff --git a/contracts/index_executor/src/actions/polkadot/xcm_v3.rs b/contracts/index_executor/src/actions/polkadot/xcm_v3.rs new file mode 100644 index 00000000..5c12f204 --- /dev/null +++ b/contracts/index_executor/src/actions/polkadot/xcm_v3.rs @@ -0,0 +1,144 @@ +use crate::account::AccountType; +use crate::call::{Call, CallBuilder, CallParams, SubCall, SubExtrinsic}; +use crate::step::Step; +use crate::utils::{h160_to_sr25519_pub, ToArray}; +use alloc::vec; +use scale::{Decode, Encode}; +use xcm::{v3::prelude::*, VersionedMultiAssets, VersionedMultiLocation}; + +#[derive(Clone)] +pub struct PolkadotXcm { + dest_chain_id: u32, + account_type: AccountType, + is_evm: bool, +} + +impl PolkadotXcm { + pub fn new(dest_chain_id: u32, account_type: AccountType, is_evm: bool) -> Self + where + Self: Sized, + { + Self { + dest_chain_id, + account_type, + is_evm, + } + } +} + +impl CallBuilder for PolkadotXcm { + fn build_call(&self, step: Step) -> Result { + let recipient = step.recipient; + let asset_location: MultiLocation = + Decode::decode(&mut step.spend_asset.as_slice()).map_err(|_| "InvalidMultilocation")?; + let dest = VersionedMultiLocation::V3(MultiLocation::new( + 0, + Junctions::X1(Parachain(self.dest_chain_id)), + )); + let beneficiary = VersionedMultiLocation::V3(MultiLocation::new( + 0, + Junctions::X1(match &self.account_type { + AccountType::Account20 => AccountKey20 { + network: None, + key: recipient.to_array(), + }, + AccountType::Account32 => AccountId32 { + network: None, + id: match self.is_evm { + true => h160_to_sr25519_pub(&recipient), + false => recipient.to_array(), + }, + }, + }), + )); + let assets = VersionedMultiAssets::V3(MultiAssets::from(vec![MultiAsset { + id: AssetId::Concrete(asset_location), + fun: Fungibility::Fungible(step.spend_amount.ok_or("MissingSpendAmount")?), + }])); + let fee_asset_item: u32 = 0; + + Ok(Call { + params: CallParams::Sub(SubCall { + calldata: SubExtrinsic { + pallet_id: 0x63u8, + call_id: 0x02u8, + call: (dest, beneficiary, assets, fee_asset_item), + } + .encode(), + }), + input_call: None, + call_index: None, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::constants::ASTAR_PARACHAIN_ID; + + #[test] + fn test_bridge_to_astar_evm() { + let xcm = PolkadotXcm { + dest_chain_id: ASTAR_PARACHAIN_ID, + account_type: AccountType::Account32, + is_evm: true, + }; + let call = xcm + .build_call(Step { + exe: String::from(""), + source_chain: String::from("Polkadot"), + dest_chain: String::from("AstarEvm"), + spend_asset: hex::decode("0000").unwrap(), + receive_asset: hex::decode("FFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF").unwrap(), + sender: None, + recipient: hex::decode("bEA1C40ecf9c4603ec25264860B9b6623Ff733F5").unwrap(), + // 1.1 DOT + spend_amount: Some(11_000_000_000 as u128), + origin_balance: None, + nonce: None, + }) + .unwrap(); + + match &call.params { + CallParams::Sub(sub_call) => { + println!("calldata: {:?}", hex::encode(&sub_call.calldata)) + } + _ => assert!(false), + } + } + + #[test] + fn test_bridge_to_astar() { + let xcm = PolkadotXcm { + dest_chain_id: ASTAR_PARACHAIN_ID, + account_type: AccountType::Account32, + is_evm: false, + }; + let call = xcm + .build_call(Step { + exe: String::from(""), + source_chain: String::from("Polkadot"), + dest_chain: String::from("Astar"), + spend_asset: hex::decode("0000").unwrap(), + receive_asset: hex::decode("0100").unwrap(), + sender: None, + recipient: hex::decode( + "04dba0677fc274ffaccc0fa1030a66b171d1da9226d2bb9d152654e6a746f276", + ) + .unwrap(), + // 1.1 DOT + spend_amount: Some(11_000_000_000 as u128), + origin_balance: None, + nonce: None, + }) + .unwrap(); + + match &call.params { + CallParams::Sub(sub_call) => { + println!("calldata: {:?}", hex::encode(&sub_call.calldata)) + } + _ => assert!(false), + } + } +} diff --git a/contracts/index_executor/src/call.rs b/contracts/index_executor/src/call.rs index 74313dee..f63f2ffb 100644 --- a/contracts/index_executor/src/call.rs +++ b/contracts/index_executor/src/call.rs @@ -22,6 +22,7 @@ pub struct EvmCall { pub need_settle: bool, pub update_offset: U256, pub update_len: U256, + pub spender: Address, pub spend_asset: Address, pub spend_amount: U256, pub receive_asset: Address, @@ -41,6 +42,7 @@ pub struct SubCall { pub calldata: Vec, } +#[allow(clippy::large_enum_variant)] #[derive(Clone, Decode, Encode, Eq, PartialEq, Ord, PartialOrd, Debug)] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] pub enum CallParams { @@ -64,28 +66,31 @@ impl Tokenizable for Call { } fn into_token(self) -> Token { + let mut tokens: Vec = vec![]; match (self.params, self.input_call, self.call_index) { - (CallParams::Evm(evm_call), Some(input_call), Some(call_index)) => Token::Tuple(vec![ - evm_call.target.into_token(), - Bytes(evm_call.calldata).into_token(), - evm_call.value.into_token(), - evm_call.need_settle.into_token(), - evm_call.update_offset.into_token(), - evm_call.update_len.into_token(), - evm_call.spend_asset.into_token(), - evm_call.spend_amount.into_token(), - evm_call.receive_asset.into_token(), - U256::from(input_call).into_token(), - U256::from(call_index).into_token(), - ]), - _ => Token::Tuple(vec![]), + (CallParams::Evm(evm_call), Some(input_call), Some(call_index)) => { + tokens.push(evm_call.target.into_token()); + tokens.push(Bytes(evm_call.calldata).into_token()); + tokens.push(evm_call.value.into_token()); + tokens.push(evm_call.need_settle.into_token()); + tokens.push(evm_call.update_offset.into_token()); + tokens.push(evm_call.update_len.into_token()); + tokens.push(evm_call.spender.into_token()); + tokens.push(evm_call.spend_asset.into_token()); + tokens.push(evm_call.spend_amount.into_token()); + tokens.push(evm_call.receive_asset.into_token()); + tokens.push(U256::from(input_call).into_token()); + tokens.push(U256::from(call_index).into_token()); + } + _ => panic!("Illegal Call"), } + Token::Tuple(tokens) } } impl TokenizableItem for Call {} pub trait CallBuilder: DynClone { - fn build_call(&self, step: Step) -> Result, &'static str>; + fn build_call(&self, step: Step) -> Result; } dyn_clone::clone_trait_object!(CallBuilder); diff --git a/contracts/index_executor/src/chain.rs b/contracts/index_executor/src/chain.rs index 92194cb0..eccf4d75 100644 --- a/contracts/index_executor/src/chain.rs +++ b/contracts/index_executor/src/chain.rs @@ -2,8 +2,7 @@ use crate::actions::base::account::{ AccountData, AccountInfo, AssetAccount, Balance, Index, OrmlTokenAccountData, }; use crate::assets::get_assetid_by_location; - -use alloc::{string::String, vec, vec::Vec}; +use alloc::{format, string::String, vec::Vec}; use pink_extension::ResultExt; use pink_subrpc::{ @@ -53,13 +52,7 @@ pub struct Chain { impl Chain { pub fn is_native(&self, asset: &Vec) -> bool { - match self.chain_type { - ChainType::Evm => { - // A little bit tricky here - asset == &vec![0] - } - ChainType::Sub => asset == &self.native_asset, - } + asset == &self.native_asset } pub fn is_evm_chain(&self) -> bool { @@ -120,13 +113,13 @@ impl BalanceFetcher for Chain { if self.is_native(&asset) { let web3 = Web3::new(transport); let balance = resolve_ready(web3.eth().balance(evm_account, None)) - .log_err("chain::get_balance: fetch data [evm native balance] failed") + .log_err(&format!("chain::get_balance: fetch data [evm native balance] failed, chain: {:?}, asset: {:?}", self.name, hex::encode(&asset))) .or(Err("FetchDataFailed"))?; balance.try_into().map_err(|_| "BalanceOverflow") } else { let eth = Eth::new(transport); let asset_account20: [u8; 20] = - asset.try_into().map_err(|_| "InvalidAddress")?; + asset.clone().try_into().map_err(|_| "InvalidAddress")?; let token_address: Address = asset_account20.into(); let token = Contract::from_json(eth, token_address, include_bytes!("./abi/erc20.json")) @@ -138,7 +131,7 @@ impl BalanceFetcher for Chain { Options::default(), None, )) - .log_err("chain::get_balance: fetch data [evm erc20 balance] failed") + .log_err(&format!("chain::get_balance: fetch data [evm erc20 balance] failed, chain: {:?}, asset: {:?}", self.name, hex::encode(&asset))) .or(Err("FetchDataFailed"))?; balance.try_into().map_err(|_| "BalanceOverflow") } @@ -175,7 +168,6 @@ impl BalanceFetcher for Chain { if asset_id.is_empty() { return Err("AssetNotRecognized"); } - if let Some(raw_storage) = get_storage( &self.endpoint, &storage_double_map_prefix::( diff --git a/contracts/index_executor/src/constants.rs b/contracts/index_executor/src/constants.rs index 26853db5..44c06182 100644 --- a/contracts/index_executor/src/constants.rs +++ b/contracts/index_executor/src/constants.rs @@ -11,8 +11,14 @@ pub const ASTAR_PARACHAIN_ID: u32 = 2006; pub const MOONBEAM_PARACHAIN_ID: u32 = 2004; pub const PHALA_PARACHAIN_ID: u32 = 2035; #[allow(dead_code)] +pub const ASSETHUB_PARACHAIN_ID: u32 = 1000; +#[allow(dead_code)] pub const KHALA_PARACHAIN_ID: u32 = 2004; #[allow(dead_code)] pub const MOONRIVER_PARACHAIN_ID: u32 = 2023; +pub const SYGMA_ETHEREUM_DOMAIN_ID: u8 = 1; -pub const HANDLER_ABI: &[u8] = include_bytes!("./abi/handler.json"); +// Ethereum block time in seconds +pub const ETHEREUM_BLOCK_TIME: u16 = 15; +// Polkadot parachain block time in seconds +pub const PARACHAIN_BLOCK_TIME: u16 = 12; diff --git a/contracts/index_executor/src/context.rs b/contracts/index_executor/src/context.rs index c0792d52..c1bd6457 100644 --- a/contracts/index_executor/src/context.rs +++ b/contracts/index_executor/src/context.rs @@ -1,4 +1,5 @@ use super::account::AccountInfo; +use crate::actions::ActionExtraInfo; use crate::{call::CallBuilder, registry::Registry}; use alloc::{boxed::Box, string::String, vec::Vec}; @@ -24,4 +25,8 @@ impl<'a> Context<'a> { .position(|e| e.0.to_lowercase() == exe.to_lowercase()) .map(|idx| dyn_clone::clone_box(&*actions[idx].1)) } + + pub fn get_action_extra_info(&self, chain: &str, action: &str) -> Option { + self.registry.get_action_extra_info(chain, action) + } } diff --git a/contracts/index_executor/src/gov.rs b/contracts/index_executor/src/gov.rs index 855b41f5..8df5b878 100644 --- a/contracts/index_executor/src/gov.rs +++ b/contracts/index_executor/src/gov.rs @@ -21,7 +21,7 @@ impl WorkerGov { ) -> Result, &'static str> { let transport = Eth::new(PinkHttp::new(endpoint)); let handler_contract = - Contract::from_json(transport, handler, crate::constants::HANDLER_ABI) + Contract::from_json(transport, handler, include_bytes!("./abi/handler.json")) .or(Err("ConstructContractFailed"))?; let worker = KeyPair::from(worker_key); @@ -33,7 +33,7 @@ impl WorkerGov { )) .or(Err("GasEstimateFailed"))?; - // Submit the `drop` transaction + // Submit the `approve` transaction let tx_id = resolve_ready(handler_contract.signed_call( "drop", id, diff --git a/contracts/index_executor/src/lib.rs b/contracts/index_executor/src/lib.rs index f8b99955..28298a94 100644 --- a/contracts/index_executor/src/lib.rs +++ b/contracts/index_executor/src/lib.rs @@ -10,6 +10,7 @@ mod chain; mod constants; mod context; mod gov; +mod price; mod registry; mod step; mod storage; @@ -28,8 +29,10 @@ mod index_executor { use crate::context::Context; use crate::gov::WorkerGov; use crate::registry::Registry; + use crate::step::{MultiStep, Simulate as StepSimulate, StepSimulateResult}; use crate::storage::StorageClient; use crate::task::{Task, TaskId, TaskStatus}; + use crate::task_deposit::Solution; use crate::task_fetcher::ActivedTaskFetcher; use alloc::{string::String, vec, vec::Vec}; use ink::storage::traits::StorageLayout; @@ -55,12 +58,18 @@ mod index_executor { FailedToInitTask, FailedToDestoryTask, FailedToUploadTask, + FailedToUploadSolution, + FailedToDecodeSolution, + InvalidSolutionData, + SolutionAlreadyExist, FailedToReApplyNonce, FailedToReRunTask, + FailedToSimulateSolution, TaskNotFoundInStorage, UnexpectedChainType, ExecutorPaused, ExecutorNotPaused, + MissingAssetInfo, } type Result = core::result::Result; @@ -87,7 +96,8 @@ mod index_executor { pub enum RunningType { // [source_chain, worker_sr25519_pub_key] Fetch(String, [u8; 32]), - Execute, + // [worker_sr25519_pub_key] + Execute([u8; 32]), } #[ink(storage)] @@ -128,13 +138,24 @@ mod index_executor { Ok(()) } - /// TODO: Debug only, remove before release + /// Debug only, remove before release #[ink(message)] pub fn export_worker_keys(&self) -> Result> { self.ensure_owner()?; Ok(self.worker_prv_keys.clone()) } + /// Debug only, remove before release + #[ink(message)] + pub fn import_worker_keys(&mut self, keys: Vec<[u8; 32]>) -> Result<()> { + self.ensure_owner()?; + self.worker_prv_keys = keys; + for key in self.worker_prv_keys.iter() { + self.worker_accounts.push(AccountInfo::from(*key)) + } + Ok(()) + } + /// FIXME: Pass the key implicitly #[ink(message)] pub fn config_engine( @@ -166,50 +187,28 @@ mod index_executor { Ok(()) } - /// Save worker account information to remote storage. #[ink(message)] - pub fn config_storage(&self) -> Result<()> { - self.ensure_owner()?; - - let config = self.ensure_configured()?; - let client = StorageClient::new(config.db_url.clone(), config.db_token.clone()); - - pink_extension::debug!("Start to config storage"); - let accounts: Vec<[u8; 32]> = self - .worker_accounts - .clone() - .into_iter() - .map(|account| account.account32) - .collect(); - client - .insert(b"free_accounts", &accounts.encode()) - .map_err(|_| Error::FailedToSetupStorage)?; - - let empty_tasks: Vec = vec![]; - client - .insert(b"pending_tasks", &empty_tasks.encode()) - .map_err(|_| Error::FailedToSetupStorage)?; - Self::env().emit_event(WorkerSetToStorage {}); - Ok(()) - } - - #[ink(message)] - pub fn update_registry( + pub fn update_chain_info( &mut self, chain: String, endpoint: String, - indexer: String, + indexer_url: String, ) -> Result<()> { self.ensure_owner()?; if let Some(index) = self.registry.chains.iter().position(|x| x.name == chain) { // Update the value at the found index self.registry.chains[index].endpoint = endpoint; - self.registry.chains[index].tx_indexer_url = indexer; - Ok(()) - } else { - Err(Error::ChainNotFound) + self.registry.chains[index].tx_indexer_url = indexer_url; } + Ok(()) + } + + #[ink(message)] + pub fn register_asset(&mut self, asset: crate::registry::Asset) -> Result<()> { + self.ensure_owner()?; + self.registry.assets.push(asset); + Ok(()) } #[ink(message)] @@ -290,6 +289,73 @@ mod index_executor { Ok(()) } + #[ink(message)] + pub fn upload_solution(&self, id: TaskId, solution: Vec) -> Result<()> { + self.ensure_running()?; + let config = self.ensure_configured()?; + let client = StorageClient::new(config.db_url.clone(), config.db_token.clone()); + + let solution_id = [b"solution".to_vec(), id.to_vec()].concat(); + if client + .read::(&solution_id) + .map_err(|_| Error::FailedToReadStorage)? + .is_some() + { + return Err(Error::SolutionAlreadyExist); + } + + client + .insert(&solution_id, &solution) + .log_err("failed to upload solution") + .or(Err(Error::FailedToUploadSolution))?; + + Ok(()) + } + + #[ink(message)] + pub fn simulate_solution( + &self, + worker: [u8; 32], + solution: Vec, + ) -> Result> { + let solution: Solution = + Decode::decode(&mut solution.as_slice()).or(Err(Error::FailedToDecodeSolution))?; + + let signer: [u8; 32] = self.pub_to_prv(worker).ok_or(Error::WorkerNotFound)?; + let context = Context { + signer, + worker_accounts: self.worker_accounts.clone(), + registry: &self.registry, + }; + let mut simulate_results: Vec = vec![]; + for multi_step_input in solution.iter() { + let mut multi_step: MultiStep = multi_step_input + .clone() + .try_into() + .or(Err(Error::InvalidSolutionData))?; + let asset_location = multi_step.as_single_step().spend_asset; + let asset_info = context + .registry + .get_asset(&multi_step.as_single_step().source_chain, &asset_location) + .ok_or(Error::MissingAssetInfo)?; + // Set spend asset 0.0001 + multi_step.set_spend(10u128.pow(asset_info.decimals as u32) / 10000); + let step_simulate_result = multi_step + .simulate(&Context { + signer, + registry: &self.registry, + worker_accounts: self.worker_accounts.clone(), + }) + .map_err(|err| { + pink_extension::error!("Solution simulation failed with error: {}", err); + Error::FailedToSimulateSolution + })?; + simulate_results.push(step_simulate_result); + } + + Ok(simulate_results) + } + #[ink(message)] pub fn run(&self, running_type: RunningType) -> Result<()> { self.ensure_running()?; @@ -301,7 +367,7 @@ mod index_executor { RunningType::Fetch(source_chain, worker) => { self.fetch_task(&client, &source_chain, worker)? } - RunningType::Execute => self.execute_task(&client)?, + RunningType::Execute(worker) => self.execute_task(&client, worker)?, }; Ok(()) @@ -326,6 +392,7 @@ mod index_executor { worker_accounts: self.worker_accounts.clone(), registry: &self.registry, }; + task.retry_counter = 0; task.reapply_nonce(execute_index as u64, &context, &client) .map_err(|_| Error::FailedToReApplyNonce)?; pink_extension::info!( @@ -361,31 +428,30 @@ mod index_executor { let config = self.ensure_configured()?; let client = StorageClient::new(config.db_url.clone(), config.db_token.clone()); - let mut tasks = Vec::new(); - let Some((ids, _)) = client - .read::>(b"pending_tasks") - .map_err(|_| Error::FailedToReadStorage)? else { - return Ok(tasks) - }; - - for id in ids.iter() { - pink_extension::debug!( - "Trying to read pending task data from remote storage, task id: {:?}", - &hex::encode(id) - ); - let (task, _) = client - .read::(id) + let mut tasks: Vec = Vec::new(); + for worker_account in self.worker_accounts.iter() { + if let Some((task_id, _)) = client + .read::(&worker_account.account32) .map_err(|_| Error::FailedToReadStorage)? - .ok_or(Error::TaskNotFoundInStorage)?; - - tasks.push(task); + { + pink_extension::debug!( + "Trying to read pending task data from remote storage, task id: {:?}", + &hex::encode(task_id) + ); + let (task, _) = client + .read::(&task_id) + .map_err(|_| Error::FailedToReadStorage)? + .ok_or(Error::TaskNotFoundInStorage)?; + + tasks.push(task); + } } Ok(tasks) } #[ink(message)] - pub fn get_running_task(&self, id: TaskId) -> Result> { + pub fn get_task(&self, id: TaskId) -> Result> { let config = self.ensure_configured()?; let client = StorageClient::new(config.db_url.clone(), config.db_token.clone()); Ok(client @@ -394,6 +460,19 @@ mod index_executor { .map(|(task, _)| task)) } + #[ink(message)] + pub fn get_solution(&self, id: TaskId) -> Result>> { + self.ensure_running()?; + let config = self.ensure_configured()?; + let client = StorageClient::new(config.db_url.clone(), config.db_token.clone()); + + let solution_id = [b"solution".to_vec(), id.to_vec()].concat(); + Ok(client + .read::(&solution_id) + .map_err(|_| Error::FailedToReadStorage)? + .map(|(solution, _)| solution.encode())) + } + /// Returs the interior registry, callable to all #[ink(message)] pub fn get_registry(&self) -> Result { @@ -406,21 +485,6 @@ mod index_executor { Ok(self.worker_accounts.clone()) } - /// Return worker accounts information that is free - #[ink(message)] - pub fn get_free_worker_account(&self) -> Result> { - let config = self.ensure_configured()?; - let client = StorageClient::new(config.db_url.clone(), config.db_token.clone()); - if let Some((accounts, _)) = client - .read::>(b"free_accounts") - .map_err(|_| Error::FailedToReadStorage)? - { - Ok(accounts) - } else { - Ok(vec![]) - } - } - /// Search actived tasks from source chain and upload them to storage pub fn fetch_task( &self, @@ -437,55 +501,53 @@ mod index_executor { .ok_or(Error::ChainNotFound)?, AccountInfo::from(signer), ) - .fetch_task() + .fetch_task(client) .map_err(|_| Error::FailedToFetchTask)?; - if let Some(mut actived_task) = actived_task { - // Initialize task, and save it to on-chain storage - actived_task - .init( - &Context { - signer, - registry: &self.registry, - worker_accounts: self.worker_accounts.clone(), - }, - client, - ) - .map_err(|e| { - pink_extension::info!( - "Initial error {:?}, initialized task data: {:?}", - &e, - &actived_task - ); - Error::FailedToInitTask - })?; - pink_extension::info!( - "An actived task was found on {:?}, initialized task data: {:?}", - &source_chain, - &actived_task - ); - } else { + let Some(mut actived_task) = actived_task else { pink_extension::debug!("No actived task found from {:?}", &source_chain); - } + return Ok(()); + }; + + // Initialize task, and save it to on-chain storage + actived_task + .init( + &Context { + signer, + registry: &self.registry, + worker_accounts: self.worker_accounts.clone(), + }, + client, + ) + .map_err(|e| { + pink_extension::info!( + "Initial error {:?}, initialized task data: {:?}", + &e, + &actived_task + ); + Error::FailedToInitTask + })?; + pink_extension::info!( + "An actived task was found on {:?}, initialized task data: {:?}", + &source_chain, + &actived_task + ); Ok(()) } /// Execute tasks from all supported blockchains. This is a query operation /// that scheduler invokes periodically. - pub fn execute_task(&self, client: &StorageClient) -> Result<()> { - let Some((ids, _)) = client - .read::>(b"pending_tasks") - .map_err(|_| Error::FailedToReadStorage)? else { - return Ok(()); - }; - - for id in ids.iter() { + pub fn execute_task(&self, client: &StorageClient, worker: [u8; 32]) -> Result<()> { + if let Some((id, _)) = client + .read::(&worker) + .map_err(|_| Error::FailedToReadStorage)? + { pink_extension::debug!( "Trying to read pending task data from remote storage, task id: {:?}", &hex::encode(id) ); let (mut task, task_doc) = client - .read::(id) + .read::(&id) .map_err(|_| Error::FailedToReadStorage)? .ok_or(Error::TaskNotFoundInStorage)?; @@ -531,12 +593,16 @@ mod index_executor { "Task execution has not finished yet, update data to remote storage: {:?}", hex::encode(task.id) ); - client - .update(task.id.as_ref(), &task.encode(), task_doc) - .map_err(|_| Error::FailedToUploadTask)?; - continue; } } + client + .update(task.id.as_ref(), &task.encode(), task_doc) + .map_err(|_| Error::FailedToUploadTask)?; + } else { + pink_extension::debug!( + "No pending task to execute for worker: {:?}, return", + &hex::encode(worker) + ); } Ok(()) @@ -581,8 +647,10 @@ mod index_executor { #[cfg(test)] mod tests { use super::*; + use crate::step::{MultiStepInput, StepInput}; // use dotenv::dotenv; // use pink_extension::PinkEnvironment; + use crate::utils::ToArray; use xcm::v3::{prelude::*, MultiLocation}; fn deploy_executor() -> Executor { @@ -618,5 +686,156 @@ mod index_executor { ) ) } + + #[ink::test] + #[ignore] + fn simulate_solution_should_work() { + pink_extension_runtime::mock_ext::mock_all_ext(); + let secret_key = std::env::vars().find(|x| x.0 == "SECRET_KEY"); + let secret_key = secret_key.unwrap().1; + let secret_bytes = hex::decode(secret_key).unwrap(); + let worker_key: [u8; 32] = secret_bytes.to_array(); + let mut executor = deploy_executor(); + assert_eq!(executor.import_worker_keys(vec![worker_key]), Ok(())); + assert_eq!( + executor.config_engine("url".to_string(), "key".to_string(), [0; 32].into(), false), + Ok(()) + ); + assert_eq!(executor.resume_executor(), Ok(())); + + let solution1: Vec = vec![ + MultiStepInput::Batch(vec![ + StepInput { + exe: "ethereum_nativewrapper".to_string(), + source_chain: "Ethereum".to_string(), + dest_chain: "Ethereum".to_string(), + spend_asset: "0x0000000000000000000000000000000000000000".to_string(), + // WETH + receive_asset: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".to_string(), + recipient: "0xd693bDC5cb0cF2a31F08744A0Ec135a68C26FE1c".to_string(), + }, + StepInput { + exe: "ethereum_uniswapv2".to_string(), + source_chain: "Ethereum".to_string(), + dest_chain: "Ethereum".to_string(), + spend_asset: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".to_string(), + // PHA + receive_asset: "0x6c5bA91642F10282b576d91922Ae6448C9d52f4E".to_string(), + recipient: "0xd693bDC5cb0cF2a31F08744A0Ec135a68C26FE1c".to_string(), + }, + StepInput { + exe: "ethereum_sygmabridge_to_phala".to_string(), + source_chain: "Ethereum".to_string(), + dest_chain: "Phala".to_string(), + spend_asset: "0x6c5bA91642F10282b576d91922Ae6448C9d52f4E".to_string(), + // PHA + receive_asset: "0x0000".to_string(), + recipient: + "0x641017970d80738617e4e9b9b01d8d2ed5bc3d881a60e5105620abfbf5cb1331" + .to_string(), + }, + ]), + MultiStepInput::Single(StepInput { + exe: "phala_bridge_to_astar".to_string(), + source_chain: "Phala".to_string(), + dest_chain: "Astar".to_string(), + spend_asset: "0x0000".to_string(), + // PHA + receive_asset: "0x010100cd1f".to_string(), + recipient: "0x641017970d80738617e4e9b9b01d8d2ed5bc3d881a60e5105620abfbf5cb1331" + .to_string(), + }), + ]; + + let result1 = executor + .simulate_solution(executor.worker_accounts[0].account32, solution1.encode()) + .unwrap(); + println!("simulation result1: {:?}", result1); + + let solution2: Solution = vec![ + MultiStepInput::Batch(vec![ + StepInput { + exe: String::from("moonbeam_nativewrapper"), + source_chain: String::from("Moonbeam"), + dest_chain: String::from("Moonbeam"), + spend_asset: String::from("0x0000000000000000000000000000000000000802"), + receive_asset: String::from("0xacc15dc74880c9944775448304b263d191c6077f"), + recipient: String::from("0x8351BAE38E3D590063544A99A95BF4fe5379110b"), + } + .try_into() + .unwrap(), + StepInput { + exe: String::from("moonbeam_stellaswap"), + source_chain: String::from("Moonbeam"), + dest_chain: String::from("Moonbeam"), + spend_asset: String::from("0xacc15dc74880c9944775448304b263d191c6077f"), + receive_asset: String::from("0xffffffff1fcacbd218edc0eba20fc2308c778080"), + recipient: String::from("0x8351BAE38E3D590063544A99A95BF4fe5379110b"), + } + .try_into() + .unwrap(), + StepInput { + exe: String::from("moonbeam_stellaswap"), + source_chain: String::from("Moonbeam"), + dest_chain: String::from("Moonbeam"), + spend_asset: String::from("0xffffffff1fcacbd218edc0eba20fc2308c778080"), + receive_asset: String::from("0xffffffffa893ad19e540e172c10d78d4d479b5cf"), + recipient: String::from("0x8351BAE38E3D590063544A99A95BF4fe5379110b"), + } + .try_into() + .unwrap(), + StepInput { + exe: String::from("moonbeam_bridge_to_astar"), + source_chain: String::from("Moonbeam"), + dest_chain: String::from("Astar"), + spend_asset: String::from("0xffffffffa893ad19e540e172c10d78d4d479b5cf"), + receive_asset: String::from("0x010100591f"), + recipient: String::from( + "0x641017970d80738617e4e9b9b01d8d2ed5bc3d881a60e5105620abfbf5cb1331", + ), + } + .try_into() + .unwrap(), + ]), + MultiStepInput::Single( + StepInput { + exe: String::from("astar_bridge_to_astarevm"), + source_chain: String::from("Astar"), + dest_chain: String::from("AstarEvm"), + spend_asset: String::from("0x010100591f"), + receive_asset: String::from("0x0000000000000000000000000000000000000000"), + recipient: String::from("0x5cddb3ad187065e0122f3f46d13ad6ca486e4644"), + } + .try_into() + .unwrap(), + ), + MultiStepInput::Batch(vec![ + StepInput { + exe: String::from("astar_evm_nativewrapper"), + source_chain: String::from("AstarEvm"), + dest_chain: String::from("AstarEvm"), + spend_asset: String::from("0x0000000000000000000000000000000000000000"), + receive_asset: String::from("0xAeaaf0e2c81Af264101B9129C00F4440cCF0F720"), + recipient: String::from("0xAE1Ab0a83de66a545229d39E874237fbaFe05714"), + } + .try_into() + .unwrap(), + StepInput { + exe: String::from("astar_evm_arthswap"), + source_chain: String::from("AstarEvm"), + dest_chain: String::from("AstarEvm"), + spend_asset: String::from("0xAeaaf0e2c81Af264101B9129C00F4440cCF0F720"), + receive_asset: String::from("0xFFFFFFFF00000000000000010000000000000003"), + recipient: String::from("0xA29D4E0F035cb50C0d78c8CeBb56Ca292616Ab20"), + } + .try_into() + .unwrap(), + ]), + ]; + let result2 = executor + .simulate_solution(executor.worker_accounts[0].account32, solution2.encode()) + .unwrap(); + println!("simulation result2: {:?}", result2); + } } } diff --git a/contracts/index_executor/src/price.rs b/contracts/index_executor/src/price.rs new file mode 100644 index 00000000..8c04e17b --- /dev/null +++ b/contracts/index_executor/src/price.rs @@ -0,0 +1,35 @@ +/// The price poller should call update_price periodically. `get_price` should +/// Return the price from local cache instead of fetch from internet. +/// +/// The key is the asset sygmbol, and the value is the price. Each asset only have one price +/// related. That means we treat xcPHA and PHA as PHA, because they are the same asset essentially +use alloc::vec::Vec; + +// TODO: Get price from local cache +pub fn get_price(chain: &str, asset: &Vec) -> Option { + // ETH + if chain == "Ethereum" && asset == &[0; 20] { + // 2000 USD + Some(20000000) + } + // ASTR + else if chain == "AstarEvm" && asset == &[0; 20] { + // 0.07 USD + Some(700) + } + // GLMR + else if chain == "Moonbeam" + && asset == &hex_literal::hex!("0000000000000000000000000000000000000802") + { + // 0.3 USD + Some(3000) + } else { + // TODO + // 0.1 USD + Some(1000) + } +} + +// TODO: Fetch asset price from internet and save to local cache +#[allow(dead_code, unused_variables)] +pub fn update_price(assets: Vec<&str>) {} diff --git a/contracts/index_executor/src/registry.rs b/contracts/index_executor/src/registry.rs index ff456fda..b1706866 100644 --- a/contracts/index_executor/src/registry.rs +++ b/contracts/index_executor/src/registry.rs @@ -1,4 +1,5 @@ //#[allow(clippy::large_enum_variant)] +use crate::actions::ActionExtraInfo; use crate::{ call::CallBuilder, chain::{Chain, ChainType, ForeignAssetModule}, @@ -12,10 +13,20 @@ use alloc::{ vec::Vec, }; +#[derive(Clone, scale::Encode, scale::Decode, Debug)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout,))] +pub struct Asset { + pub chain: String, + pub symbol: String, + pub location: Vec, + pub decimals: u8, +} + #[derive(Clone, scale::Encode, scale::Decode, Debug)] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout,))] pub struct Registry { pub chains: Vec, + pub assets: Vec, } impl Default for Registry { @@ -31,26 +42,25 @@ impl Registry { Chain { id: 0, name: "Ethereum".to_string(), - endpoint: "https://mainnet.infura.io/v3/6d61e7957c1c489ea8141e947447405b" - .to_string(), + endpoint: "https://rpc.ankr.com/eth".to_string(), chain_type: ChainType::Evm, native_asset: hex_literal::hex!("0000000000000000000000000000000000000000") .to_vec(), foreign_asset: None, - handler_contract: hex_literal::hex!("F9eaE3Ec6BFE94F510eb3a5de8Ac9dEB9E74DF39") - .to_vec(), - tx_indexer_url: "null".to_string(), + handler_contract: hex::decode("d693bDC5cb0cF2a31F08744A0Ec135a68C26FE1c") + .expect("InvalidLocation"), + tx_indexer_url: "https://squid.subsquid.io/graph-ethereum/graphql".to_string(), }, Chain { id: 1, name: "Moonbeam".to_string(), - endpoint: "https://moonbeam.api.onfinality.io/public".to_string(), + endpoint: "https://rpc.api.moonbeam.network".to_string(), chain_type: ChainType::Evm, - native_asset: hex_literal::hex!("0000000000000000000000000000000000000000") - .to_vec(), + native_asset: hex::decode("0000000000000000000000000000000000000802") + .expect("InvalidLocation"), foreign_asset: None, - handler_contract: hex_literal::hex!("635eA86804200F80C16ea8EdDc3c749a54a9C37D") - .to_vec(), + handler_contract: hex::decode("8351BAE38E3D590063544A99A95BF4fe5379110b") + .expect("InvalidLocation"), tx_indexer_url: "https://squid.subsquid.io/graph-moonbeam/graphql".to_string(), }, Chain { @@ -61,8 +71,8 @@ impl Registry { native_asset: hex_literal::hex!("0000000000000000000000000000000000000000") .to_vec(), foreign_asset: None, - handler_contract: hex_literal::hex!("B376b0Ee6d8202721838e76376e81eEc0e2FE864") - .to_vec(), + handler_contract: hex::decode("AE1Ab0a83de66a545229d39E874237fbaFe05714") + .expect("InvalidLocation"), tx_indexer_url: "https://squid.subsquid.io/graph-astar/graphql".to_string(), }, Chain { @@ -107,6 +117,71 @@ impl Registry { handler_contract: hex::decode("00").expect("InvalidLocation"), tx_indexer_url: "https://squid.subsquid.io/graph-acala/graphql".to_string(), }, + Chain { + id: 7, + name: "Polkadot".to_string(), + endpoint: "https://polkadot.api.onfinality.io/public".to_string(), + chain_type: ChainType::Sub, + native_asset: hex::decode("0000").expect("InvalidLocation"), + foreign_asset: Some(ForeignAssetModule::PalletAsset), + // FIXME: No Handler pallet in Polkadot + handler_contract: hex::decode("00").expect("InvalidLocation"), + tx_indexer_url: "https://squid.subsquid.io/graph-polkadot/graphql".to_string(), + }, + ], + assets: vec![ + Asset { + chain: "Ethereum".to_string(), + symbol: "ETH".to_string(), + location: hex::decode("0000000000000000000000000000000000000000") + .expect("InvalidLocation"), + decimals: 18, + }, + Asset { + chain: "Ethereum".to_string(), + symbol: "PHA".to_string(), + location: hex::decode("6c5bA91642F10282b576d91922Ae6448C9d52f4E") + .expect("InvalidLocation"), + decimals: 18, + }, + Asset { + chain: "Phala".to_string(), + symbol: "PHA".to_string(), + location: hex::decode("0000").expect("InvalidLocation"), + decimals: 12, + }, + Asset { + chain: "Khala".to_string(), + symbol: "PHA".to_string(), + location: hex::decode("0000").expect("InvalidLocation"), + decimals: 12, + }, + Asset { + chain: "Moonbeam".to_string(), + symbol: "GLMR".to_string(), + location: hex::decode("0000000000000000000000000000000000000802") + .expect("InvalidLocation"), + decimals: 18, + }, + Asset { + chain: "AstarEvm".to_string(), + symbol: "ASTR".to_string(), + location: [0; 20].to_vec(), + decimals: 18, + }, + Asset { + chain: "AstarEvm".to_string(), + symbol: "GLMR".to_string(), + location: hex::decode("FFFFFFFF00000000000000010000000000000003") + .expect("InvalidLocation"), + decimals: 18, + }, + Asset { + chain: "Astar".to_string(), + symbol: "ASTR".to_string(), + location: hex::decode("010100591f").expect("InvalidLocation"), + decimals: 18, + }, ], } } @@ -119,6 +194,14 @@ impl Registry { .map(|idx| chains[idx].clone()) } + pub fn get_asset(&self, chain: &String, location: &Vec) -> Option { + let assets = &self.assets; + assets + .iter() + .position(|c| &c.chain == chain && &c.location == location) + .map(|idx| assets[idx].clone()) + } + pub fn create_actions(&self, chain: &String) -> Vec<(String, Box)> { let chain = self.get_chain(chain).expect("ChainNotFound"); @@ -129,7 +212,20 @@ impl Registry { "Ethereum" => crate::actions::ethereum::create_actions(&chain), "Moonbeam" => crate::actions::moonbeam::create_actions(&chain), "Phala" | "Khala" => crate::actions::phala::create_actions(&chain), + "Polkadot" => crate::actions::polkadot::create_actions(&chain), _ => vec![], } } + + pub fn get_action_extra_info(&self, chain: &str, action: &str) -> Option { + match chain { + "Acala" => crate::actions::acala::get_extra_info(chain, action), + "AstarEvm" => crate::actions::astar::get_extra_info(chain, action), + "Astar" => crate::actions::astar::get_extra_info(chain, action), + "Ethereum" => crate::actions::ethereum::get_extra_info(chain, action), + "Moonbeam" => crate::actions::moonbeam::get_extra_info(chain, action), + "Phala" | "Khala" => crate::actions::phala::get_extra_info(chain, action), + _ => None, + } + } } diff --git a/contracts/index_executor/src/step.rs b/contracts/index_executor/src/step.rs index a786a1f0..ea7200ba 100644 --- a/contracts/index_executor/src/step.rs +++ b/contracts/index_executor/src/step.rs @@ -1,7 +1,8 @@ +use crate::actions::ActionExtraInfo; use crate::chain::{BalanceFetcher, Chain, ChainType}; use crate::utils::ToArray; -use alloc::{borrow::ToOwned, boxed::Box, format, string::String, vec, vec::Vec}; -use pink_extension::ResultExt; +use alloc::vec; +use alloc::{borrow::ToOwned, boxed::Box, string::String, vec::Vec}; use pink_subrpc::{create_transaction_with_calldata, send_transaction, ExtraParam}; use crate::account::AccountInfo; @@ -18,30 +19,28 @@ use pink_web3::{ types::U256, }; use scale::{Decode, Encode}; -use serde::Deserialize; -/// The json object that the execution plan consists of -#[derive(Deserialize, Clone)] -pub struct StepJson { - pub exe_type: String, +#[derive(Clone, Debug, Decode, Encode, PartialEq)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct StepInput { pub exe: String, pub source_chain: String, pub dest_chain: String, pub spend_asset: String, pub receive_asset: String, + pub recipient: String, } #[derive(Clone, Decode, Encode, Eq, PartialEq, Ord, PartialOrd)] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] pub struct Step { - pub exe_type: String, pub exe: String, pub source_chain: String, pub dest_chain: String, pub spend_asset: Vec, pub receive_asset: Vec, pub sender: Option>, - pub recipient: Option>, + pub recipient: Vec, pub spend_amount: Option, // Used to check balance change pub origin_balance: Option, @@ -51,14 +50,13 @@ pub struct Step { impl sp_std::fmt::Debug for Step { fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result { f.debug_struct("Step") - .field("exe_type", &self.exe_type) .field("exe", &self.exe) .field("source_chain", &self.source_chain) .field("dest_chain", &self.dest_chain) .field("spend_asset", &hex::encode(&self.spend_asset)) .field("receive_asset", &hex::encode(&self.receive_asset)) .field("sender", &self.sender.as_ref().map(hex::encode)) - .field("recipient", &self.recipient.as_ref().map(hex::encode)) + .field("recipient", &hex::encode(&self.recipient)) .field("spend_amount", &self.spend_amount) .field("origin_balance", &self.origin_balance) .field("nonce", &self.nonce) @@ -66,19 +64,18 @@ impl sp_std::fmt::Debug for Step { } } -impl TryFrom for Step { +impl TryFrom for Step { type Error = &'static str; - fn try_from(json: StepJson) -> Result { + fn try_from(input: StepInput) -> Result { Ok(Self { - exe_type: json.exe_type, - exe: json.exe, - source_chain: json.source_chain, - dest_chain: json.dest_chain, - spend_asset: Self::decode_address(&json.spend_asset)?, - receive_asset: Self::decode_address(&json.receive_asset)?, + exe: input.exe, + source_chain: input.source_chain, + dest_chain: input.dest_chain, + spend_asset: Self::decode_address(&input.spend_asset)?, + receive_asset: Self::decode_address(&input.receive_asset)?, sender: None, - recipient: None, + recipient: Self::decode_address(&input.recipient)?, spend_amount: Some(0), origin_balance: None, nonce: None, @@ -101,7 +98,17 @@ impl Step { "Trying to build calldata for according to step data: {:?}", self, ); - action.build_call(self.clone()) + let source_chain = self.source_chain(context).ok_or("MissingSourceChain")?; + let worker_account = AccountInfo::from(context.signer); + let sender = match source_chain.chain_type { + ChainType::Evm => worker_account.account20.to_vec(), + ChainType::Sub => worker_account.account32.to_vec(), + }; + + let mut step = self.clone(); + step.sender = Some(sender); + let call = action.build_call(step)?; + Ok(vec![call]) } pub fn is_bridge_step(&self) -> bool { @@ -123,7 +130,34 @@ impl Step { return Err("InvalidAddressInStep"); } - hex::decode(&address[2..]).or(Err("DecodeAddressFailed")) + hex::decode(&address[2..]).map_err(|_| "DecodeAddressFailed") + } +} + +#[derive(Clone, Debug, Decode, Encode, PartialEq)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +#[allow(clippy::large_enum_variant)] +pub enum MultiStepInput { + Single(StepInput), + Batch(Vec), +} + +impl TryFrom for MultiStep { + type Error = &'static str; + + fn try_from(input: MultiStepInput) -> Result { + match input { + MultiStepInput::Single(step_input) => { + Ok(MultiStep::Single(Step::try_from(step_input)?)) + } + MultiStepInput::Batch(vec_step_input) => { + let mut vec_step = Vec::new(); + for step_input in vec_step_input { + vec_step.push(Step::try_from(step_input)?); + } + Ok(MultiStep::Batch(vec_step)) + } + } } } @@ -137,12 +171,18 @@ pub enum MultiStep { } impl MultiStep { - pub fn derive_calls(&mut self, context: &Context) -> Result, &'static str> { + pub fn derive_calls(&self, context: &Context) -> Result, &'static str> { if self.as_single_step().spend_amount.is_none() { return Err("MissingSpendAmount"); } let calls = match self { - MultiStep::Single(step) => step.derive_calls(context)?, + MultiStep::Single(step) => { + let mut calls = step.derive_calls(context)?; + assert!(calls.len() == 1); + calls[0].call_index = Some(0); + calls[0].input_call = Some(0); + calls + } MultiStep::Batch(batch_steps) => { if batch_steps.is_empty() { return Err("BatchStepEmpty"); @@ -212,7 +252,7 @@ impl MultiStep { let step = self.as_single_step(); let dest_chain = step.dest_chain(context).ok_or("MissingDestChain")?; let origin_balance = step.origin_balance.ok_or("MissingBalance")?; - let recipient = step.recipient.clone().ok_or("MissingRecipient")?; + let recipient = step.recipient.clone(); let latest_balance = dest_chain.get_balance(step.receive_asset, recipient.clone())?; pink_extension::debug!( "Settle info of account {:?}: origin_balance is {:?}, latest_balance is {:?}", @@ -244,8 +284,7 @@ impl MultiStep { .registry .get_chain(&dest_chain) .ok_or("MissingDestChain")?; - let origin_balance = - chain.get_balance(receive_asset, recipient.ok_or("MissingRecipient")?)?; + let origin_balance = chain.get_balance(receive_asset, recipient)?; match self { MultiStep::Single(step) => { @@ -300,12 +339,14 @@ impl Runner for MultiStep { fn run(&mut self, nonce: u64, context: &Context) -> Result, &'static str> { let as_single_step = self.as_single_step(); + let spend_amount = as_single_step.spend_amount.ok_or("MissingSpendAmount")?; let chain = as_single_step .source_chain(context) .ok_or("MissingSourceChain")?; let signer = context.signer; let worker_account = AccountInfo::from(context.signer); let calls = self.derive_calls(context)?; + pink_extension::debug!("Derived calls to be sumitted: {:?}", &calls); self.sync_origin_balance(context)?; @@ -313,25 +354,31 @@ impl Runner for MultiStep { let tx_id = match chain.chain_type { ChainType::Evm => { let handler = Contract::from_json( - Eth::new(PinkHttp::new(chain.endpoint)), + Eth::new(PinkHttp::new(&chain.endpoint)), chain.handler_contract.to_array().into(), - crate::constants::HANDLER_ABI, + include_bytes!("./abi/handler.json"), ) .expect("Bad abi data"); + let is_spend_native = chain.is_native(&as_single_step.spend_asset); // Estiamte gas before submission let gas = resolve_ready(handler.estimate_gas( "batchCall", calls.clone(), worker_account.account20.into(), - Options::default(), - )) - .log_err(&format!( - "Step.run: failed to estimated step gas cost with calls: {:?}", - &calls + Options::with(|opt| { + opt.value = if is_spend_native { + Some(U256::from(spend_amount)) + } else { + None + } + }), )) - .or(Err("FailedToEstimateGas"))?; - pink_extension::debug!("Estimated step gas error: {:?}", gas); + .map_err(|e| { + pink_extension::error!("Failed to estimated step gas cost with error: {:?}", e); + "FailedToEstimateGas" + })?; + pink_extension::debug!("Estimated step gas: {:?}", gas); // Actually submit the tx (no guarantee for success) let tx_id = resolve_ready(handler.signed_call( @@ -340,14 +387,21 @@ impl Runner for MultiStep { Options::with(|opt| { opt.gas = Some(gas); opt.nonce = Some(U256::from(nonce)); + opt.value = if is_spend_native { + Some(U256::from(spend_amount)) + } else { + None + } }), KeyPair::from(signer), )) - .log_err(&format!( - "Step.run: failed to submit tx with nonce: {:?}", - &nonce - )) - .or(Err("FailedToSubmitTransaction"))?; + .map_err(|e| { + pink_extension::error!( + "Failed to submit step execution tx with error: {:?}", + e + ); + "FailedToSubmitTransaction" + })?; tx_id.as_bytes().to_owned() } @@ -364,18 +418,21 @@ impl Runner for MultiStep { era: None, }, ) - .log_err(&format!( - "Step.run: failed to create transaction with nonce: {:?}", - hex::encode(&calldata) - )) - .or(Err("FailedToCreateTransaction"))?; - - send_transaction(&chain.endpoint, &signed_tx) - .log_err(&format!( - "Step.run: failed to submit step execution tx with signed tx: {:?}", - &signed_tx - )) - .or(Err("FailedToSubmitTransaction"))? + .map_err(|e| { + pink_extension::error!( + "Failed to construct substrate tx with error: {:?}", + e + ); + "FailedToCreateTransaction" + })?; + + send_transaction(&chain.endpoint, &signed_tx).map_err(|e| { + pink_extension::error!( + "Failed to submit step execution tx with error: {:?}", + e + ); + "FailedToSubmitTransaction" + })? } _ => return Err("UnexpectedCallType"), }, @@ -397,7 +454,7 @@ impl Runner for MultiStep { .source_chain(context) .ok_or("MissingSourceChain")?; let worker_account = AccountInfo::from(context.signer); - let recipient = as_single_step.recipient.clone().ok_or("MissingRecipient")?; + let recipient = as_single_step.recipient.clone(); // Query off-chain indexer directly get the execution result let account = match source_chain.chain_type { @@ -433,7 +490,172 @@ impl Runner for MultiStep { } } +#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, Ord, PartialOrd)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct StepSimulateResult { + pub action_extra_info: ActionExtraInfo, + // Estimate gas cost for the step on EVM chain + pub gas_limit: Option, + // Current suggest gas price on EVM chains + pub gas_price: Option, + // Native asset price in USD + // the USD amount is the value / 10000 + pub native_price_in_usd: u32, + // tx fee will be paied in USD, calculated based on `gas_cost` and `gas_limit` + // the USD amount is the value / 10000 + // potentially use Fixed crates here + pub tx_fee_in_usd: u32, +} + +pub trait Simulate { + fn simulate(&self, context: &Context) -> Result; +} + +impl Simulate for MultiStep { + #[allow(unused_variables)] + fn simulate(&self, context: &Context) -> Result { + let action_extra_info = match self { + MultiStep::Single(step) => context + .get_action_extra_info(&step.source_chain, &step.exe) + .ok_or("NoActionFound")?, + MultiStep::Batch(batch_steps) => { + let mut extra_info = ActionExtraInfo::default(); + for step in batch_steps.iter() { + let single_extra_step = context + .get_action_extra_info(&step.source_chain, &step.exe) + .ok_or("NoActionFound")?; + extra_info.extra_proto_fee_in_usd += single_extra_step.extra_proto_fee_in_usd; + extra_info.const_proto_fee_in_usd += single_extra_step.const_proto_fee_in_usd; + extra_info.percentage_proto_fee = + extra_info.percentage_proto_fee + single_extra_step.percentage_proto_fee; + // Batch txs will happen within same block, so we don't need to accumulate it + extra_info.confirm_time_in_sec = single_extra_step.confirm_time_in_sec; + } + extra_info + } + }; + + let calls = self.derive_calls(context)?; + + let as_single_step = self.as_single_step(); + let chain = as_single_step + .source_chain(context) + .ok_or("MissingSourceChain")?; + let worker_account = AccountInfo::from(context.signer); + + pink_extension::debug!("Start to simulate step with calls: {:?}", &calls); + let (gas_limit, gas_price, native_price_in_usd, tx_fee_in_usd) = match chain.chain_type { + ChainType::Evm => { + let eth = Eth::new(PinkHttp::new(chain.endpoint)); + let handler = Contract::from_json( + eth.clone(), + chain.handler_contract.to_array().into(), + include_bytes!("./abi/handler.json"), + ) + .expect("Bad abi data"); + let options = if as_single_step.spend_asset == chain.native_asset { + Options::with(|opt| { + opt.value = Some(U256::from(as_single_step.spend_amount.unwrap())) + }) + } else { + Options::default() + }; + + // Estiamte gas before submission + let gas = resolve_ready(handler.estimate_gas( + "batchCall", + calls, + worker_account.account20.into(), + options, + )) + .map_err(|e| { + pink_extension::error!("Failed to estimated step gas cost with error: {:?}", e); + "FailedToEstimateGas" + })?; + + let gas_price = resolve_ready(eth.gas_price()).or(Err("FailedToGetGasPrice"))?; + let native_asset_price = crate::price::get_price(&chain.name, &chain.native_asset) + .ok_or("MissingPriceData")?; + ( + Some(gas), + Some(gas_price), + native_asset_price, + // The usd value is the return amount / 10000 + // TODO: here we presume the decimals of all EVM native asset is 18, but we should get it from asset info + ((gas * gas_price * native_asset_price) / 10u128.pow(18)) + .try_into() + .expect("Tx fee overflow"), + ) + } + ChainType::Sub => match calls[0].params.clone() { + CallParams::Sub(SubCall { calldata }) => { + let native_asset_price = + crate::price::get_price(&chain.name, &chain.native_asset) + .ok_or("MissingPriceData")?; + ( + None, + None, + native_asset_price, + // TODO: estimate tx_fee according to calldata size + // 0.001 USD + 100, + ) + } + _ => return Err("UnexpectedCallType"), + }, + }; + + Ok(StepSimulateResult { + action_extra_info, + gas_limit, + gas_price, + native_price_in_usd, + tx_fee_in_usd, + }) + } +} + #[cfg(test)] mod tests { - // use super::*; + use super::*; + use crate::registry::Registry; + use crate::step::StepInput; + + #[test] + #[ignore] + fn test_batch_call_spend_erc20() { + pink_extension_runtime::mock_ext::mock_all_ext(); + + let secret_key = std::env::vars().find(|x| x.0 == "SECRET_KEY"); + let secret_key = secret_key.unwrap().1; + let secret_bytes = hex::decode(secret_key).unwrap(); + let worker_key: [u8; 32] = secret_bytes.to_array(); + + let context = Context { + signer: worker_key, + registry: &Registry::default(), + worker_accounts: vec![], + }; + + let mut step: MultiStep = MultiStepInput::Batch(vec![StepInput { + exe: String::from("astar_evm_arthswap"), + source_chain: String::from("AstarEvm"), + dest_chain: String::from("AstarEvm"), + spend_asset: String::from("0xFFFFFFFF00000000000000010000000000000003"), + receive_asset: String::from("0xAeaaf0e2c81Af264101B9129C00F4440cCF0F720"), + recipient: String::from("0xA29D4E0F035cb50C0d78c8CeBb56Ca292616Ab20"), + } + .try_into() + .unwrap()]) + .try_into() + .unwrap(); + // 0.01 GLMR + step.set_spend(1_000_000_000_000_000); + + println!("Simulate tx: {:?}", step.simulate(&context)); + println!( + "Submit tx to run step: {:?}", + hex::encode(&step.run(2, &context).unwrap()) + ); + } } diff --git a/contracts/index_executor/src/storage.rs b/contracts/index_executor/src/storage.rs index 64da91c9..e723a322 100644 --- a/contracts/index_executor/src/storage.rs +++ b/contracts/index_executor/src/storage.rs @@ -233,9 +233,11 @@ mod tests { status: TaskStatus::Actived, source: "Ethereum".to_string(), amount: 0, + fee: None, claim_nonce: None, - steps: vec![], + claim_tx: None, merged_steps: vec![], + execute_txs: vec![], execute_index: 0, sender: vec![], recipient: vec![], diff --git a/contracts/index_executor/src/task.rs b/contracts/index_executor/src/task.rs index c0038505..77a40750 100644 --- a/contracts/index_executor/src/task.rs +++ b/contracts/index_executor/src/task.rs @@ -2,21 +2,24 @@ use super::account::AccountInfo; use super::context::Context; use super::traits::Runner; use crate::chain::{Chain, ChainType, NonceFetcher}; -use crate::step::{MultiStep, Step}; +use crate::price; +use crate::step::{MultiStep, Simulate as StepSimulate}; use crate::storage::StorageClient; use crate::tx; -use alloc::{collections::BTreeMap, format, string::String, vec, vec::Vec}; -use pink_extension::ResultExt; -use pink_subrpc::{create_transaction, send_transaction, ExtraParam}; + +use alloc::{string::String, vec, vec::Vec}; +use ink::storage::Mapping; use scale::{Decode, Encode}; +use pink_subrpc::{create_transaction, send_transaction, ExtraParam}; + use pink_web3::{ api::{Eth, Namespace}, contract::{Contract, Options}, keys::pink::KeyPair, signing::Key, transports::{resolve_ready, PinkHttp}, - types::H160, + types::{H160, U256}, }; #[derive(Clone, Decode, Encode, Eq, PartialEq, Ord, PartialOrd, Debug)] @@ -26,6 +29,8 @@ pub enum TaskStatus { Actived, /// Task has been initialized, e.g. being applied nonce. Initialized, + /// Indicating that task already been claimed on source chain along with the transaction + Claimed(Vec), /// Task is being executing with step index. /// Transaction can be indentified by worker account nonce on specific chain /// [step_index, worker_nonce] @@ -49,12 +54,16 @@ pub struct Task { pub source: String, // Amount of first spend asset pub amount: u128, + // Fee represented by spend asset calculated when claim + pub fee: Option, // Nonce applied to claim task froom source chain pub claim_nonce: Option, - /// All steps to included in the task - pub steps: Vec, + // Transaction hash of claim operation + pub claim_tx: Option>, /// Steps after merged, those actually will be executed pub merged_steps: Vec, + /// Transaction hash of each step operation + pub execute_txs: Vec>, /// Current step index that is executing pub execute_index: u8, /// Sender address on source chain @@ -73,9 +82,11 @@ impl Default for Task { status: TaskStatus::Actived, source: String::default(), amount: 0, + fee: None, claim_nonce: None, - steps: vec![], + claim_tx: None, merged_steps: vec![], + execute_txs: vec![], execute_index: 0, sender: vec![], recipient: vec![], @@ -93,7 +104,6 @@ impl sp_std::fmt::Debug for Task { .field("source", &self.source) .field("amount", &self.amount) .field("claim_nonce", &self.claim_nonce) - .field("steps", &self.steps) .field("merged_steps", &self.merged_steps) .field("execute_index", &self.execute_index) .field("sender", &hex::encode(&self.sender)) @@ -106,62 +116,34 @@ impl sp_std::fmt::Debug for Task { impl Task { // Initialize task pub fn init(&mut self, context: &Context, client: &StorageClient) -> Result<(), &'static str> { - let (mut free_accounts, free_accounts_doc) = client - .read::>(b"free_accounts")? - .ok_or("StorageNotConfigured")?; - let (mut pending_tasks, pending_tasks_doc) = client - .read::>(b"pending_tasks")? - .ok_or("StorageNotConfigured")?; - - pink_extension::debug!( - "Trying to lookup storage for task {:?} before initializing.", - hex::encode(self.id), - ); - - // Lookup free worker list to find if the worker we expected is free, if it's free remove it or return error - if let Some(index) = free_accounts.iter().position(|&x| x == self.worker) { - free_accounts.remove(index); + if let Some((task, _)) = client + .read::(&self.id) + .map_err(|_| "FailedToReadStorage")? + { pink_extension::debug!( - "Worker {:?} is free, will be applied to this task {:?}.", - hex::encode(self.worker), + "Task {:?} already initialized, will check if it is missed to be added to execute", hex::encode(self.id) ); + if client + .read::(&self.worker) + .map_err(|_| "FailedToReadStorage")? + .is_none() + // Still need to check status in case task actually has completed + && task.status == TaskStatus::Initialized + { + client.insert(&self.worker, &self.id.encode())?; + } } else { - pink_extension::debug!( - "Worker {:?} is busy, try again later for this task {:?}.", - hex::encode(self.worker), - hex::encode(self.id) - ); - return Err("WorkerIsBusy"); - } + // Apply worker nonce for each step in task + self.apply_nonce(0, context, client)?; - // Apply recipient for each step before merged - self.apply_recipient(context)?; - - // Merge steps - self.merge_step(context)?; - - // Apply worker nonce for each step in task - self.apply_nonce(0, context, client)?; - - // TODO: query initial balance of worker account and setup to specific step - self.status = TaskStatus::Initialized; - self.execute_index = 0; - // Push to pending tasks queue - pending_tasks.push(self.id); - // Save task data - client.insert(self.id.as_ref(), &self.encode())?; - - client.update( - b"free_accounts".as_ref(), - &free_accounts.encode(), - free_accounts_doc, - )?; - client.update( - b"pending_tasks".as_ref(), - &pending_tasks.encode(), - pending_tasks_doc, - )?; + // TODO: query initial balance of worker account and setup to specific step + self.status = TaskStatus::Initialized; + self.execute_index = 0; + + client.insert(self.id.as_ref(), &self.encode())?; + client.insert(&self.worker, &self.id.encode())?; + } Ok(()) } @@ -178,16 +160,12 @@ impl Task { hex::encode(self.id), hex::encode(self.worker), ); - self.claim(context)?; + let claim_tx = self.claim(context)?; + self.claim_tx = Some(claim_tx); return Ok(self.status.clone()); } let step_count = self.merged_steps.len(); - // To avoid unnecessary remote check, we check execute_index in advance - if self.execute_index as usize == step_count { - return Ok(TaskStatus::Completed); - } - match self.merged_steps[self.execute_index as usize].has_finished( // An executing task must have nonce applied self.merged_steps[self.execute_index as usize] @@ -197,14 +175,15 @@ impl Task { ) { // If step already executed successfully, execute next step Ok(true) => { - self.execute_index += 1; - self.retry_counter = 0; // If all step executed successfully, set task as `Completed` - if self.execute_index as usize == step_count { + if self.execute_index as usize == (step_count - 1) { self.status = TaskStatus::Completed; return Ok(self.status.clone()); } + self.execute_index += 1; + self.retry_counter = 0; + // Settle last step before execute next step let settle_balance = self.merged_steps[(self.execute_index - 1) as usize].settle(context)?; @@ -231,8 +210,8 @@ impl Task { ); // Since we don't actually understand what happened, retry is the only choice. // To avoid we retry too many times, we involved `retry_counter` - self.retry_counter += 1; - if self.retry_counter < 5 { + if self.retry_counter < 100 { + self.retry_counter += 1; // FIXME: handle returned error let _ = self.execute_step(context, client)?; } else { @@ -266,8 +245,14 @@ impl Task { self.execute_index, nonce ); - self.merged_steps[self.execute_index as usize].run(nonce, context)?; self.status = TaskStatus::Executing(self.execute_index, Some(nonce)); + let execute_tx = self.merged_steps[self.execute_index as usize].run(nonce, context)?; + if self.execute_txs.len() == self.execute_index as usize + 1 { + // Not the first time to execute the step, just replace it with new tx hash + self.execute_txs[self.execute_index as usize] = execute_tx; + } else { + self.execute_txs.push(execute_tx); + } } else { pink_extension::debug!("Step[{:?}] not runnable, return", self.execute_index); } @@ -276,117 +261,10 @@ impl Task { /// Delete task record from on-chain storage pub fn destroy(&mut self, client: &StorageClient) -> Result<(), &'static str> { - let (mut free_accounts, free_accounts_doc) = client - .read::>(b"free_accounts")? - .ok_or("StorageNotConfigured")?; - let (mut pending_tasks, pending_tasks_doc) = client - .read::>(b"pending_tasks")? - .ok_or("StorageNotConfigured")?; - - if let Some((_, task_doc)) = client.read::(&self.id)? { - if let Some(idx) = pending_tasks.iter().position(|id| *id == self.id) { - // Remove from pending tasks queue - pending_tasks.remove(idx); - // Recycle worker account - free_accounts.push(self.worker); - // Delete task data - client.delete(self.id.as_ref(), task_doc)?; - } - client.update( - b"free_accounts".as_ref(), - &free_accounts.encode(), - free_accounts_doc, - )?; - client.update( - b"pending_tasks".as_ref(), - &pending_tasks.encode(), - pending_tasks_doc, - )?; - } - - Ok(()) - } - - pub fn apply_recipient(&mut self, context: &Context) -> Result<(), &'static str> { - let step_count = self.steps.len(); - for (index, step) in self.steps.iter_mut().enumerate() { - let step_source_chain = &step.source_chain(context).ok_or("MissingChain")?; - let step_dest_chain = &step.dest_chain(context).ok_or("MissingChain")?; - let worker_account_info = context.get_account(self.worker).ok_or("WorkerNotFound")?; - - // For sure last step we should put real recipient, or else the recipient could be either - // worker account or handler account - step.recipient = if index == step_count - 1 { - Some(self.recipient.clone()) - } else { - // If bridge to a EVM chain, asset should be send to Handler account to execute the reset of calls - if step.is_bridge_step() { - if step_dest_chain.is_evm_chain() { - Some(step_dest_chain.handler_contract.clone()) - } else { - Some(worker_account_info.account32.to_vec()) - } - } else { - // For non-bridge operatoions, because we don't batch call in Sub chains, so recipient should - // be worker account on source chain, or should be Handler address on source chain - if step_source_chain.is_sub_chain() { - let worker_account = match step_source_chain.chain_type { - ChainType::Evm => worker_account_info.account20.to_vec(), - ChainType::Sub => worker_account_info.account32.to_vec(), - }; - Some(worker_account) - } else { - Some(step_source_chain.handler_contract.clone()) - } - } - }; - } - Ok(()) - } - - pub fn merge_step(&mut self, context: &Context) -> Result<(), &'static str> { - let mut merged_steps: Vec = vec![]; - let mut batch_steps: Vec = vec![]; - - for (index, step) in self.steps.iter().enumerate() { - let step_source_chain = &step.source_chain(context).ok_or("MissingChain")?; - - if step_source_chain.is_sub_chain() { - if !batch_steps.is_empty() { - merged_steps.push(MultiStep::Batch(batch_steps.clone())); - } - merged_steps.push(MultiStep::Single(step.clone())); - // clear queue - batch_steps = vec![]; - } else { - if batch_steps.is_empty() { - batch_steps.push(step.clone()); - } else { - // EVM chain hasn't changed - if step_source_chain.name.to_lowercase() - == batch_steps[batch_steps.len() - 1] - .source_chain - .to_lowercase() - { - batch_steps.push(step.clone()) - } - // EVM chain changed - else { - // Push batch step - merged_steps.push(MultiStep::Batch(batch_steps.clone())); - // Reshipment batch step - batch_steps = vec![step.clone()]; - } - } - // Save it if this is the last step - if index == self.steps.len() - 1 { - merged_steps.push(MultiStep::Batch(batch_steps.clone())); - } - } - } - - // Replace existing task steps - self.merged_steps = merged_steps; + let (_, running_task_doc) = client + .read::(&self.worker)? + .ok_or("TaskNotBeingExecuted")?; + client.delete(&self.worker, running_task_doc)?; Ok(()) } @@ -406,25 +284,34 @@ impl Task { context: &Context, _client: &StorageClient, ) -> Result<(), &'static str> { - let mut nonce_map: BTreeMap = BTreeMap::default(); + let mut nonce_map: Mapping = Mapping::default(); // Apply claim nonce if hasn't claimed if self.claim_nonce.is_none() || !self.has_claimed(context)? { let claim_nonce = self.get_nonce(context, &self.source)?; - nonce_map.insert(self.source.clone(), claim_nonce + 1); + nonce_map.insert(self.source.clone(), &(claim_nonce + 1)); self.claim_nonce = Some(claim_nonce); } // Apply nonce for each step for index in start_index as usize..self.merged_steps.len() { - let source_chain = self.merged_steps[index].as_single_step().source_chain; - let nonce: u64 = match nonce_map.get(&source_chain) { - Some(nonce) => *nonce, - None => self.get_nonce(context, &source_chain)?, - }; + let nonce: u64 = + match nonce_map.get(&self.merged_steps[index].as_single_step().source_chain) { + Some(nonce) => nonce, + None => self.get_nonce( + context, + &self.merged_steps[index].as_single_step().source_chain, + )?, + }; self.merged_steps[index].set_nonce(nonce); // Increase nonce by 1 - nonce_map.insert(source_chain, nonce + 1); + nonce_map.insert( + self.merged_steps[index] + .as_single_step() + .source_chain + .clone(), + &(nonce + 1), + ); } Ok(()) @@ -438,14 +325,7 @@ impl Task { ChainType::Sub => account_info.account32.to_vec(), // ChainType::Unknown => panic!("chain not supported!"), }; - let nonce = chain - .get_nonce(account.clone()) - .log_err(&format!( - "call task get_nonce failed on chain {:?} for account {:?}", - chain.name, - hex::encode(account) - )) - .or(Err("FetchNonceFailed"))?; + let nonce = chain.get_nonce(account).map_err(|_| "FetchNonceFailed")?; Ok(nonce) } @@ -453,15 +333,22 @@ impl Task { let chain = context .registry .get_chain(&self.source) - .ok_or("MissingChain")?; + .map(Ok) + .unwrap_or(Err("MissingChain"))?; let claim_nonce = self.claim_nonce.ok_or("MissingClaimNonce")?; + let fee: u128 = self.calculate_fee(context)?; + if fee >= self.amount { + return Err("TooExpensive"); + } + self.fee = Some(fee); + match chain.chain_type { ChainType::Evm => { - Ok(self.claim_evm_actived_tasks(chain, self.id, context, claim_nonce)?) + Ok(self.claim_evm_actived_tasks(chain, self.id, fee, context, claim_nonce)?) } ChainType::Sub => { - Ok(self.claim_sub_actived_tasks(chain, self.id, context, claim_nonce)?) + Ok(self.claim_sub_actived_tasks(chain, self.id, fee, context, claim_nonce)?) } } } @@ -471,7 +358,8 @@ impl Task { let chain = context .registry .get_chain(&self.source) - .ok_or("MissingChain")?; + .map(Ok) + .unwrap_or(Err("MissingChain"))?; let account = match chain.chain_type { ChainType::Evm => worker_account.account20.to_vec(), ChainType::Sub => worker_account.account32.to_vec(), @@ -495,26 +383,25 @@ impl Task { &mut self, chain: Chain, task_id: TaskId, + fee: u128, context: &Context, nonce: u64, ) -> Result, &'static str> { let handler: H160 = H160::from_slice(&chain.handler_contract); let transport = Eth::new(PinkHttp::new(chain.endpoint)); - let handler = Contract::from_json(transport, handler, crate::constants::HANDLER_ABI) - .log_err(&format!( - "claim_evm_actived_tasks: failed to instatiate handler {:?} on {:?}", - handler, &chain.name - )) - .or(Err("ConstructContractFailed"))?; + let handler = Contract::from_json(transport, handler, include_bytes!("./abi/handler.json")) + .map_err(|_| "ConstructContractFailed")?; let worker = KeyPair::from(context.signer); // We call claimAndBatchCall so that first step will be executed along with the claim operation let first_step = &mut self.merged_steps[0]; - first_step.set_spend(self.amount); + // FIXME: We definitely need to consider the minimal amount allowed + first_step.set_spend(self.amount - fee); first_step.sync_origin_balance(context)?; - let params = (task_id, first_step.derive_calls(context)?); - pink_extension::info!("claimAndBatchCall params {:?}", ¶ms); + let calls = first_step.derive_calls(context)?; + pink_extension::debug!("Calls will be executed along with claim: {:?}", &calls); + let params = (task_id, U256::from(fee), calls); // Estiamte gas before submission let gas = resolve_ready(handler.estimate_gas( "claimAndBatchCall", @@ -522,44 +409,55 @@ impl Task { worker.address(), Options::default(), )) - .log_err(&format!( - "claim_evm_actived_tasks: failed to estimate gas cost with params: {:?}", - ¶ms - )) - .or(Err("GasEstimateFailed"))?; + .map_err(|e| { + pink_extension::error!( + "claimAndBatchCall: failed to estimate gas cost with error {:?}", + &e + ); + "GasEstimateFailed" + })?; // Submit the claim transaction let tx_id = resolve_ready(handler.signed_call( "claimAndBatchCall", params, Options::with(|opt| { - opt.gas = Some(gas); + // Give 50% gas for potentially gas exceeding + opt.gas = Some(gas * U256::from(15) / U256::from(10)); opt.nonce = Some(nonce.into()); }), worker, )) - .log_err(&format!( - "claim_evm_actived_tasks: failed to submit tx with nonce: {:?}", - &nonce - )) - .or(Err("ClaimSubmitFailed"))?; + .map_err(|e| { + pink_extension::error!("claimAndBatchCall: failed to submit tx with error {:?}", &e); + "ClaimSubmitFailed" + })? + .as_bytes() + .to_vec(); // Merge nonce to let check for first step work properly first_step.set_nonce(self.claim_nonce.unwrap()); + // Set first step execution transaction hash + if self.execute_txs.is_empty() { + self.execute_txs.push(tx_id.clone()); + } else { + self.execute_txs[0] = tx_id.clone(); + } pink_extension::info!( "Submit transaction to claim task {:?} on {:?}, tx id: {:?}", hex::encode(task_id), &chain.name, - hex::encode(tx_id.clone().as_bytes()) + hex::encode(&tx_id) ); - Ok(tx_id.as_bytes().to_vec()) + Ok(tx_id) } fn claim_sub_actived_tasks( - &self, + &mut self, chain: Chain, task_id: TaskId, + fee: u128, context: &Context, nonce: u64, ) -> Result, &'static str> { @@ -574,26 +472,61 @@ impl Task { .ok_or("ClaimMissingPalletId")?, // Call index of `claim_task` 0x03u8, - task_id, + (task_id, fee), ExtraParam { tip: 0, nonce: Some(nonce), era: None, }, ) - .log_err("claim_sub_actived_tasks: failed to create signed transaction") - .or(Err("ClaimInvalidSignature"))?; - let tx_id = send_transaction(&chain.endpoint, &signed_tx) - .log_err("claim_sub_actived_tasks: failed to submit transaction to execute task claim") - .or(Err("ClaimSubmitFailed"))?; + .map_err(|_| "ClaimInvalidSignature")?; + let tx_id = + send_transaction(&chain.endpoint, &signed_tx).map_err(|_| "ClaimSubmitFailed")?; pink_extension::info!( "Submit transaction to claim task {:?} on ${:?}, tx id: {:?}", hex::encode(task_id), &chain.name, hex::encode(tx_id.clone()) ); + let first_step = &mut self.merged_steps[0]; + first_step.set_spend(self.amount); Ok(tx_id) } + + fn calculate_fee(&self, context: &Context) -> Result { + let mut fee_in_usd: u32 = 0; + for step in self.merged_steps.iter() { + let mut simulate_step = step.clone(); // A minimal amount + let asset_location = simulate_step.as_single_step().spend_asset; + let source_chain = simulate_step.as_single_step().source_chain; + let asset_info = context + .registry + .get_asset(&source_chain, &asset_location) + .ok_or("MissingAssetInfo")?; + // Set spend asset 0.0001 + simulate_step.set_spend(10u128.pow(asset_info.decimals as u32) / 10000); + let step_simulate_result = simulate_step.simulate(context).map_err(|e| { + pink_extension::error!("Some error occurred when simulating: {:?}", e); + "SimulateRrror" + })?; + + // We only need to collect tx fee and extra protocol fee, those fee are actually paied by worker + // during execution + fee_in_usd += step_simulate_result.tx_fee_in_usd + + step_simulate_result + .action_extra_info + .extra_proto_fee_in_usd; + } + + let asset_location = self.merged_steps[0].as_single_step().spend_asset; + let asset_info = context + .registry + .get_asset(&self.source, &asset_location) + .ok_or("MissingAssetInfo")?; + let asset_price = + price::get_price(&self.source, &asset_location).ok_or("MissingPriceData")?; + Ok(10u128.pow(asset_info.decimals as u32) * fee_in_usd as u128 / asset_price as u128) + } } #[cfg(test)] @@ -602,12 +535,11 @@ mod tests { use crate::account::AccountInfo; use crate::chain::{BalanceFetcher, Chain, ChainType}; use crate::registry::Registry; - use crate::step::StepJson; + use crate::step::StepInput; use crate::task_fetcher::ActivedTaskFetcher; use crate::utils::ToArray; use dotenv::dotenv; use hex_literal::hex; - use pink_web3::contract::tokens::Tokenize; use primitive_types::H160; #[test] @@ -618,8 +550,9 @@ mod tests { pink_extension_runtime::mock_ext::mock_all_ext(); + let client: StorageClient = StorageClient::new("url".to_string(), "key".to_string()); let worker_address: H160 = hex!("f60dB2d02af3f650798b59CB6D453b78f2C1BC90").into(); - let task = ActivedTaskFetcher { + let _task = ActivedTaskFetcher { chain: Chain { id: 0, name: String::from("Ethereum"), @@ -637,15 +570,9 @@ mod tests { account32: [0; 32], }, } - .fetch_task() + .fetch_task(&client) .unwrap() .unwrap(); - assert_eq!(task.steps.len(), 3); - assert_eq!( - task.steps[1].spend_amount.unwrap(), - 100_000_000_000_000_000_000 as u128 - ); - assert_eq!(task.steps[2].spend_amount.unwrap(), 12_000_000); } #[test] @@ -678,6 +605,7 @@ mod tests { signer: mock_worker_key, registry: &Registry { chains: vec![goerli], + assets: vec![], }, worker_accounts: vec![], }; @@ -707,11 +635,12 @@ mod tests { dotenv().ok(); pink_extension_runtime::mock_ext::mock_all_ext(); + let client: StorageClient = StorageClient::new("url".to_string(), "key".to_string()); // Worker public key let worker_key: [u8; 32] = hex!("2eaaf908adda6391e434ff959973019fb374af1076edd4fec55b5e6018b1a955").into(); // We already deposit task with scritps/sub-depopsit.js - let task = ActivedTaskFetcher { + let _task = ActivedTaskFetcher { chain: Chain { id: 0, name: String::from("Khala"), @@ -727,15 +656,9 @@ mod tests { account32: worker_key, }, } - .fetch_task() + .fetch_task(&client) .unwrap() .unwrap(); - assert_eq!(task.steps.len(), 3); - assert_eq!(task.steps[1].spend_amount.unwrap(), 301_000_000_000_000); - assert_eq!( - task.steps[2].spend_amount.unwrap(), - 1_000_000_000_000_000_000 as u128 - ); } #[test] @@ -768,6 +691,7 @@ mod tests { signer: mock_worker_prv_key, registry: &Registry { chains: vec![khala.clone()], + assets: vec![], }, worker_accounts: vec![], }; @@ -843,13 +767,6 @@ mod tests { // Create storage client let client: StorageClient = StorageClient::new("url".to_string(), "key".to_string()); - // Setup initial worker accounts to storage - let accounts: Vec<[u8; 32]> = worker_accounts - .clone() - .into_iter() - .map(|account| account.account32.clone()) - .collect(); - client.insert(b"free_accounts", &accounts.encode()).unwrap(); // Fetch actived task from chain let pre_mock_executor_address: H160 = @@ -872,10 +789,9 @@ mod tests { account32: [0; 32], }, } - .fetch_task() + .fetch_task(&client) .unwrap() .unwrap(); - assert_eq!(task.steps.len(), 3); // Init task assert_eq!(task.init( @@ -904,6 +820,7 @@ mod tests { tx_indexer_url: Default::default(), } ], + assets: vec![], }, worker_accounts: worker_accounts.clone(), }, @@ -914,398 +831,129 @@ mod tests { std::thread::sleep(std::time::Duration::from_millis(3000)); // Now let's query if the task is exist in rollup storage with another rollup client - let another_client = StorageClient::new("another url".to_string(), "key".to_string()); + let another_client: StorageClient = + StorageClient::new("another url".to_string(), "key".to_string()); let onchain_task = another_client.read::(&task.id).unwrap().unwrap().0; assert_eq!(onchain_task.status, TaskStatus::Initialized); assert_eq!( onchain_task.worker, worker_accounts.last().unwrap().account32 ); - assert_eq!(onchain_task.steps.len(), 3); - assert_eq!(onchain_task.steps[0].nonce, Some(0)); - assert_eq!(onchain_task.steps[1].nonce, Some(1)); - assert_eq!(onchain_task.steps[2].nonce, Some(2)); } - fn build_steps() -> Vec { + fn build_multi_steps() -> Vec { vec![ - // moonbeam_stellaswap - StepJson { - exe_type: String::from("swap"), - exe: String::from("moonbeam_stellaswap"), - source_chain: String::from("Moonbeam"), - dest_chain: String::from("Moonbeam"), - spend_asset: String::from("0xAcc15dC74880C9944775448304B263D191c6077F"), - receive_asset: String::from("0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080"), - } - .try_into() - .unwrap(), - // moonbeam_stellaswap - StepJson { - exe_type: String::from("swap"), - exe: String::from("moonbeam_stellaswap"), - source_chain: String::from("Moonbeam"), - dest_chain: String::from("Moonbeam"), - spend_asset: String::from("0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080"), - receive_asset: String::from("0xFFFfFfFf63d24eCc8eB8a7b5D0803e900F7b6cED"), - } - .try_into() - .unwrap(), - // moonbeam_bridge_to_phala - StepJson { - exe_type: String::from("bridge"), - exe: String::from("moonbeam_bridge_to_phala"), - source_chain: String::from("Moonbeam"), - dest_chain: String::from("Phala"), - spend_asset: String::from("0xFFFfFfFf63d24eCc8eB8a7b5D0803e900F7b6cED"), - receive_asset: String::from("0x0000"), - } - .try_into() - .unwrap(), + MultiStep::Batch(vec![ + // moonbeam_stellaswap + StepInput { + exe: String::from("moonbeam_stellaswap"), + source_chain: String::from("Moonbeam"), + dest_chain: String::from("Moonbeam"), + spend_asset: String::from("0xAcc15dC74880C9944775448304B263D191c6077F"), + receive_asset: String::from("0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080"), + recipient: String::from("0xB8D20dfb8c3006AA17579887ABF719DA8bDf005B"), + } + .try_into() + .unwrap(), + // moonbeam_stellaswap + StepInput { + exe: String::from("moonbeam_stellaswap"), + source_chain: String::from("Moonbeam"), + dest_chain: String::from("Moonbeam"), + spend_asset: String::from("0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080"), + receive_asset: String::from("0xFFFfFfFf63d24eCc8eB8a7b5D0803e900F7b6cED"), + recipient: String::from("0xB8D20dfb8c3006AA17579887ABF719DA8bDf005B"), + } + .try_into() + .unwrap(), + // moonbeam_bridge_to_phala + StepInput { + exe: String::from("moonbeam_bridge_to_phala"), + source_chain: String::from("Moonbeam"), + dest_chain: String::from("Phala"), + spend_asset: String::from("0xFFFfFfFf63d24eCc8eB8a7b5D0803e900F7b6cED"), + receive_asset: String::from("0x0000"), + recipient: String::from( + "0x1111111111111111111111111111111111111111111111111111111111111111", + ), + } + .try_into() + .unwrap(), + ]), // phala_bridge_to_astar - StepJson { - exe_type: String::from("bridge"), - exe: String::from("phala_bridge_to_astar"), - source_chain: String::from("Phala"), - dest_chain: String::from("Astar"), - spend_asset: String::from("0x0000"), - receive_asset: String::from("0x010100cd1f"), - } - .try_into() - .unwrap(), - // astar_bridge_to_astar_evm - StepJson { - exe_type: String::from("bridge"), - exe: String::from("astar_bridge_to_astarevm"), - source_chain: String::from("Astar"), - dest_chain: String::from("AstarEvm"), - spend_asset: String::from("0x010100cd1f"), - receive_asset: String::from("0xFFFFFFFF00000000000000010000000000000006"), - } - .try_into() - .unwrap(), - // astar_arthswap - StepJson { - exe_type: String::from("swap"), - exe: String::from("astar_evm_arthswap"), - source_chain: String::from("AstarEvm"), - dest_chain: String::from("AstarEvm"), - spend_asset: String::from("0xFFFFFFFF00000000000000010000000000000006"), - receive_asset: String::from("0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF"), - } - .try_into() - .unwrap(), - // astar_arthswap - StepJson { - exe_type: String::from("swap"), - exe: String::from("astar_evm_arthswap"), - source_chain: String::from("AstarEvm"), - dest_chain: String::from("AstarEvm"), - spend_asset: String::from("0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF"), - receive_asset: String::from("0xFFFFFFFF00000000000000010000000000000003"), - } - .try_into() - .unwrap(), + MultiStep::Single( + StepInput { + exe: String::from("phala_bridge_to_astar"), + source_chain: String::from("Phala"), + dest_chain: String::from("Astar"), + spend_asset: String::from("0x0000"), + receive_asset: String::from("0x010100cd1f"), + recipient: String::from( + "0x1111111111111111111111111111111111111111111111111111111111111111", + ), + } + .try_into() + .unwrap(), + ), + MultiStep::Single( + // astar_bridge_to_astar_evm + StepInput { + exe: String::from("astar_bridge_to_astarevm"), + source_chain: String::from("Astar"), + dest_chain: String::from("AstarEvm"), + spend_asset: String::from("0x010100cd1f"), + receive_asset: String::from("0xFFFFFFFF00000000000000010000000000000006"), + recipient: String::from("0xbEA1C40ecf9c4603ec25264860B9b6623Ff733F5"), + } + .try_into() + .unwrap(), + ), + MultiStep::Batch(vec![ + // astar_arthswap + StepInput { + exe: String::from("astar_evm_arthswap"), + source_chain: String::from("AstarEvm"), + dest_chain: String::from("AstarEvm"), + spend_asset: String::from("0xFFFFFFFF00000000000000010000000000000006"), + receive_asset: String::from("0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF"), + recipient: String::from("0xbEA1C40ecf9c4603ec25264860B9b6623Ff733F5"), + } + .try_into() + .unwrap(), + // astar_arthswap + StepInput { + exe: String::from("astar_evm_arthswap"), + source_chain: String::from("AstarEvm"), + dest_chain: String::from("AstarEvm"), + spend_asset: String::from("0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF"), + receive_asset: String::from("0xFFFFFFFF00000000000000010000000000000003"), + recipient: String::from("0xA29D4E0F035cb50C0d78c8CeBb56Ca292616Ab20"), + } + .try_into() + .unwrap(), + ]), ] } - #[test] - fn test_apply_recipient() { - dotenv().ok(); - pink_extension_runtime::mock_ext::mock_all_ext(); - - let worker_key = [0x11; 32]; - let steps = build_steps(); - let mut task = Task { - id: [1; 32], - worker: AccountInfo::from(worker_key).account32, - status: TaskStatus::Actived, - source: "Moonbeam".to_string(), - amount: 0, - claim_nonce: None, - steps, - merged_steps: vec![], - execute_index: 0, - sender: vec![], - recipient: hex::decode("A29D4E0F035cb50C0d78c8CeBb56Ca292616Ab20").unwrap(), - retry_counter: 0, - }; - let context = Context { - signer: worker_key, - worker_accounts: vec![AccountInfo::from(worker_key)], - registry: &Registry::new(), - }; - - task.apply_recipient(&context).unwrap(); - - // moonbeam_stellaswap - assert_eq!( - task.steps[0].recipient, - Some( - context - .registry - .get_chain(&String::from("Moonbeam")) - .unwrap() - .handler_contract - ) - ); - // moonbeam_stellaswap - assert_eq!( - task.steps[1].recipient, - Some( - context - .registry - .get_chain(&String::from("Moonbeam")) - .unwrap() - .handler_contract - ) - ); - // moonbeam_bridge_to_phala - assert_eq!( - task.steps[2].recipient, - Some(AccountInfo::from(worker_key).account32.to_vec()) - ); - // phala_bridge_to_astar - assert_eq!( - task.steps[3].recipient, - Some(AccountInfo::from(worker_key).account32.to_vec()) - ); - // astar_bridge_to_astar_evm - assert_eq!( - task.steps[4].recipient, - Some( - context - .registry - .get_chain(&String::from("AstarEvm")) - .unwrap() - .handler_contract - ) - ); - // astar_arthswap - assert_eq!( - task.steps[5].recipient, - Some( - context - .registry - .get_chain(&String::from("AstarEvm")) - .unwrap() - .handler_contract - ) - ); - // astar_arthswap - assert_eq!(task.steps[6].recipient, Some(task.recipient)); - } - - #[test] - fn test_merge_step() { - dotenv().ok(); - pink_extension_runtime::mock_ext::mock_all_ext(); - - let worker_key = [0x11; 32]; - let steps = build_steps(); - let mut task = Task { - id: [1; 32], - worker: AccountInfo::from(worker_key).account32, - status: TaskStatus::Actived, - source: "Moonbeam".to_string(), - amount: 0, - claim_nonce: None, - steps: steps.clone(), - merged_steps: vec![], - execute_index: 0, - sender: vec![], - recipient: hex::decode("A29D4E0F035cb50C0d78c8CeBb56Ca292616Ab20").unwrap(), - retry_counter: 0, - }; - let context = Context { - signer: worker_key, - worker_accounts: vec![AccountInfo::from(worker_key)], - registry: &Registry::new(), - }; - - task.apply_recipient(&context).unwrap(); - task.merge_step(&context).unwrap(); - - assert_eq!(task.merged_steps.len(), 4); - assert!(task.merged_steps[0].is_batch_step()); - match &task.merged_steps[0] { - MultiStep::Batch(batch_steps) => { - assert_eq!(batch_steps.len(), 3); - assert_eq!(batch_steps[0].source_chain, "Moonbeam"); - assert_eq!(batch_steps[1].source_chain, "Moonbeam"); - assert_eq!(batch_steps[2].dest_chain, "Phala"); - } - _ => assert!(false), - }; - assert!(task.merged_steps[1].is_single_step()); - match &task.merged_steps[1] { - MultiStep::Single(step) => { - assert_eq!(step.source_chain, "Phala"); - assert_eq!(step.dest_chain, "Astar"); - } - _ => assert!(false), - }; - assert!(task.merged_steps[2].is_single_step()); - match &task.merged_steps[2] { - MultiStep::Single(step) => { - assert_eq!(step.source_chain, "Astar"); - assert_eq!(step.dest_chain, "AstarEvm"); - } - _ => assert!(false), - }; - assert!(task.merged_steps[3].is_batch_step()); - match &task.merged_steps[3] { - MultiStep::Batch(batch_steps) => { - assert_eq!(batch_steps.len(), 2); - assert_eq!(batch_steps[0].source_chain, "AstarEvm"); - assert_eq!(batch_steps[1].source_chain, "AstarEvm"); - } - _ => assert!(false), - }; - - let mut task1 = Task { - id: [1; 32], - worker: AccountInfo::from(worker_key).account32, - status: TaskStatus::Actived, - source: "Moonbeam".to_string(), - amount: 0, - claim_nonce: None, - steps: steps.clone().as_slice()[3..].to_vec(), - merged_steps: vec![], - execute_index: 0, - sender: vec![], - recipient: hex::decode("A29D4E0F035cb50C0d78c8CeBb56Ca292616Ab20").unwrap(), - retry_counter: 0, - }; - - task1.apply_recipient(&context).unwrap(); - task1.merge_step(&context).unwrap(); - assert_eq!(task1.merged_steps.len(), 3); - assert!(task1.merged_steps[0].is_single_step()); - match &task1.merged_steps[0] { - MultiStep::Single(step) => { - assert_eq!(step.source_chain, "Phala"); - assert_eq!(step.dest_chain, "Astar"); - } - _ => assert!(false), - }; - assert!(task1.merged_steps[1].is_single_step()); - match &task1.merged_steps[1] { - MultiStep::Single(step) => { - assert_eq!(step.source_chain, "Astar"); - assert_eq!(step.dest_chain, "AstarEvm"); - } - _ => assert!(false), - }; - assert!(task1.merged_steps[2].is_batch_step()); - match &task1.merged_steps[2] { - MultiStep::Batch(batch_steps) => { - assert_eq!(batch_steps.len(), 2); - assert_eq!(batch_steps[0].source_chain, "AstarEvm"); - assert_eq!(batch_steps[1].source_chain, "AstarEvm"); - } - _ => assert!(false), - }; - - let mut task2 = Task { - id: [1; 32], - worker: AccountInfo::from(worker_key).account32, - status: TaskStatus::Actived, - source: "Moonbeam".to_string(), - amount: 0, - claim_nonce: None, - steps: steps.clone().as_slice()[..4].to_vec(), - merged_steps: vec![], - execute_index: 0, - sender: vec![], - recipient: hex::decode("A29D4E0F035cb50C0d78c8CeBb56Ca292616Ab20").unwrap(), - retry_counter: 0, - }; - - task2.apply_recipient(&context).unwrap(); - task2.merge_step(&context).unwrap(); - - assert_eq!(task2.merged_steps.len(), 2); - assert!(task2.merged_steps[0].is_batch_step()); - match &task2.merged_steps[0] { - MultiStep::Batch(batch_steps) => { - assert_eq!(batch_steps.len(), 3); - assert_eq!(batch_steps[0].source_chain, "Moonbeam"); - assert_eq!(batch_steps[1].source_chain, "Moonbeam"); - assert_eq!(batch_steps[2].dest_chain, "Phala"); - } - _ => assert!(false), - }; - assert!(task2.merged_steps[1].is_single_step()); - match &task2.merged_steps[1] { - MultiStep::Single(step) => { - assert_eq!(step.source_chain, "Phala"); - assert_eq!(step.dest_chain, "Astar"); - } - _ => assert!(false), - }; - - let mut task3 = Task { - id: [1; 32], - worker: AccountInfo::from(worker_key).account32, - status: TaskStatus::Actived, - source: "Moonbeam".to_string(), - amount: 0, - claim_nonce: None, - steps: [ - &steps.clone().as_slice()[..3], - &steps.clone().as_slice()[5..], - ] - .concat() - .to_vec(), - merged_steps: vec![], - execute_index: 0, - sender: vec![], - recipient: hex::decode("A29D4E0F035cb50C0d78c8CeBb56Ca292616Ab20").unwrap(), - retry_counter: 0, - }; - - task3.apply_recipient(&context).unwrap(); - task3.merge_step(&context).unwrap(); - - assert_eq!(task3.merged_steps.len(), 2); - assert!(task3.merged_steps[0].is_batch_step()); - match &task3.merged_steps[0] { - MultiStep::Batch(batch_steps) => { - assert_eq!(batch_steps.len(), 3); - assert_eq!(batch_steps[0].source_chain, "Moonbeam"); - assert_eq!(batch_steps[1].source_chain, "Moonbeam"); - assert_eq!(batch_steps[2].dest_chain, "Phala"); - } - _ => assert!(false), - }; - assert!(task3.merged_steps[1].is_batch_step()); - match &task3.merged_steps[1] { - MultiStep::Batch(batch_steps) => { - assert_eq!(batch_steps.len(), 2); - assert_eq!(batch_steps[0].source_chain, "AstarEvm"); - assert_eq!(batch_steps[1].source_chain, "AstarEvm"); - } - _ => assert!(false), - }; - } - #[test] fn test_calldata_generation() { dotenv().ok(); pink_extension_runtime::mock_ext::mock_all_ext(); + let merged_steps = build_multi_steps(); + let worker_key = [0x11; 32]; - let steps = build_steps(); let mut task = Task { id: [1; 32], worker: AccountInfo::from(worker_key).account32, status: TaskStatus::Actived, source: "Moonbeam".to_string(), amount: 0xf0f1f2f3f4f5f6f7f8f9, + fee: None, claim_nonce: None, - steps: steps.clone(), - merged_steps: vec![], + claim_tx: None, + merged_steps: merged_steps.clone(), + execute_txs: vec![], execute_index: 0, sender: vec![], recipient: hex::decode("A29D4E0F035cb50C0d78c8CeBb56Ca292616Ab20").unwrap(), @@ -1317,9 +965,6 @@ mod tests { registry: &Registry::new(), }; - task.apply_recipient(&context).unwrap(); - task.merge_step(&context).unwrap(); - let mut calls = vec![]; for step in task.merged_steps.iter_mut() { @@ -1327,306 +972,82 @@ mod tests { step.set_spend(0xf0f1f2f3f4f5f6f7f8f9); calls.append(&mut step.derive_calls(&context).unwrap()); } - assert_eq!(calls.len(), 2 * 3 + 1 + 1 + 2 * 2); + assert_eq!(calls.len(), 3 + 1 + 1 + 2); // Origin Step means Steps before merge // ========== First Merged Step ============= - // calls[0] and calls[1] build according to origin Step 0, + // calls[0] build according to origin Step 0, // and origin Step 0 don't relay any previous steps happened // on the same chain assert_eq!(calls[0].input_call, Some(0)); - assert_eq!(calls[1].input_call, Some(0)); - // calls[2] and calls[3] build according to origin Step 1, + // calls[1] build according to origin Step 1, // and origin Step 1 relay Step 0 as input, so take last call // of Step 0 as input call - assert_eq!(calls[2].input_call, Some(1)); - assert_eq!(calls[3].input_call, Some(1)); - // calls[4] and calls[5] build according to origin Step 2, + assert_eq!(calls[1].input_call, Some(0)); + // calls[2] build according to origin Step 2, // and origin Step 2 relay Step 1 as input, so take last call // of Step 1 as input call - assert_eq!(calls[4].input_call, Some(3)); - assert_eq!(calls[5].input_call, Some(3)); + assert_eq!(calls[2].input_call, Some(1)); // ========== Second Merged Step ============= - // calls[0] and calls[1] build according to origin Step 5, - // and origin Step 5 don't relay any previous steps heppened + // calls[5] build according to origin Step 5, + // and origin Step 5 don't relay any previous steps happened // on the same chain - assert_eq!(calls[0].input_call, Some(0)); - assert_eq!(calls[1].input_call, Some(0)); - // calls[2] and calls[3] build according to origin Step 6, + assert_eq!(calls[5].input_call, Some(0)); + // calls[6] build according to origin Step 6, // and origin Step 6 relay Step 5 as input, so take last call // of Step 5 as input call - assert_eq!(calls[2].input_call, Some(1)); - assert_eq!(calls[3].input_call, Some(1)); + assert_eq!(calls[6].input_call, Some(0)); } #[test] - fn test_calldata() { + #[ignore] + fn test_fee_calculation() { dotenv().ok(); pink_extension_runtime::mock_ext::mock_all_ext(); - use crate::call::{Call, CallParams, EvmCall}; - use pink_web3::types::{Address, U256}; + let secret_key = std::env::vars().find(|x| x.0 == "SECRET_KEY"); + let secret_key = secret_key.unwrap().1; + let secret_bytes = hex::decode(secret_key).unwrap(); + let worker_key: [u8; 32] = secret_bytes.to_array(); - let handler: H160 = - H160::from_slice(&hex::decode("B30A27eE79514614dc363CE0aABb0B939b9deAeD").unwrap()); - let transport = Eth::new(PinkHttp::new("https://rpc.api.moonbeam.network")); - let handler = - Contract::from_json(transport, handler, crate::constants::HANDLER_ABI).unwrap(); - let task_id: [u8; 32] = - hex::decode("1125000000000000000000000000000000000000000000000000000000000000") - .unwrap() - .to_array(); - // We call claimAndBatchCall so that first step will be executed along with the claim operation + let context = Context { + signer: worker_key, + registry: &Registry::default(), + worker_accounts: vec![], + }; + let mut task = Task::default(); + task.id = hex::decode("0000000000000000000000000000000000000000000000000000000000000001") + .unwrap() + .to_array(); + task.merged_steps = vec![MultiStep::Batch(vec![ + StepInput { + exe: String::from("moonbeam_nativewrapper"), + source_chain: String::from("Moonbeam"), + dest_chain: String::from("Moonbeam"), + spend_asset: String::from("0x0000000000000000000000000000000000000802"), + receive_asset: String::from("0xacc15dc74880c9944775448304b263d191c6077f"), + recipient: String::from("0x8351BAE38E3D590063544A99A95BF4fe5379110b"), + } + .try_into() + .unwrap(), + StepInput { + exe: String::from("moonbeam_stellaswap"), + source_chain: String::from("Moonbeam"), + dest_chain: String::from("Moonbeam"), + spend_asset: String::from("0xacc15dc74880c9944775448304b263d191c6077f"), + receive_asset: String::from("0xffffffff1fcacbd218edc0eba20fc2308c778080"), + recipient: String::from("0xa29d4e0f035cb50c0d78c8cebb56ca292616ab20"), + } + .try_into() + .unwrap(), + ])]; + task.source = "Moonbeam".to_string(); - let params = ( - task_id, - vec![ - Call { - params: CallParams::Evm(EvmCall { - target: Address::from_slice( - hex::decode("acc15dc74880c9944775448304b263d191c6077f") - .unwrap() - .as_slice(), - ), - calldata: vec![ - 9, 94, 167, 179, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 8, 90, 9, - 211, 13, 111, 140, 78, 207, 110, 225, 1, 32, 209, 132, 115, 131, 187, - 87, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 13, 224, 182, 179, 167, 100, 0, 0, - ], - value: U256::from(0), - - need_settle: false, - update_offset: U256::from(36), - update_len: U256::from(32), - spend_asset: Address::from_slice( - hex::decode("acc15dc74880c9944775448304b263d191c6077f") - .unwrap() - .as_slice(), - ), - spend_amount: U256::from(1000000000000000000_u128), - receive_asset: Address::from_slice( - hex::decode("acc15dc74880c9944775448304b263d191c6077f") - .unwrap() - .as_slice(), - ), - }), - input_call: Some(0), - call_index: Some(0), - }, - Call { - params: CallParams::Evm(EvmCall { - target: Address::from_slice( - hex::decode("70085a09d30d6f8c4ecf6ee10120d1847383bb57") - .unwrap() - .as_slice(), - ), - calldata: vec![ - 56, 237, 23, 57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 13, 224, 182, 179, 167, 100, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, - 94, 168, 104, 4, 32, 15, 128, 193, 110, 168, 237, 220, 60, 116, 154, - 84, 169, 195, 125, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 14, 240, 187, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 172, 193, 93, 199, 72, 128, - 201, 148, 71, 117, 68, 131, 4, 178, 99, 209, 145, 198, 7, 127, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 31, 202, 203, 210, 24, - 237, 192, 235, 162, 15, 194, 48, 140, 119, 128, 128, - ], - value: U256::from(0), - - need_settle: true, - update_offset: U256::from(4), - update_len: U256::from(32), - spend_asset: Address::from_slice( - hex::decode("acc15dc74880c9944775448304b263d191c6077f") - .unwrap() - .as_slice(), - ), - spend_amount: U256::from(1000000000000000000_u128), - receive_asset: Address::from_slice( - hex::decode("ffffffff1fcacbd218edc0eba20fc2308c778080") - .unwrap() - .as_slice(), - ), - }), - input_call: Some(0), - call_index: Some(1), - }, - Call { - params: CallParams::Evm(EvmCall { - target: Address::from_slice( - hex::decode("ffffffff1fcacbd218edc0eba20fc2308c778080") - .unwrap() - .as_slice(), - ), - calldata: vec![ - 9, 94, 167, 179, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 8, 90, 9, - 211, 13, 111, 140, 78, 207, 110, 225, 1, 32, 209, 132, 115, 131, 187, - 87, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ], - value: U256::from(0), - - need_settle: false, - update_offset: U256::from(36), - update_len: U256::from(32), - spend_asset: Address::from_slice( - hex::decode("ffffffff1fcacbd218edc0eba20fc2308c778080") - .unwrap() - .as_slice(), - ), - spend_amount: U256::from(0), - receive_asset: Address::from_slice( - hex::decode("ffffffff1fcacbd218edc0eba20fc2308c778080") - .unwrap() - .as_slice(), - ), - }), - input_call: Some(1), - call_index: Some(2), - }, - Call { - params: CallParams::Evm(EvmCall { - target: Address::from_slice( - hex::decode("70085a09d30d6f8c4ecf6ee10120d1847383bb57") - .unwrap() - .as_slice(), - ), - calldata: vec![ - 56, 237, 23, 57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 94, 168, - 104, 4, 32, 15, 128, 193, 110, 168, 237, 220, 60, 116, 154, 84, 169, - 195, 125, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 101, 14, 240, 189, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 31, 202, 203, 210, - 24, 237, 192, 235, 162, 15, 194, 48, 140, 119, 128, 128, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 99, 210, 78, 204, 142, 184, - 167, 181, 208, 128, 62, 144, 15, 123, 108, 237, - ], - value: U256::from(0), - - need_settle: true, - update_offset: U256::from(4), - update_len: U256::from(32), - spend_asset: Address::from_slice( - hex::decode("ffffffff1fcacbd218edc0eba20fc2308c778080") - .unwrap() - .as_slice(), - ), - spend_amount: U256::from(0), - receive_asset: Address::from_slice( - hex::decode("ffffffff63d24ecc8eb8a7b5d0803e900f7b6ced") - .unwrap() - .as_slice(), - ), - }), - input_call: Some(1), - call_index: Some(3), - }, - Call { - params: CallParams::Evm(EvmCall { - target: Address::from_slice( - hex::decode("ffffffff63d24ecc8eb8a7b5d0803e900f7b6ced") - .unwrap() - .as_slice(), - ), - calldata: vec![ - 9, 94, 167, 179, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ], - value: U256::from(0), - - need_settle: false, - update_offset: U256::from(36), - update_len: U256::from(32), - spend_asset: Address::from_slice( - hex::decode("ffffffff63d24ecc8eb8a7b5d0803e900f7b6ced") - .unwrap() - .as_slice(), - ), - spend_amount: U256::from(0), - receive_asset: Address::from_slice( - hex::decode("ffffffff63d24ecc8eb8a7b5d0803e900f7b6ced") - .unwrap() - .as_slice(), - ), - }), - input_call: Some(3), - call_index: Some(4), - }, - Call { - params: CallParams::Evm(EvmCall { - target: Address::from_slice( - hex::decode("0000000000000000000000000000000000000804") - .unwrap() - .as_slice(), - ), - calldata: vec![ - 185, 248, 19, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, - 255, 99, 210, 78, 204, 142, 184, 167, 181, 208, 128, 62, 144, 15, 123, - 108, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 1, 101, 160, 188, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 7, 243, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 34, 1, 4, 219, 160, 103, 127, 194, 116, 255, 172, 204, 15, 161, - 3, 10, 102, 177, 113, 209, 218, 146, 38, 210, 187, 157, 21, 38, 84, - 230, 167, 70, 242, 118, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ], - value: U256::from(0), - - need_settle: false, - update_offset: U256::from(36), - update_len: U256::from(32), - spend_asset: Address::from_slice( - hex::decode("ffffffff63d24ecc8eb8a7b5d0803e900f7b6ced") - .unwrap() - .as_slice(), - ), - spend_amount: U256::from(0), - receive_asset: Address::from_slice( - hex::decode("0000000000000000000000000000000000000000") - .unwrap() - .as_slice(), - ), - }), - input_call: Some(3), - call_index: Some(5), - }, - ], + println!( + "fee of Moonbeam/GLMR -> Moonbeam/xcDOT is {}", + task.calculate_fee(&context).unwrap() ); - - let claim_func = handler - .abi() - .function("claimAndBatchCall") - .map_err(|_| "NoFunctionFound") - .unwrap(); - let calldata = claim_func - .encode_input(¶ms.into_tokens()) - .map_err(|_| "EncodeParamError") - .unwrap(); - println!("claim calldata: {:?}", hex::encode(calldata)); } } diff --git a/contracts/index_executor/src/task_deposit.rs b/contracts/index_executor/src/task_deposit.rs index ae22981c..1947fe01 100644 --- a/contracts/index_executor/src/task_deposit.rs +++ b/contracts/index_executor/src/task_deposit.rs @@ -1,4 +1,4 @@ -use crate::step::StepJson; +use crate::step::MultiStepInput; use crate::task::Task; use alloc::{string::String, vec::Vec}; use pink_web3::{ @@ -9,13 +9,16 @@ use pink_web3::{ use scale::{Decode, Encode}; use xcm::v3::AssetId as XcmAssetId; +pub type Solution = Vec; + #[derive(Debug)] +#[allow(dead_code)] pub struct EvmDepositData { - // TODO: use Bytes sender: Address, - amount: U256, + token: Address, recipient: Vec, - task: String, + amount: U256, + pub solution: Option>, } impl Detokenize for EvmDepositData { @@ -23,38 +26,39 @@ impl Detokenize for EvmDepositData { where Self: Sized, { - if tokens.len() != 1 { - return Err(PinkError::InvalidOutputType(String::from("Invalid length"))); - } - - let deposit_raw = tokens[0].clone(); - match deposit_raw { - Token::Tuple(deposit_data) => { - match ( - deposit_data[0].clone(), - deposit_data[2].clone(), - deposit_data[3].clone(), - deposit_data[4].clone(), - ) { - ( - Token::Address(sender), - Token::Uint(amount), - Token::Bytes(recipient), - Token::String(task), - ) => Ok(EvmDepositData { - sender, - amount, - recipient, - task, - }), - _ => Err(PinkError::InvalidOutputType(String::from( - "Return type dismatch", - ))), + if tokens.len() == 1 { + let deposit_raw = tokens[0].clone(); + match deposit_raw { + Token::Tuple(deposit_data) => { + match ( + deposit_data[0].clone(), + deposit_data[1].clone(), + deposit_data[2].clone(), + deposit_data[3].clone(), + ) { + ( + Token::Address(sender), + Token::Address(token), + Token::Bytes(recipient), + Token::Uint(amount), + ) => Ok(EvmDepositData { + sender, + token, + recipient, + amount, + solution: None, + }), + _ => Err(PinkError::InvalidOutputType(String::from( + "Return type mismatch", + ))), + } } + _ => Err(PinkError::InvalidOutputType(String::from( + "Unexpected output type", + ))), } - _ => Err(PinkError::InvalidOutputType(String::from( - "Unexpected output type", - ))), + } else { + Err(PinkError::InvalidOutputType(String::from("Invalid length"))) } } } @@ -66,28 +70,28 @@ pub struct SubDepositData { pub asset: XcmAssetId, pub amount: u128, pub recipient: Vec, - pub task: Vec, + pub solution: Vec, } // Define the structures to parse deposit data json #[allow(dead_code)] #[derive(Debug)] pub struct DepositData { - // TODO: use Bytes sender: Vec, amount: u128, recipient: Vec, - task: String, + solution: Vec, } -impl From for DepositData { - fn from(value: EvmDepositData) -> Self { - Self { +impl TryFrom for DepositData { + type Error = &'static str; + fn try_from(value: EvmDepositData) -> Result { + Ok(Self { sender: value.sender.as_bytes().into(), amount: value.amount.try_into().expect("Amount overflow"), recipient: value.recipient, - task: value.task, - } + solution: value.solution.ok_or("MissingSolution")?, + }) } } @@ -97,7 +101,7 @@ impl From for DepositData { sender: value.sender.into(), amount: value.amount, recipient: value.recipient, - task: String::from_utf8_lossy(&value.task).into_owned(), + solution: value.solution, } } } @@ -110,13 +114,14 @@ impl DepositData { worker: [u8; 32], ) -> Result { pink_extension::debug!("Trying to parse task data from json string"); - let execution_plan_json: ExecutionPlan = - pink_json::from_str(&self.task).map_err(|_| "InvalidTask")?; + + let solution: Solution = + Decode::decode(&mut self.solution.as_slice()).map_err(|_| "InvalidTask")?; pink_extension::debug!( "Parse task data successfully, found {:?} operations", - execution_plan_json.len() + solution.len() ); - if execution_plan_json.is_empty() { + if solution.is_empty() { return Err("EmptyTask"); } pink_extension::debug!("Trying to convert task data to task"); @@ -131,12 +136,12 @@ impl DepositData { ..Default::default() }; - for step_json in execution_plan_json.iter() { - uninitialized_task.steps.push(step_json.clone().try_into()?); + for multi_step_input in solution.iter() { + uninitialized_task + .merged_steps + .push(multi_step_input.clone().try_into()?); } Ok(uninitialized_task) } } - -type ExecutionPlan = Vec; diff --git a/contracts/index_executor/src/task_fetcher.rs b/contracts/index_executor/src/task_fetcher.rs index 9d1897b1..519e7de5 100644 --- a/contracts/index_executor/src/task_fetcher.rs +++ b/contracts/index_executor/src/task_fetcher.rs @@ -1,8 +1,9 @@ use crate::account::AccountInfo; use crate::chain::{Chain, ChainType}; +use crate::storage::StorageClient; use crate::task::Task; -use crate::task_deposit::{DepositData, EvmDepositData, SubDepositData}; -use alloc::{format, vec::Vec}; +use crate::task_deposit::{DepositData, EvmDepositData, Solution, SubDepositData}; +use alloc::vec::Vec; use pink_extension::ResultExt; use pink_subrpc::{ get_storage, @@ -15,9 +16,13 @@ use pink_web3::{ transports::{resolve_ready, PinkHttp}, types::{Address, H160}, }; +use scale::Encode; /// Fetch actived tasks from blockchains and construct a `Task` from it. /// If the given chain is EVM based, fetch tasks from solidity-based smart contract storage through RPC task. +/// For example, call RPC methods defined here: +/// https://github.com/Phala-Network/index-solidity/blob/7b4458f9b8277df8a1c705a4d0f264476f42fcf2/contracts/Handler.sol#L147 +/// https://github.com/Phala-Network/index-solidity/blob/7b4458f9b8277df8a1c705a4d0f264476f42fcf2/contracts/Handler.sol#L165 /// If the given chain is Substrate based, fetch tasks from pallet storage through RPC task. pub struct ActivedTaskFetcher { pub chain: Chain, @@ -28,9 +33,9 @@ impl ActivedTaskFetcher { ActivedTaskFetcher { chain, worker } } - pub fn fetch_task(&self) -> Result, &'static str> { + pub fn fetch_task(&self, client: &StorageClient) -> Result, &'static str> { match self.chain.chain_type { - ChainType::Evm => Ok(self.query_evm_actived_task(&self.chain, &self.worker)?), + ChainType::Evm => Ok(self.query_evm_actived_task(&self.chain, &self.worker, client)?), ChainType::Sub => Ok(self.query_sub_actived_task(&self.chain, &self.worker)?), } } @@ -39,15 +44,16 @@ impl ActivedTaskFetcher { &self, chain: &Chain, worker: &AccountInfo, + client: &StorageClient, ) -> Result, &'static str> { - let handler: H160 = H160::from_slice(&chain.handler_contract); + let handler_on_goerli: H160 = H160::from_slice(&chain.handler_contract); let transport = Eth::new(PinkHttp::new(&chain.endpoint)); - let handler = Contract::from_json(transport, handler, crate::constants::HANDLER_ABI) - .log_err(&format!( - "query_evm_actived_task: failed to instantiate handler {:?} on {:?}", - handler, &chain.name - )) - .or(Err("ConstructContractFailed"))?; + let handler = Contract::from_json( + transport, + handler_on_goerli, + include_bytes!("./abi/handler.json"), + ) + .map_err(|_| "ConstructContractFailed")?; let worker_address: Address = worker.account20.into(); pink_extension::debug!( @@ -57,38 +63,44 @@ impl ActivedTaskFetcher { ); let task_id: [u8; 32] = resolve_ready(handler.query( - "getLastActivedTask", + "getNextActivedTask", worker_address, None, Options::default(), None, )) - .log_err(&format!( - "query_evm_actived_task: failed to get last actived task for worker {:?}", - worker_address - )) - .or(Err("FailedGetLastActivedTask"))?; + .map_err(|_| "FailedGetNextActivedTask")?; if task_id == [0; 32] { return Ok(None); } pink_extension::debug!( - "getLastActivedTask, return task_id: {:?}", + "getNextActivedTask, return task_id: {:?}", hex::encode(task_id) ); - let evm_deposit_data: EvmDepositData = + let mut evm_deposit_data: EvmDepositData = resolve_ready(handler.query("getTaskData", task_id, None, Options::default(), None)) - .log_err(&format!( - "query_evm_actived_task: failed to get task data of task id: {:?}", - hex::encode(task_id) - )) - .or(Err("FailedGetTaskData"))?; + .map_err(|_| "FailedGetTaskData")?; pink_extension::debug!( "Fetch deposit data successfully for task {:?} on {:?}, deposit data: {:?}", &hex::encode(task_id), &chain.name, &evm_deposit_data, ); - let deposit_data: DepositData = evm_deposit_data.into(); + + // Read solution from db + let solution_id = [b"solution".to_vec(), task_id.to_vec()].concat(); + let (solution, _) = client + .read::(&solution_id) + .map_err(|_| "FailedToReadStorage")? + .ok_or("NoSolutionFound")?; + pink_extension::debug!( + "Found solution data associate to task {:?}, solution: {:?}", + &hex::encode(task_id), + &solution, + ); + + evm_deposit_data.solution = Some(solution.encode()); + let deposit_data: DepositData = evm_deposit_data.try_into()?; let task = deposit_data.to_task(&chain.name, task_id, self.worker.account32)?; Ok(Some(task)) } @@ -106,15 +118,12 @@ impl ActivedTaskFetcher { ), None, ) - .log_err(&format!( - "query_sub_actived_task: get actived task failed on chain {:?}", - &chain.name - )) - .or(Err("FailedGetTaskData"))? + .log_err("Read storage [actived task] failed") + .map_err(|_| "FailedGetTaskData")? { let actived_tasks: Vec<[u8; 32]> = scale::Decode::decode(&mut raw_storage.as_slice()) - .log_err("query_sub_actived_task: decode storage [actived task] failed") - .or(Err("DecodeStorageFailed"))?; + .log_err("Decode storage [actived task] failed") + .map_err(|_| "DecodeStorageFailed")?; if !actived_tasks.is_empty() { let oldest_task = actived_tasks[0]; if let Some(raw_storage) = get_storage( @@ -125,16 +134,13 @@ impl ActivedTaskFetcher { ), None, ) - .log_err(&format!( - "query_sub_actived_task: failed to get task data for task id {:?}", - hex::encode(oldest_task) - )) - .or(Err("FailedGetDepositData"))? + .log_err("Read storage [actived task] failed") + .map_err(|_| "FailedGetDepositData")? { let sub_deposit_data: SubDepositData = scale::Decode::decode(&mut raw_storage.as_slice()) - .log_err("query_sub_actived_task: decode storage [deposit data] failed") - .or(Err("DecodeStorageFailed"))?; + .log_err("Decode storage [deposit data] failed") + .map_err(|_| "DecodeStorageFailed")?; pink_extension::debug!( "Fetch deposit data successfully for task {:?} on {:?}, deposit data: {:?}", &hex::encode(oldest_task), @@ -156,3 +162,37 @@ impl ActivedTaskFetcher { } } } + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + use primitive_types::H160; + + #[test] + #[ignore] + fn test_fetch_task_from_moonbeam() { + pink_extension_runtime::mock_ext::mock_all_ext(); + let client: StorageClient = StorageClient::new("url".to_string(), "key".to_string()); + let worker_address: H160 = hex!("bf526928373748b00763875448ee905367d97f96").into(); + let _task = ActivedTaskFetcher { + chain: Chain { + id: 0, + name: String::from("Moonbeam"), + chain_type: ChainType::Evm, + endpoint: String::from("https://moonbeam.api.onfinality.io/public"), + native_asset: vec![0], + foreign_asset: None, + handler_contract: hex!("f778f213B618bBAfCF827b2a5faE93966697E4B5").into(), + tx_indexer_url: Default::default(), + }, + worker: AccountInfo { + account20: worker_address.into(), + account32: [0; 32], + }, + } + .fetch_task(&client) + .unwrap() + .unwrap(); + } +} diff --git a/contracts/index_executor/src/utils.rs b/contracts/index_executor/src/utils.rs index 9197da0d..92c73aeb 100644 --- a/contracts/index_executor/src/utils.rs +++ b/contracts/index_executor/src/utils.rs @@ -1,4 +1,5 @@ use alloc::vec::Vec; +use pink_subrpc::hasher::{Blake2_256, Hasher}; use xcm::v3::prelude::*; pub trait ToArray { @@ -42,3 +43,7 @@ pub fn slice_to_generalkey(key: &[u8]) -> Junction { }, } } + +pub fn h160_to_sr25519_pub(addr: &[u8]) -> [u8; 32] { + Blake2_256::hash(&[b"evm:", addr].concat()) +} diff --git a/doc/play-with-index.md b/doc/play-with-index.md index 89ffc430..30e8683a 100644 --- a/doc/play-with-index.md +++ b/doc/play-with-index.md @@ -44,41 +44,24 @@ with `--resume` will unpause the executor (executor is paused by default after d ## Run scheduler The scheduler is responsible for scheduling the execution of inDEX engine. It will call `executor.run` periodically to 1) fetch tasks that distributed to a specific worker from handler on source chain and 2) run tasks that are successfully claimed by the worker. +Also it will update the access token generated by [gcloud cli](https://cloud.google.com/sdk/docs/install) command periodically. -- get worker account info +Issue command ```sh -node src/console.js --config src/config.poc5.json worker list +node src/console.js --config src/config.poc5.json scheduler run --fetch-interval 30000 --execute-interval 10000 --token-update-interval 600000 ``` -- update worker account in config.poc5.json -- execute `node src/scheduler.js` - ** Don't forget fund the worker account on specific chain to pay gas fee ** ## Others -- You may need to let worker approve the handler contract - -For example you want to approve `Moonbeam bridge contract: 0x70085a09D30D6f8C4ecF6eE10120d1847383BB57` sepnd your asset: - -```sh -node src/console.js --config src/config.poc5.json worker approve \ ---worker \ ---chain Moonbeam \ ---token 0xAcc15dC74880C9944775448304B263D191c6077F \ ---spender 0x70085a09D30D6f8C4ecF6eE10120d1847383BB57 \ ---amount 10000000000000000000000 -``` - -This command will call `executor.worker_approve` where worker account will send ERC20 approve transaction on behalf of the call - - You may need to deposit your task with handler contract Suppose you got solution data like below: ```sh -SOLUTION_DATA="[{\"op_type\":\"swap\",\"source_chain\":\"Moonbeam\",\"dest_chain\":\"Moonbeam\",\"spend_asset\":\"0xAcc15dC74880C9944775448304B263D191c6077F\",\"receive_asset\":\"0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080\",\"dex\":\"StellaSwap\",\"fee\":\"0\",\"cap\":\"0\",\"flow\":\"2000000000000000000\",\"impact\":\"0\",\"spend\":\"2000000000000000000\"},{\"op_type\":\"bridge\",\"source_chain\":\"Moonbeam\",\"dest_chain\":\"Acala\",\"spend_asset\":\"0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080\",\"receive_asset\":\"0x010200411f06080002\",\"dex\":\"null\",\"fee\":\"0\",\"cap\":\"0\",\"flow\":\"1400000000\",\"impact\":\"0\",\"spend\":\"1400000000\"}]" +SOLUTION_DATA="[{\"exe_type\":\"swap\",\"exe\":\"moonbeam_stellaswap\",\"source_chain\":\"Moonbeam\",\"dest_chain\":\"Moonbeam\",\"spend_asset\":\"0xAcc15dC74880C9944775448304B263D191c6077F\",\"receive_asset\":\"0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080\"},{\"exe_type\":\"swap\",\"exe\":\"moonbeam_stellaswap\",\"source_chain\":\"Moonbeam\",\"dest_chain\":\"Moonbeam\",\"spend_asset\":\"0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080\",\"receive_asset\":\"0xFFFfFfFf63d24eCc8eB8a7b5D0803e900F7b6cED\"},{\"exe_type\":\"bridge\",\"exe\":\"moonbeam_bridge_to_phala\",\"source_chain\":\"Moonbeam\",\"dest_chain\":\"Phala\",\"spend_asset\":\"0xFFFfFfFf63d24eCc8eB8a7b5D0803e900F7b6cED\",\"receive_asset\":\"0x0000\"},{\"exe_type\":\"bridge\",\"exe\":\"phala_bridge_to_astar\",\"source_chain\":\"Phala\",\"dest_chain\":\"Astar\",\"spend_asset\":\"0x0000\",\"receive_asset\":\"0x010100cd1f\"},{\"exe_type\":\"bridge\",\"exe\":\"astar_bridge_to_astarevm\",\"source_chain\":\"Astar\",\"dest_chain\":\"AstarEvm\",\"spend_asset\":\"0x010100cd1f\",\"receive_asset\":\"0xFFFFFFFF00000000000000010000000000000006\"},{\"exe_type\":\"swap\",\"exe\":\"astar_evm_arthswap\",\"source_chain\":\"AstarEvm\",\"dest_chain\":\"AstarEvm\",\"spend_asset\":\"0xFFFFFFFF00000000000000010000000000000006\",\"receive_asset\":\"0xaeaaf0e2c81af264101b9129c00f4440ccf0f720\"},{\"exe_type\":\"swap\",\"exe\":\"astar_evm_arthswap\",\"source_chain\":\"AstarEvm\",\"dest_chain\":\"AstarEvm\",\"spend_asset\":\"0xaeaaf0e2c81af264101b9129c00f4440ccf0f720\",\"receive_asset\":\"0xFFFFFFFF00000000000000010000000000000003\"}]" ``` then deposit task in `Handler` contract we deployed on Moonbeam for test: diff --git a/rust-toolchain b/rust-toolchain index 25865274..74517af0 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1,5 +1,5 @@ [toolchain] -channel = "1.69.0" +channel = "nightly-2023-12-26" components = [ "cargo", "clippy", diff --git a/scripts/package.json b/scripts/package.json index 09a2745e..5e4d19bc 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -8,13 +8,8 @@ "author": "", "license": "GPL-3.0-or-later", "dependencies": { - "@phala/sdk": "^0.4.0", - "@phala/typedefs": "^0.2.33", - "@polkadot/api": "^10.3.4", - "@polkadot/api-contract": "^10.3.4", - "@polkadot/types-support": "^10.3.4", - "@polkadot/util": "^11.1.3", - "bn.js": "^5.2.1", + "@phala/sdk": "^0.5.1-nightly-20230901", + "child_process": "^1.0.2", "commander": "^10.0.0", "console-stamp": "^3.1.0", "decimal.js": "^10.4.3", diff --git a/scripts/src/HandlerABI.json b/scripts/src/HandlerABI.json new file mode 100644 index 00000000..e20e620f --- /dev/null +++ b/scripts/src/HandlerABI.json @@ -0,0 +1,657 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "worker", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "taskId", + "type": "bytes32" + } + ], + "name": "Claimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "worker", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "taskId", + "type": "bytes32" + } + ], + "name": "ClaimedAndExecuted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "recipient", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "string", + "name": "task", + "type": "string" + } + ], + "name": "Deposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "worker", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "taskId", + "type": "bytes32" + } + ], + "name": "Dropped", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "_activedTasks", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "_depositRecords", + "outputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "recipient", + "type": "bytes" + }, + { + "internalType": "string", + "name": "task", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "_workers", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "needSettle", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "updateOffset", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "updateLen", + "type": "uint256" + }, + { + "internalType": "address", + "name": "spendAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "spendAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiveAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "inputCall", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "callIndex", + "type": "uint256" + } + ], + "internalType": "struct Handler.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "batchCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "taskId", + "type": "bytes32" + } + ], + "name": "claim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "taskId", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "needSettle", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "updateOffset", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "updateLen", + "type": "uint256" + }, + { + "internalType": "address", + "name": "spendAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "spendAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiveAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "inputCall", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "callIndex", + "type": "uint256" + } + ], + "internalType": "struct Handler.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "claimAndBatchCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "recipient", + "type": "bytes" + }, + { + "internalType": "address", + "name": "worker", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "taskId", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "task", + "type": "string" + } + ], + "name": "deposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "taskId", + "type": "bytes32" + } + ], + "name": "drop", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "worker", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "taskId", + "type": "bytes32" + } + ], + "name": "findActivedTask", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "recipient", + "type": "bytes" + }, + { + "internalType": "string", + "name": "task", + "type": "string" + } + ], + "internalType": "struct Handler.DepositInfo", + "name": "depositInfo", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "worker", + "type": "address" + } + ], + "name": "getActivedTasks", + "outputs": [ + { + "internalType": "bytes32[]", + "name": "", + "type": "bytes32[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "worker", + "type": "address" + } + ], + "name": "getLastActivedTask", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "taskId", + "type": "bytes32" + } + ], + "name": "getTaskData", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "recipient", + "type": "bytes" + }, + { + "internalType": "string", + "name": "task", + "type": "string" + } + ], + "internalType": "struct Handler.DepositInfo", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "worker", + "type": "address" + } + ], + "name": "removeWorker", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "workers", + "type": "address[]" + } + ], + "name": "setMultiWorkers", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "worker", + "type": "address" + } + ], + "name": "setWorker", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/scripts/src/config.poc5.json b/scripts/src/config.poc5.json index 1f0ce5bc..9025fd34 100644 --- a/scripts/src/config.poc5.json +++ b/scripts/src/config.poc5.json @@ -3,20 +3,29 @@ "node_rpc_endpoint": "https://poc5.phala.network/rpc", "pruntine_endpoint": "https://poc5.phala.network/tee-api-1", - "executor_contract_id": "0x0f48a4ef35cba2dd348fba52e11ce2f4a27a59a36fa11bd24ffd1eecbf09fa8c", - "key_store_contract_id": "0x2f663b09beb280c6ec6f05c35a18e18d595ba035a306358f195344de28683404", + "executor_contract_id": "0x1182f96e6a1b317a0bcd73af555314577fc8fbcc949ed3fd27a5f757e8707383", + "key_store_contract_id": "0x961f852bea0d3eaf0bbae6bf954f86a01315b60d21019905c973c4e1d11497fe", "handlers": [ { - "Goerli": "0x7da7D95B07Ba569945e33e93063600c4A434FC6a" + "Ethereum": "0xd693bDC5cb0cF2a31F08744A0Ec135a68C26FE1c" }, { - "Moonbeam": "0x3a62a4980b952C92f4d4243c4A009336Ee0a26eB" + "Moonbeam": "0x635eA86804200F80C16ea8EdDc3c749a54a9C37D" + }, + { + "AstarEvm": "0xB376b0Ee6d8202721838e76376e81eEc0e2FE864" } ], "chains": [ { - "Goerli": "https://goerli.infura.io/v3/6d61e7957c1c489ea8141e947447405b" + "Astar": "https://astar.public.blastapi.io" + }, + { + "AstarEvm": "https://astar.public.blastapi.io" + }, + { + "Ethereum": "https://mainnet.infura.io/v3/6d61e7957c1c489ea8141e947447405b" }, { "Poc5": "https://poc5.phala.network/rpc" diff --git a/scripts/src/console.js b/scripts/src/console.js index 4b96c989..621a10d3 100644 --- a/scripts/src/console.js +++ b/scripts/src/console.js @@ -2,20 +2,22 @@ require('dotenv').config(); const fs = require('fs'); const path = require('path') +const { execSync } = require('child_process') const { program } = require('commander'); const { Decimal } = require('decimal.js'); const BN = require('bn.js'); const { ApiPromise, Keyring, WsProvider } = require('@polkadot/api'); const { cryptoWaitReady } = require('@polkadot/util-crypto'); const { stringToHex } = require('@polkadot/util'); -const ethers = require('ethers') -const PhalaSdk = require('@phala/sdk') -const PhalaSDKTypes = PhalaSdk.types +const ethers = require('ethers'); +const PhalaSdk = require('@phala/sdk'); +const PhalaSDKTypes = PhalaSdk.types; const KhalaTypes = require('@phala/typedefs').khalaDev const { loadContractFile, createContract, delay } = require('./utils'); -const ERC20ABI = require('../../contracts/index_executor/src/abi/erc20.json') -const HandlerABI = require('../../contracts/index_executor/src/abi//handler.json') +// TODO: load from config file +const SOURCE_CHAINS = ['Moonbeam', 'AstarEvm']; +const EXE_WORKER = '0x04dba0677fc274ffaccc0fa1030a66b171d1da9226d2bb9d152654e6a746f276'; function run(afn) { function runner(...args) { @@ -66,13 +68,12 @@ async function useApi(endpoint) { return api; } -async function useCert(uri, api) { +async function useCert(uri, _api) { await cryptoWaitReady(); const keyring = new Keyring({ type: 'sr25519' }) - const account = keyring.addFromUri(uri) + const pair = keyring.addFromUri(uri) return await PhalaSdk.signCertificate({ - api: api, - pair: account, + pair }); } @@ -145,7 +146,7 @@ keystore let pair = await usePair(uri); let keystore = await useKeystore(api, config.pruntine_endpoint, config.key_store_contract_id); // costs estimation - let { gasRequired, storageDeposit } = await keystore.query.setExecutor(cert, {}, config.executor_contract_id); + let { gasRequired, storageDeposit } = await keystore.query.setExecutor(cert.address, {cert}, config.executor_contract_id); // transaction / extrinct let options = { gasLimit: gasRequired.refTime, @@ -176,7 +177,7 @@ executor { // costs estimation - let { gasRequired, storageDeposit } = await executor.query.configEngine(cert, {}, + let { gasRequired, storageDeposit } = await executor.query.configEngine(cert.address, {cert}, storageUrl, storageKey, config.key_store_contract_id, @@ -196,15 +197,9 @@ executor console.log(`✅ Config executor`) } - await delay(10*1000); // 10 seconds - { - await executor.query.configStorage(cert, {}); - console.log(`✅ Config storage`) - } - if (opt.resume !== false) { // costs estimation - let { gasRequired, storageDeposit } = await executor.query.resumeExecutor(cert, {}); + let { gasRequired, storageDeposit } = await executor.query.resumeExecutor(cert.address, {cert}); // transaction / extrinct let options = { gasLimit: gasRequired.refTime, @@ -345,8 +340,8 @@ worker let api = await useApi(config.node_wss_endpoint); let executor = await useExecutor(api, config.pruntine_endpoint, config.executor_contract_id); let cert = await useCert(uri, api); - let ret = (await executor.query.getWorkerAccounts(cert, - {}, + let ret = (await executor.query.getWorkerAccounts(cert.address, + {cert}, )); let workers = ret.output.asOk.toJSON().ok; if (opt.worker !== null) { @@ -373,8 +368,8 @@ worker let cert = await useCert(uri, api); console.log(`Call Executor::worker_approve to approve ERC20 for specific asset`); - let queryRecipient = await executor.query.workerApprove(cert, - {}, + let queryRecipient = await executor.query.workerApprove(cert.address, + {cert}, opt.worker, opt.chain.toLowerCase(), opt.token, @@ -417,5 +412,141 @@ worker } })); +worker + .command('drop-task') + .description('drop a task that has not been claimed from handler') + .requiredOption('--worker ', 'worker sr25519 public key', null) + .requiredOption('--chain ', 'chain name', null) + .requiredOption('--id ', 'task id', null) + + .action( + run(async (opt) => { + let { uri } = program.opts(); + let config = useConfig(); + let api = await useApi(config.node_wss_endpoint); + let executor = await useExecutor( + api, + config.pruntine_endpoint, + config.executor_contract_id + ); + let cert = await useCert(uri, api); + + console.log( + `Call Executor::worker_drop_task...` + ); + let queryRecipient = await executor.query.workerDropTask(cert.address, + {cert}, + opt.worker, + opt.chain.charAt(0).toUpperCase() + opt.chain.slice(1).toLowerCase(), + opt.id + ); + console.log( + `Query recipient: ${JSON.stringify(queryRecipient, null, 2)}` + ); + }) + ); + +const scheduler = program + .command('scheduler') + .description('inDEX scheduler'); + +scheduler + .command('run') + .description('Run scheduled tasks periodically [this command never return]') + .option('--fetch-interval ', 'Interval of fetch task from all source chains in millisecond', null) + .option('--execute-interval ', 'Interval of execute task in millisecond', null) + .option('--token-update-interval ', 'Interval of update google firbase access token in millisecond', null) + + .action( + run(async (opt) => { + let { uri, storageUrl } = program.opts(); + let config = useConfig(); + let api = await useApi(config.node_wss_endpoint); + let executor = await useExecutor( + api, + config.pruntine_endpoint, + config.executor_contract_id + ); + let cert = await useCert(uri, api); + let pair = await usePair(uri); + + if ((await executor.query.isRunning(cert.address, {cert})).asOk === false) { + throw new Error("Executor not running") + } + + console.log(`Start run interval tasks...`) + + async function runIntervalTasks() { + return new Promise(async (_resolve, reject) => { + // Trigger task lookup + setInterval(async () => { + for (const source of SOURCE_CHAINS) { + console.log(`🔍Trigger actived task search from ${source} for worker ${EXE_WORKER}`) + let { output } = await executor.query.run(cert.address, + {cert}, + {'Fetch': [source, EXE_WORKER]} + ) + console.log(`Task lookup result: ${JSON.stringify(output, null, 2)}`) + } + }, Number(opt.fetchInterval | 30000)) + + // Trigger task execution + setInterval(async () => { + console.log(`🐌Trigger task executing`) + let {output} = await executor.query.run(cert.address, + {cert}, + 'Execute' + ) + console.log(`Task execute result: ${JSON.stringify(output, null, 2)}`) + }, Number(opt.executeInterval | 10000)) + + // Trigger gcloud access token update + setInterval(async () => { + try { + const token = execSync('gcloud auth print-access-token').toString().trim(); + console.log(`Generate new token: ${token}`); + + let { gasRequired, storageDeposit } = await executor.query.configEngine(cert.address, + {cert}, + storageUrl, + token, + config.key_store_contract_id, + false, + ); + // transaction / extrinct + let options = { + gasLimit: gasRequired.refTime, + storageDepositLimit: storageDeposit.isCharge ? storageDeposit.asCharge : null, + }; + await executor.tx.configEngine(options, + storageUrl, + token, + config.key_store_contract_id, + false, + ).signAndSend(pair, { nonce: -1 }); + console.log(`✅ Access token updated successfully`); + } catch (error) { + throw new Error(`Failed to generate access token due to error: ${error}`) + } + }, Number(opt.tokenUpdateInterval | 60000)) + }) + } + + while (true) { + try { + await runIntervalTasks(); + } catch (error) { + if ( + error.message.includes('InkResponse') || + error.message.includes('inkMessageReturn') || + error.code == 'ECONNRESET') { + console.warn('Got known exception caused by network traffic, will continue to execute'); + } else { + throw error; + } + } + } + }) + ); program.parse(process.argv); diff --git a/scripts/src/registry.json b/scripts/src/registry.json new file mode 100644 index 00000000..0398551e --- /dev/null +++ b/scripts/src/registry.json @@ -0,0 +1,565 @@ +{ + "chains": [ + { + "name": "Ethereum", + "chainId": 1, + "chainType": "evm", + "foeignAsset": "N/A", + "isTest": false, + "nativeAsset": "0x0000000000000000000000000000000000000000", + "handlerContract": "0xd693bDC5cb0cF2a31F08744A0Ec135a68C26FE1c", + "endpoint": "https://rpc.ankr.com/eth", + "txIndexerURL": "https://squid.subsquid.io/graph-ethereum/graphql", + "explorerURL": "", + "icon": "" + }, + { + "name": "Moonbeam", + "chainId": 1, + "chainType": "evm", + "foeignAsset": "N/A", + "isTest": false, + "nativeAsset": "0x0000000000000000000000000000000000000802", + "handlerContract": "0x8351BAE38E3D590063544A99A95BF4fe5379110b", + "endpoint": "https://rpc.api.moonbeam.network", + "txIndexerURL": "https://squid.subsquid.io/graph-moonbeam/graphql", + "explorerURL": "", + "icon": "" + }, + { + "name": "AstarEvm", + "chainId": 1, + "chainType": "evm", + "foeignAsset": "N/A", + "isTest": false, + "nativeAsset": "0x0000000000000000000000000000000000000000", + "handlerContract": "0xAE1Ab0a83de66a545229d39E874237fbaFe05714", + "endpoint": "https://astar.public.blastapi.io", + "txIndexerURL": "https://squid.subsquid.io/graph-astar/graphql", + "explorerURL": "", + "icon": "" + }, + { + "name": "Astar", + "chainId": 2004, + "chainType": "sub", + "foeignAsset": "PalletAsset", + "isTest": false, + "nativeAsset": "0x010100591f", + "handlerContract": "0x", + "endpoint": "https://astar.public.blastapi.io", + "txIndexerURL": "https://squid.subsquid.io/graph-astar/graphql", + "explorerURL": "", + "icon": "" + }, + { + "name": "Khala", + "chainId": 2006, + "chainType": "sub", + "foeignAsset": "PalletAsset", + "isTest": false, + "nativeAsset": "0x0000", + "handlerContract": "0x79", + "endpoint": "https://khala-api.phala.network/rpc", + "txIndexerURL": "https://squid.subsquid.io/graph-khala/graphql", + "explorerURL": "", + "icon": "" + }, + { + "name": "Phala", + "chainId": 2035, + "chainType": "sub", + "foeignAsset": "PalletAsset", + "isTest": false, + "nativeAsset": "0x0000", + "handlerContract": "0x79", + "endpoint": "https://api.phala.network/rpc", + "txIndexerURL": "https://squid.subsquid.io/graph-phala/graphql", + "explorerURL": "", + "icon": "" + }, + { + "name": "Acala", + "chainId": 2000, + "chainType": "sub", + "foeignAsset": "OrmlToken", + "isTest": false, + "nativeAsset": "0x010200411f06080000", + "handlerContract": "0x", + "endpoint": "https://acala-rpc.dwellir.com", + "txIndexerURL": "https://squid.subsquid.io/graph-acala/graphql", + "explorerURL": "", + "icon": "" + }, + { + "name": "Polkadot", + "chainId": 1, + "chainType": "sub", + "foeignAsset": "PalletAsset", + "isTest": false, + "nativeAsset": "0x0000", + "handlerContract": "0x", + "endpoint": "https://polkadot.api.onfinality.io/public", + "txIndexerURL": "https://squid.subsquid.io/graph-polkadot/graphql", + "explorerURL": "", + "icon": "" + } + ], + "assets": [ + { + "name": "Ether", + "symbol": "ETH", + "decimals": 18, + "location": "0x0000000000000000000000000000000000000000", + "chainName": "Ethereum", + "existentialDeposit": [], + "icon": "" + }, + { + "name": "Wrap Ether", + "symbol": "WETH", + "decimals": 18, + "location": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "chainName": "Ethereum", + "existentialDeposit": [], + "icon": "" + }, + { + "name": "USD Token", + "symbol": "USDT", + "decimals": 6, + "location": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "chainName": "Ethereum", + "existentialDeposit": [], + "icon": "" + }, + { + "name": "USD Coin", + "symbol": "USDC", + "decimals": 6, + "location": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "chainName": "Ethereum", + "existentialDeposit": [], + "icon": "" + }, + { + "name": "Phala Token", + "symbol": "PHA", + "decimals": 18, + "location": "0x6c5bA91642F10282b576d91922Ae6448C9d52f4E", + "chainName": "Ethereum", + "existentialDeposit": [ + { + "Khala": "0.001", + "Phala": "0.001" + } + ], + "icon": "" + }, + { + "name": "GLMR", + "symbol": "GLMR", + "decimals": 18, + "location": "0x0000000000000000000000000000000000000802", + "chainName": "Moonbeam", + "existentialDeposit": [], + "icon": "" + }, + { + "name": "Wrap GLMR", + "symbol": "WGLMR", + "decimals": 18, + "location": "0xAcc15dC74880C9944775448304B263D191c6077F", + "chainName": "Moonbeam", + "existentialDeposit": [], + "icon": "" + }, + { + "name": "xcDOT", + "symbol": "xcDOT", + "decimals": 10, + "location": "0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080", + "chainName": "Moonbeam", + "existentialDeposit": [ + { + "Polkadot": "0.001" + } + ], + "icon": "" + }, + { + "name": "xcASTR", + "symbol": "xcASTR", + "decimals": 18, + "location": "0xFfFFFfffA893AD19e540E172C10d78D4d479B5Cf", + "chainName": "Moonbeam", + "existentialDeposit": [], + "icon": "" + }, + { + "name": "xcPHA", + "symbol": "xcPHA", + "decimals": 12, + "location": "0xFFFfFfFf63d24eCc8eB8a7b5D0803e900F7b6cED", + "chainName": "Moonbeam", + "existentialDeposit": [], + "icon": "" + }, + { + "name": "ASTR", + "symbol": "ASTR", + "decimals": 18, + "location": "0x0000000000000000000000000000000000000000", + "chainName": "AstarEvm", + "existentialDeposit": [], + "icon": "" + }, + { + "name": "WASTR", + "symbol": "WASTR", + "decimals": 18, + "location": "0xaeaaf0e2c81af264101b9129c00f4440ccf0f720", + "chainName": "AstarEvm", + "existentialDeposit": [], + "icon": "" + }, + { + "name": "PHA", + "symbol": "PHA", + "decimals": 12, + "location": "0xFFFFFFFF00000000000000010000000000000006", + "chainName": "AstarEvm", + "existentialDeposit": [ + { + "Phala": "0.001" + } + ], + "icon": "" + }, + { + "name": "GLMR", + "symbol": "GLMR", + "decimals": 18, + "location": "0xFFFFFFFF00000000000000010000000000000003", + "chainName": "AstarEvm", + "existentialDeposit": [], + "icon": "" + }, + { + "name": "DOT", + "symbol": "DOT", + "decimals": 10, + "location": "0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF", + "chainName": "AstarEvm", + "existentialDeposit": [], + "icon": "" + }, + { + "name": "USDT", + "symbol": "USDT", + "decimals": 6, + "location": "0xFFFFFFFF000000000000000000000001000007C0", + "chainName": "AstarEvm", + "existentialDeposit": [], + "icon": "" + }, + { + "name": "Phala Token", + "symbol": "PHA", + "decimals": 12, + "location": "0x0000", + "chainName": "Phala", + "existentialDeposit": [], + "icon": "" + }, + { + "name": "Phala Token", + "symbol": "PHA", + "decimals": 12, + "location": "0x0000", + "chainName": "Khala", + "existentialDeposit": [], + "icon": "" + }, + { + "name": "DOT", + "symbol": "DOT", + "decimals": 10, + "location": "0x0000", + "chainName": "Polkadot", + "existentialDeposit": [], + "icon": "" + }, + { + "name": "GLMR", + "symbol": "GLMR", + "decimals": 18, + "location": "0x03010200511f040a", + "chainName": "Astar", + "existentialDeposit": [], + "icon": "" + }, + { + "name": "ASTR", + "symbol": "ASTR", + "decimals": 18, + "location": "0x010100591f", + "chainName": "Astar", + "existentialDeposit": [], + "icon": "" + }, + { + "name": "PHA", + "symbol": "PHA", + "decimals": 12, + "location": "0x010100cd1f", + "chainName": "Astar", + "existentialDeposit": [], + "icon": "" + } + ], + "bridges": [ + { + "name": "Ethereum2Phala", + "action": "ethereum_sygmabridge_to_phala", + "sourceChain": "Ethereum", + "destChain": "Phala", + "pairs": [ + { + "asset0": "Ethereum/PHA", + "asset1": "Phala/PHA", + "reserve0": "", + "reserve1": "" + } + ] + }, + { + "name": "Phala2Ethereum", + "action": "phala_bridge_to_ethereum", + "sourceChain": "Phala", + "destChain": "Ethereum", + "pairs": [ + { + "asset0": "Phala/PHA", + "asset1": "Ethereum/PHA", + "reserve0": "", + "reserve1": "" + } + ] + }, + { + "name": "Phala2Moonbeam", + "action": "phala_bridge_to_moonbeam", + "sourceChain": "Phala", + "destChain": "Moonbeam", + "pairs": [ + { + "asset0": "Phala/PHA", + "asset1": "Moonbeam/PHA", + "reserve0": "", + "reserve1": "" + } + ] + }, + { + "name": "Phala2Astar", + "action": "phala_bridge_to_astar", + "sourceChain": "Phala", + "destChain": "Astar", + "pairs": [ + { + "asset0": "Phala/PHA", + "asset1": "Astar/PHA", + "reserve0": "", + "reserve1": "" + } + ] + }, + { + "name": "Moonbeam2Phala", + "action": "moonbeam_bridge_to_phala", + "sourceChain": "Moonbeam", + "destChain": "Phala", + "pairs": [ + { + "asset0": "Moonbeam/PHA", + "asset1": "Phala/PHA", + "reserve0": "", + "reserve1": "" + } + ] + }, + { + "name": "Moonbeam2Astar", + "action": "moonbeam_bridge_to_astar", + "sourceChain": "Moonbeam", + "destChain": "Astar", + "pairs": [ + { + "asset0": "Moonbeam/GLMR", + "asset1": "Astar/GLMR", + "reserve0": "", + "reserve1": "" + }, + { + "asset0": "Moonbeam/ASTR", + "asset1": "Astar/ASTR", + "reserve0": "", + "reserve1": "" + } + ] + }, + { + "name": "Astar2AstarEvm", + "action": "astar_bridge_to_astarevm", + "sourceChain": "Astar", + "destChain": "AstarEvm", + "pairs": [ + { + "asset0": "Astar/ASTR", + "asset1": "AstarEvm/ASTR", + "reserve0": "", + "reserve1": "" + }, + { + "asset0": "Astar/GLMR", + "asset1": "AstarEvm/GLMR", + "reserve0": "", + "reserve1": "" + }, + { + "asset0": "Astar/PHA", + "asset1": "AstarEvm/PHA", + "reserve0": "", + "reserve1": "" + } + ] + }, + { + "name": "Ethereum2Khala", + "action": "ethereum_sygmabridge_to_khala", + "sourceChain": "Ethereum", + "destChain": "Khala", + "pairs": [ + { + "asset0": "Ethereum/PHA", + "asset1": "Khala/PHA", + "reserve0": "", + "reserve1": "" + } + ] + }, + { + "name": "Astar2Phala", + "action": "astar_bridge_to_phala", + "sourceChain": "Astar", + "destChain": "Phala", + "pairs": [ + { + "asset0": "Astar/PHA", + "asset1": "Phala/PHA", + "reserve0": "", + "reserve1": "" + } + ] + } + ], + "dexes": [ + { + "name": "UniswapV2", + "action": "ethereum_uniswapv2", + "chainName": "Ethereum", + "pairs": [ + { + "asset0": "Ethereum/WETH", + "asset1": "Ethereum/PHA" + }, + { + "asset0": "Ethereum/WETH", + "asset1": "Ethereum/USDT" + }, + { + "asset0": "Ethereum/WETH", + "asset1": "Ethereum/USDC" + } + ] + }, + { + "name": "UniswapV3", + "action": "ethereum_uniswapv3", + "chainName": "Ethereum", + "pairs": [] + }, + { + "name": "StellaSwap", + "action": "moonbeam_stellaswap", + "chainName": "Moonbeam", + "pairs": [ + { + "asset0": "Moonbeam/WGLMR", + "asset1": "Moonbeam/xcDOT" + }, + { + "asset0": "Moonbeam/xcPHA", + "asset1": "Moonbeam/xcDOT" + }, + { + "asset0": "Moonbeam/xcASTR", + "asset1": "Moonbeam/xcDOT" + } + ] + }, + { + "name": "ArthSwap", + "action": "astar_evm_arthswap", + "chainName": "AstarEvm", + "pairs": [ + { + "asset0": "AstarEvm/WASTR", + "asset1": "AstarEvm/DOT" + }, + { + "asset0": "AstarEvm/WASTR", + "asset1": "AstarEvm/GLMR" + }, + { + "asset0": "AstarEvm/WASTR", + "asset1": "AstarEvm/PHA" + } + ] + }, + { + "name": "EthereumWETH9", + "action": "ethereum_nativewrapper", + "chainName": "Ethereum", + "pairs": [ + { + "asset0": "Ethereum/ETH", + "asset1": "Ethereum/WETH" + } + ] + }, + { + "name": "MoonbeamWETH9", + "action": "moonbeam_nativewrapper", + "chainName": "Moonbeam", + "pairs": [ + { + "asset0": "Moonbeam/GLMR", + "asset1": "Moonbeam/WGLMR" + } + ] + }, + { + "name": "AstarEvmWETH9", + "action": "astar_evm_nativewrapper", + "chainName": "AstarEvm", + "pairs": [ + { + "asset0": "AstarEvm/ASTR", + "asset1": "AstarEvm/WASTR" + } + ] + } + ] +} \ No newline at end of file diff --git a/scripts/src/scheduler.js b/scripts/src/scheduler.js deleted file mode 100644 index b42cebdc..00000000 --- a/scripts/src/scheduler.js +++ /dev/null @@ -1,82 +0,0 @@ -require('console-stamp')(console, '[HH:MM:ss.l]') -const { ApiPromise, WsProvider, Keyring } = require('@polkadot/api') -const PhalaSdk = require('@phala/sdk') -const PhalaSDKTypes = PhalaSdk.types -const KhalaTypes = require('@phala/typedefs').khalaDev -const path = require('path') - -const { loadContractFile, createContract } = require('./utils') -const config = require('./config.poc5.json') - -const NODE_ENDPOINT = 'wss://poc5.phala.network/ws' -const PRUNTIME_ENDPOINT = 'https://poc5.phala.network/tee-api-1' -const CONTRACT_ID = config.executor_contract_id -const EXE_WORKERS = config.workers -const SOURCE = 'Moonbeam' - -async function loop_task() { - return new Promise(async (_resolve, reject) => { - console.log(`Loading contract metedata form file system`) - const contract = loadContractFile( - path.join(__dirname, '../../target/ink/index_executor/index_executor.contract'), - ) - - console.log(`Establishing connection with blockchain node`) - const nodeApi = await ApiPromise.create({ - provider: new WsProvider(NODE_ENDPOINT), - types: { - ...KhalaTypes, - ...PhalaSDKTypes, - }, - }) - console.log(`Connected to node, create contract object`) - const executor = await createContract(nodeApi, PRUNTIME_ENDPOINT, contract, CONTRACT_ID) - console.log(`Contract objected created with contract ID: ${CONTRACT_ID}`) - - const keyring = new Keyring({ type: 'sr25519' }) - const alice = keyring.addFromUri('//Alice') - const certAlice = await PhalaSdk.signCertificate({ - api: nodeApi, - pair: alice, - }) - - if ((await executor.query.isRunning(certAlice, {})).asOk === false) { - throw new Error("Executor not running") - } - - console.log(`Start query contract periodically...`) - - // Trigger task search every 30 seconds - setInterval(async () => { - for (let worker in EXE_WORKERS) { - console.log(`🔍Trigger actived task search from ${SOURCE} for worker ${worker}`) - let { output } = await executor.query.run(certAlice, - {}, - {'Fetch': [SOURCE, worker]} - ) - console.log(`Fetch result: ${JSON.stringify(output, null, 2)}`) - } - }, 15000) - - // Trigger task executing every 10 seconds - setInterval(async () => { - console.log(`🐌Trigger task executing`) - let {output} = await executor.query.run(certAlice, - {}, - 'Execute' - ) - console.log(`Execute result: ${JSON.stringify(output, null, 2)}`) - }, 10000) - }) -} - -async function main() { - try { - // never return - await loop_task() - } catch (err) { - console.error(`task run failed: ${err}`) - } -} - -main() \ No newline at end of file diff --git a/scripts/src/utils.js b/scripts/src/utils.js index a7a4945f..669fe366 100644 --- a/scripts/src/utils.js +++ b/scripts/src/utils.js @@ -1,8 +1,6 @@ const fs = require('fs') -const { ApiPromise, WsProvider, Keyring } = require('@polkadot/api') -const { ContractPromise } = require('@polkadot/api-contract') +const { OnChainRegistry, PinkContractPromise } = require('@phala/sdk') const PhalaSdk = require('@phala/sdk') -const PhalaSDKTypes = PhalaSdk.types function loadContractFile(contractFile) { const metadata = JSON.parse(fs.readFileSync(contractFile, 'utf8')) @@ -14,19 +12,16 @@ function loadContractFile(contractFile) { return { wasm, metadata, constructor, name } } -async function createContract(api, pruntimeUrl, contract, contractID) { - const { api: workerApi } = await PhalaSdk.create({ +async function createContract(api, _pruntimeUrl, contract, contractID) { + const phatRegistry = await OnChainRegistry.create(api); + const contractKey = await phatRegistry.getContractKey(contractID) + return new PinkContractPromise( api, - baseURL: pruntimeUrl, - contractId: contractID, - autoDeposit: true, - }) - const contractApi = new ContractPromise( - workerApi, + phatRegistry, contract.metadata, contractID, - ) - return contractApi + contractKey, + ); } async function delay(ms) {