From bbcffa87b2355e70e4abb087196359b5f0470f7b Mon Sep 17 00:00:00 2001 From: npty <78221556+npty@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:50:03 +0700 Subject: [PATCH] feat: create axelar-local-dev-sui (#87) * feat: init package * chore: remove account export * chore: remove file * chore: remove dep * feat: support sui <-> evm call_contracts * feat: add createSuiRelayer script * chore: add afterRelay script for sui * chore: clean up SuiNetwork * chore: cleanup decoder in SuiRelayer * chore: refactor * chore: add payload_hash to gateway module * chore: remove only * chore: fix relay tests * chore: add sui-test-validator command for tests * chore: refactor tests workflow * chore: use nohup to run sui in bg * chore: delete unused steps in workflow * chore: update version to 2.0.5 * chore: export EvmRelayer * chore: change RelayerType to enum * chore: update sui doc * chore: improve usage text * chore: remove build sui contract step in workflow * chore: update docs * chore: include to move.toml example * chore: update doc in root README.md * chore: fix formatting * chore: update evm_to_sui guide * chore: update evm to sui guide * chore: update Sui to Evm guide * chore: update description --- .github/workflows/tests.yml | 27 ++- README.md | 3 + lerna.json | 2 +- package-lock.json | 201 +++++++++++++++--- package.json | 4 +- packages/axelar-local-dev-aptos/.prettierrc | 8 +- packages/axelar-local-dev-aptos/package.json | 4 +- packages/axelar-local-dev-near/package.json | 4 +- packages/axelar-local-dev-sui/.gitignore | 4 + packages/axelar-local-dev-sui/.prettierrc | 15 ++ packages/axelar-local-dev-sui/README.md | 30 +++ .../__tests__/deploy.spec.ts | 48 +++++ .../__tests__/e2e.spec.ts | 89 ++++++++ .../__tests__/relayer.spec.ts | 43 ++++ .../contracts/TestExecutable.sol | 46 ++++ .../docs/develop_sui_module.md | 89 ++++++++ .../axelar-local-dev-sui/docs/evm_to_sui.md | 133 ++++++++++++ .../axelar-local-dev-sui/docs/sui_to_evm.md | 137 ++++++++++++ .../axelar-local-dev-sui/hardhat.config.js | 18 ++ packages/axelar-local-dev-sui/jest.config.js | 10 + .../move/axelar/Move.lock | 22 ++ .../move/axelar/Move.toml | 10 + .../move/axelar/sources/gateway.move | 23 ++ .../move/sample/Move.lock | 31 +++ .../move/sample/Move.toml | 11 + .../move/sample/sources/hello_world.move | 51 +++++ packages/axelar-local-dev-sui/package.json | 26 +++ packages/axelar-local-dev-sui/src/Command.ts | 47 ++++ .../axelar-local-dev-sui/src/SuiNetwork.ts | 198 +++++++++++++++++ .../axelar-local-dev-sui/src/SuiRelayer.ts | 167 +++++++++++++++ packages/axelar-local-dev-sui/src/index.ts | 6 + packages/axelar-local-dev-sui/src/setup.ts | 38 ++++ packages/axelar-local-dev-sui/src/types.ts | 4 + packages/axelar-local-dev-sui/src/utils.ts | 6 + packages/axelar-local-dev-sui/tsconfig.json | 34 +++ packages/axelar-local-dev/package.json | 2 +- packages/axelar-local-dev/src/Network.ts | 4 +- packages/axelar-local-dev/src/exportUtils.ts | 6 +- .../axelar-local-dev/src/relay/Command.ts | 3 +- .../axelar-local-dev/src/relay/EvmRelayer.ts | 6 +- .../axelar-local-dev/src/relay/Relayer.ts | 8 +- packages/axelar-local-dev/src/relay/index.ts | 1 + 42 files changed, 1569 insertions(+), 50 deletions(-) create mode 100644 packages/axelar-local-dev-sui/.gitignore create mode 100644 packages/axelar-local-dev-sui/.prettierrc create mode 100644 packages/axelar-local-dev-sui/README.md create mode 100644 packages/axelar-local-dev-sui/__tests__/deploy.spec.ts create mode 100644 packages/axelar-local-dev-sui/__tests__/e2e.spec.ts create mode 100644 packages/axelar-local-dev-sui/__tests__/relayer.spec.ts create mode 100644 packages/axelar-local-dev-sui/contracts/TestExecutable.sol create mode 100644 packages/axelar-local-dev-sui/docs/develop_sui_module.md create mode 100644 packages/axelar-local-dev-sui/docs/evm_to_sui.md create mode 100644 packages/axelar-local-dev-sui/docs/sui_to_evm.md create mode 100644 packages/axelar-local-dev-sui/hardhat.config.js create mode 100644 packages/axelar-local-dev-sui/jest.config.js create mode 100644 packages/axelar-local-dev-sui/move/axelar/Move.lock create mode 100644 packages/axelar-local-dev-sui/move/axelar/Move.toml create mode 100644 packages/axelar-local-dev-sui/move/axelar/sources/gateway.move create mode 100644 packages/axelar-local-dev-sui/move/sample/Move.lock create mode 100644 packages/axelar-local-dev-sui/move/sample/Move.toml create mode 100644 packages/axelar-local-dev-sui/move/sample/sources/hello_world.move create mode 100644 packages/axelar-local-dev-sui/package.json create mode 100644 packages/axelar-local-dev-sui/src/Command.ts create mode 100644 packages/axelar-local-dev-sui/src/SuiNetwork.ts create mode 100644 packages/axelar-local-dev-sui/src/SuiRelayer.ts create mode 100644 packages/axelar-local-dev-sui/src/index.ts create mode 100644 packages/axelar-local-dev-sui/src/setup.ts create mode 100644 packages/axelar-local-dev-sui/src/types.ts create mode 100644 packages/axelar-local-dev-sui/src/utils.ts create mode 100644 packages/axelar-local-dev-sui/tsconfig.json diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ab8cb5b5..b365680d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,30 +10,49 @@ jobs: os: [ubuntu-22.04] arch: [amd64] steps: - - name: Download Aptos Binary + - name: Download and Install Aptos Binary run: | - wget --no-check-certificate https://github.com/aptos-labs/aptos-core/releases/download/aptos-cli-v1.0.4/aptos-cli-1.0.4-Ubuntu-22.04-x86_64.zip + wget --no-check-certificate https://github.com/aptos-labs/aptos-core/releases/download/aptos-cli-v1.0.4/aptos-cli-1.0.4-Ubuntu-22.04-x86_64.zip unzip aptos-cli-1.0.4-Ubuntu-22.04-x86_64.zip - chmod +x aptos - cp aptos /usr/local/bin + sudo mv aptos /usr/local/bin + + - name: Setup Dependencies for Sui Binary + run: sudo apt-get update && sudo apt-get install -y libpq-dev + + - name: Download and Install Sui Binary + run: | + wget https://github.com/MystenLabs/sui/releases/download/devnet-v1.7.0/sui-devnet-v1.7.0-ubuntu-x86_64.tgz + tar -xvf sui-devnet-v1.7.0-ubuntu-x86_64.tgz + sudo mv ./target/release/sui-test-validator-ubuntu-x86_64 /usr/local/bin/sui-test-validator + sudo mv ./target/release/sui-ubuntu-x86_64 /usr/local/bin/sui + + - name: Cleanup + run: rm -rf target aptos-cli-1.0.4-Ubuntu-22.04-x86_64.zip sui-devnet-v1.7.0-ubuntu-x86_64.tgz + - name: Setup Node uses: actions/setup-node@v3 with: node-version: 16 + - name: Checkout code uses: actions/checkout@v3 + - name: Cache node_modules uses: actions/cache@v3 with: path: node_modules key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + - name: Install dependencies run: npm ci + - name: Build run: npm run build + - name: Test timeout-minutes: 15 run: | + nohup sh -c "sui-test-validator" > nohup.out 2> nohup.err < /dev/null & nohup sh -c "aptos node run-local-testnet --with-faucet" > nohup.out 2> nohup.err < /dev/null & sleep 10 npm run test diff --git a/README.md b/README.md index 3da01db3..924f20c8 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,13 @@ Welcome to the Axelar Local Development Environment! This monorepo contains esse - **Optional Packages**: - [@axelar-network/axelar-local-dev-aptos](./packages/axelar-local-dev-aptos/) - [@axelar-network/axelar-local-dev-near](./packages/axelar-local-dev-near/) + - [@axelar-network/axelar-local-dev-sui](./packages/axelar-local-dev-sui/) The `axelar-local-dev` package is all you need for cross-chain applications between EVM chains. However, if you wish to explore cross-chain applications between EVM chains and other chain stacks, check out our specific guides: - [EVM <-> Aptos Integration Guide](./packages/axelar-local-dev-aptos/README.md#configuration) - [EVM <-> Near Integration Guide](./packages/axelar-local-dev-near/README.md#configuration) +- [Evm <-> Sui Integration Guide](./packages/axelar-local-dev-sui/README.md) ## Installation @@ -36,3 +38,4 @@ We currently support the following chain stacks: - [Aptos](./packages/axelar-local-dev-aptos/) - [Near](./packages/axelar-local-dev-near/) +- [Sui](./packages/axelar-local-dev-sui/) diff --git a/lerna.json b/lerna.json index a1df6bdb..1571177a 100644 --- a/lerna.json +++ b/lerna.json @@ -1,7 +1,7 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", "useWorkspaces": true, - "version": "2.0.5", + "version": "2.1.0", "packages": [ "packages/*" ] diff --git a/package-lock.json b/package-lock.json index 9496ec0b..5148e12e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,6 +71,10 @@ "resolved": "packages/axelar-local-dev-near", "link": true }, + "node_modules/@axelar-network/axelar-local-dev-sui": { + "resolved": "packages/axelar-local-dev-sui", + "link": true + }, "node_modules/@babel/code-frame": { "version": "7.21.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", @@ -3263,6 +3267,83 @@ "node": ">=12.0.0" } }, + "node_modules/@mysten/bcs": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@mysten/bcs/-/bcs-0.7.3.tgz", + "integrity": "sha512-fbusBfsyc2MpTACi72H5edWJ670T84va+qn9jSPpb5BzZ+pzUM1Q0ApPrF5OT+mB1o5Ng+mxPQpBCZQkfiV2TA==", + "dependencies": { + "bs58": "^5.0.0" + } + }, + "node_modules/@mysten/bcs/node_modules/base-x": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", + "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" + }, + "node_modules/@mysten/bcs/node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "dependencies": { + "base-x": "^4.0.0" + } + }, + "node_modules/@mysten/sui.js": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@mysten/sui.js/-/sui.js-0.41.0.tgz", + "integrity": "sha512-IVxlo7qoxDFsE/WiJiLW3gmqftS6QI7LrTHqsDs7YGsBd69cqp95RH3OerJ3Wrbl8bMsQyG+HByX3c3zN6G97A==", + "dependencies": { + "@mysten/bcs": "0.7.3", + "@noble/curves": "^1.1.0", + "@noble/hashes": "^1.3.1", + "@open-rpc/client-js": "^1.8.1", + "@scure/bip32": "^1.3.1", + "@scure/bip39": "^1.2.1", + "@suchipi/femver": "^1.0.0", + "events": "^3.3.0", + "superstruct": "^1.0.3", + "tweetnacl": "^1.0.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@mysten/sui.js/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@mysten/sui.js/node_modules/@scure/bip32": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.2.tgz", + "integrity": "sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==", + "dependencies": { + "@noble/curves": "~1.2.0", + "@noble/hashes": "~1.3.2", + "@scure/base": "~1.1.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@mysten/sui.js/node_modules/@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "dependencies": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -3272,6 +3353,28 @@ "eslint-scope": "5.1.1" } }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/hashes": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.3.tgz", @@ -4996,6 +5099,17 @@ "@octokit/openapi-types": "^17.1.1" } }, + "node_modules/@open-rpc/client-js": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@open-rpc/client-js/-/client-js-1.8.1.tgz", + "integrity": "sha512-vV+Hetl688nY/oWI9IFY0iKDrWuLdYhf7OIKI6U1DcnJV7r4gAgwRJjEr1QVYszUc0gjkHoQJzqevmXMGLyA0g==", + "dependencies": { + "isomorphic-fetch": "^3.0.0", + "isomorphic-ws": "^5.0.0", + "strict-event-emitter-types": "^2.0.0", + "ws": "^7.0.0" + } + }, "node_modules/@parcel/watcher": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.0.4.tgz", @@ -5031,15 +5145,12 @@ } }, "node_modules/@scure/base": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", - "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.2.tgz", + "integrity": "sha512-sSCrnIdaUZQHhBxZThMuk7Wm1TWzMD3uJNdGgx3JS23xSqevu0tAOsg8k66nL3R2NwQe65AI9GgqpPOgZys/eA==", + "funding": { + "url": "https://paulmillr.com/funding/" + } }, "node_modules/@scure/bip32": { "version": "1.1.5", @@ -5241,6 +5352,11 @@ "antlr4ts": "^0.5.0-alpha.4" } }, + "node_modules/@suchipi/femver": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@suchipi/femver/-/femver-1.0.0.tgz", + "integrity": "sha512-bprE8+K5V+DPX7q2e2K57ImqNBdfGHDIWaGI5xHxZoxbKOuQZn4wzPiUxOAHnsUr3w3xHrWXwN7gnG/iIuEMIg==" + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -9688,7 +9804,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -9698,7 +9813,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -11467,7 +11581,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, "engines": { "node": ">=0.8.x" } @@ -14994,6 +15107,23 @@ "node": ">=0.10.0" } }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -19688,7 +19818,6 @@ "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "devOptional": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -23189,7 +23318,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "devOptional": true }, "node_modules/sc-istanbul": { "version": "0.4.6", @@ -24449,6 +24578,11 @@ "node": ">=10.0.0" } }, + "node_modules/strict-event-emitter-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz", + "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==" + }, "node_modules/strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", @@ -24723,6 +24857,14 @@ "node": ">=4" } }, + "node_modules/superstruct": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.3.tgz", + "integrity": "sha512-8iTn3oSS8nRGn+C2pgXSKPI3jmpm6FExNazNpjvqS6ZUJQCej3PUXEKM8NjHBOs54ExM+LPW/FBRhymrdcCiSg==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -25418,8 +25560,7 @@ "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "devOptional": true + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/treeverse": { "version": "3.0.0", @@ -28750,8 +28891,7 @@ "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "devOptional": true + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/websocket": { "version": "1.0.34", @@ -28785,11 +28925,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/whatwg-fetch": { + "version": "3.6.17", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.17.tgz", + "integrity": "sha512-c4ghIvG6th0eudYwKZY5keb81wtFz9/WeAHAoy8+r18kcWlitUIrmGFQ2rWEl4UCKUilD3zCLHOIPheHx5ypRQ==" + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "devOptional": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -29403,7 +29547,7 @@ }, "packages/axelar-local-dev": { "name": "@axelar-network/axelar-local-dev", - "version": "2.0.5", + "version": "2.1.0", "license": "ISC", "dependencies": { "@axelar-network/axelar-cgp-solidity": "^5.0.0", @@ -29450,21 +29594,30 @@ }, "packages/axelar-local-dev-aptos": { "name": "@axelar-network/axelar-local-dev-aptos", - "version": "2.0.5", + "version": "2.1.0", "license": "ISC", "dependencies": { "@axelar-network/axelar-cgp-aptos": "^1.0.5", - "@axelar-network/axelar-local-dev": "2.0.5", + "@axelar-network/axelar-local-dev": "2.1.0", "aptos": "1.3.16" } }, "packages/axelar-local-dev-near": { "name": "@axelar-network/axelar-local-dev-near", - "version": "2.0.5", + "version": "2.1.0", "license": "ISC", "dependencies": { "@axelar-network/axelar-cgp-near": "^1.0.0", - "@axelar-network/axelar-local-dev": "2.0.5" + "@axelar-network/axelar-local-dev": "2.1.0" + } + }, + "packages/axelar-local-dev-sui": { + "name": "@axelar-network/axelar-local-dev-sui", + "version": "2.1.0", + "license": "ISC", + "dependencies": { + "@axelar-network/axelar-local-dev": "2.1.0", + "@mysten/sui.js": "^0.41.0" } } } diff --git a/package.json b/package.json index 53402796..97e31206 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,12 @@ "test:core": "lerna exec --scope=@axelar-network/axelar-local-dev npm run test", "test:near": "lerna exec --scope=@axelar-network/axelar-local-dev-near npm run test", "test:aptos": "lerna exec --scope=@axelar-network/axelar-local-dev-aptos npm run test", + "test:sui": "lerna exec --scope=@axelar-network/axelar-local-dev-sui npm run test", "build": "lerna run build", "build:core": "lerna exec --scope=@axelar-network/axelar-local-dev npm run build", "build:near": "lerna exec --scope=@axelar-network/axelar-local-dev-near npm run build", - "build:aptos": "lerna exec --scope=@axelar-network/axelar-local-dev-aptos npm run build" + "build:aptos": "lerna exec --scope=@axelar-network/axelar-local-dev-aptos npm run build", + "build:sui": "lerna exec --scope=@axelar-network/axelar-local-dev-sui npm run build" }, "devDependencies": { "lerna": "^6.6.1", diff --git a/packages/axelar-local-dev-aptos/.prettierrc b/packages/axelar-local-dev-aptos/.prettierrc index 60246dfe..a2a158db 100644 --- a/packages/axelar-local-dev-aptos/.prettierrc +++ b/packages/axelar-local-dev-aptos/.prettierrc @@ -6,13 +6,7 @@ "bracketSpacing": true, "overrides": [ { - "files": "*.sol", - "options": { - "explicitTypes": "always" - } - }, - { - "files": "*.js", + "files": "*.ts", "options": { "trailingComma": "all" } diff --git a/packages/axelar-local-dev-aptos/package.json b/packages/axelar-local-dev-aptos/package.json index 1e3a4609..456a37fd 100644 --- a/packages/axelar-local-dev-aptos/package.json +++ b/packages/axelar-local-dev-aptos/package.json @@ -1,6 +1,6 @@ { "name": "@axelar-network/axelar-local-dev-aptos", - "version": "2.0.5", + "version": "2.1.0", "main": "dist/index.js", "files": [ "dist/", @@ -19,7 +19,7 @@ }, "dependencies": { "@axelar-network/axelar-cgp-aptos": "^1.0.5", - "@axelar-network/axelar-local-dev": "2.0.5", + "@axelar-network/axelar-local-dev": "2.1.0", "aptos": "1.3.16" }, "author": "", diff --git a/packages/axelar-local-dev-near/package.json b/packages/axelar-local-dev-near/package.json index 7bc94855..80762877 100644 --- a/packages/axelar-local-dev-near/package.json +++ b/packages/axelar-local-dev-near/package.json @@ -1,6 +1,6 @@ { "name": "@axelar-network/axelar-local-dev-near", - "version": "2.0.5", + "version": "2.1.0", "description": "", "main": "dist/index.js", "files": [ @@ -21,7 +21,7 @@ }, "dependencies": { "@axelar-network/axelar-cgp-near": "^1.0.0", - "@axelar-network/axelar-local-dev": "2.0.5" + "@axelar-network/axelar-local-dev": "2.1.0" }, "author": "", "license": "ISC" diff --git a/packages/axelar-local-dev-sui/.gitignore b/packages/axelar-local-dev-sui/.gitignore new file mode 100644 index 00000000..ba94889d --- /dev/null +++ b/packages/axelar-local-dev-sui/.gitignore @@ -0,0 +1,4 @@ +**/node_modules +sui.log.* +backup +presets diff --git a/packages/axelar-local-dev-sui/.prettierrc b/packages/axelar-local-dev-sui/.prettierrc new file mode 100644 index 00000000..a2a158db --- /dev/null +++ b/packages/axelar-local-dev-sui/.prettierrc @@ -0,0 +1,15 @@ +{ + "printWidth": 140, + "singleQuote": true, + "tabWidth": 4, + "useTabs": false, + "bracketSpacing": true, + "overrides": [ + { + "files": "*.ts", + "options": { + "trailingComma": "all" + } + } + ] +} diff --git a/packages/axelar-local-dev-sui/README.md b/packages/axelar-local-dev-sui/README.md new file mode 100644 index 00000000..08313969 --- /dev/null +++ b/packages/axelar-local-dev-sui/README.md @@ -0,0 +1,30 @@ +# Axelar Local Dev: Sui Integration + +Welcome to the Axelar Local Development Suite featuring Sui Integration. This package empowers developers to establish a local development environment for streamlined cross-chain communication utilizing the [Sui protocol](https://sui.io/). Currently, the integration facilitates general message passing exclusively with the EVM chain. + +## Prequisites + +Before you delve into the development, ensure you have the following components installed on your local machine: + +- `sui` +- `sui-test-validator` + +To set these up, adhere to the step-by-step guide provided by Sui, which can be accessed [here](https://docs.sui.io/build/sui-local-network#install-sui-from-github). + +> **Note**: This package has been rigorously tested and found compatible with the [devnet-v1.8.1](https://github.com/MystenLabs/sui/releases/tag/devnet-v1.8.1) version. + +## Initiating the Local Sui Network + +To initiate the local Sui network, execute the command below in your terminal: + +``` +RUST_LOG="consensus=off" cargo run --bin sui-test-validator +``` + +## Usage Guidelines + +Here, you'll find detailed guides that will assist you in various functionalities, including: + +- [Relaying Transactions from EVM to Sui](./docs/evm_to_sui.md) +- [Relaying Transactions from Sui to EVM](./docs/sui_to_evm.md) +- [Developing a Sui Module](./docs/develop_sui_module.md) diff --git a/packages/axelar-local-dev-sui/__tests__/deploy.spec.ts b/packages/axelar-local-dev-sui/__tests__/deploy.spec.ts new file mode 100644 index 00000000..b2cb92e3 --- /dev/null +++ b/packages/axelar-local-dev-sui/__tests__/deploy.spec.ts @@ -0,0 +1,48 @@ +import { SuiNetwork } from '../src/SuiNetwork'; +import { TransactionBlock } from '@mysten/sui.js/transactions'; +import { toHEX } from '@mysten/bcs'; +import path from 'path'; + +describe('Sui Network', () => { + let client: SuiNetwork; + + beforeEach(async () => { + client = new SuiNetwork(); + await client.init(); + }); + + it('should deploy a sample module', async () => { + const response = await client.deploy(path.join(__dirname, '../move/sample')); + expect(response.packages.length).toBe(client.gatewayObjects.length); + expect(response.packages[0].packageId).toBe(client.gatewayObjects[0].packageId); + }); + + it('should deploy and execute a function', async () => { + const response = await client.deploy(path.join(__dirname, '../move/sample')); + + const tx = new TransactionBlock(); + const msg = 'hello from test'; + const msgBytes = new Uint8Array(Buffer.from(msg, 'utf8')); + + tx.moveCall({ + target: `${response.packages[0].packageId}::hello_world::execute`, + arguments: [tx.pure('0x0'), tx.pure('Avalanche'), tx.pure('0x0'), tx.pure(toHEX(msgBytes))], + }); + await client.execute(tx); + + const { data } = await client.queryEvents({ + query: { + MoveModule: { + module: `hello_world`, + package: response.packages[0].packageId, + }, + }, + limit: 1, + }); + + const updatedMessage = (data[0].parsedJson as any).updated_message; + + // query the value + expect(updatedMessage).toEqual(msg); + }); +}); diff --git a/packages/axelar-local-dev-sui/__tests__/e2e.spec.ts b/packages/axelar-local-dev-sui/__tests__/e2e.spec.ts new file mode 100644 index 00000000..5038d8b0 --- /dev/null +++ b/packages/axelar-local-dev-sui/__tests__/e2e.spec.ts @@ -0,0 +1,89 @@ +import { Contract, ethers } from 'ethers'; +import { TransactionBlock } from '@mysten/sui.js/transactions'; +import { Network, createNetwork, deployContract, EvmRelayer, RelayerType } from '@axelar-network/axelar-local-dev'; +import { SuiNetwork, SuiRelayer, initSui } from '@axelar-network/axelar-local-dev-sui'; +import path from 'path'; + +describe('e2e', () => { + let client: SuiNetwork; + let relayer: SuiRelayer; + let evmNetwork: Network; + let evmContract: Contract; + const evmChainName = 'Avalanche'; + const Executable = require('../artifacts/contracts/TestExecutable.sol/TestExecutable.json'); + + beforeEach(async () => { + const response = await initSui(); + client = response.suiNetwork; + relayer = response.suiRelayer; + + evmNetwork = await createNetwork({ + name: evmChainName, + }); + }); + + it('should be able to relay from sui to evm', async () => { + // deploy a contract on Avalanche + evmContract = await deployContract(evmNetwork.userWallets[0], Executable, [ + evmNetwork.gateway.address, + evmNetwork.gasService.address, + ]); + + console.log('Deployed contract on Avalanche: ', evmContract.address); + + // Deploy a sample module + const response = await client.deploy(path.join(__dirname, '../move/sample')); + const msg = 'hello from sui'; + + const payload = ethers.utils.defaultAbiCoder.encode(['string'], [msg]); + + // Send a callContract transaction + const tx = new TransactionBlock(); + tx.moveCall({ + target: `${response.packages[0].packageId}::hello_world::call`, + arguments: [tx.pure(evmChainName), tx.pure(evmContract.address), tx.pure(payload), tx.pure(1)], + }); + await client.execute(tx); + + await relayer.relay(); + + const updatedMsg = await evmContract.value(); + expect(updatedMsg).toEqual(msg); + }); + + it('should be able to relay from evm to sui', async () => { + const evmRelayer = new EvmRelayer(); + evmRelayer.setRelayer(RelayerType.Sui, relayer); + + // deploy a contract on Avalanche + evmContract = await deployContract(evmNetwork.userWallets[0], Executable, [ + evmNetwork.gateway.address, + evmNetwork.gasService.address, + ]); + + // Deploy a sample module + const response = await client.deploy(path.join(__dirname, '../move/sample')); + + // add sibling + await evmContract.addSibling('sui', `${response.packages[0].packageId}::hello_world`); + await evmContract.set('sui', 'hello from evm', { + value: 10000000, + }); + + await evmRelayer.relay(); + + const { data } = await client.queryEvents({ + query: { + MoveModule: { + module: `hello_world`, + package: response.packages[0].packageId, + }, + }, + limit: 1, + }); + + const updatedMessage = (data[0].parsedJson as any).updated_message; + + expect(updatedMessage).toEqual('hello from evm'); + }); +}); diff --git a/packages/axelar-local-dev-sui/__tests__/relayer.spec.ts b/packages/axelar-local-dev-sui/__tests__/relayer.spec.ts new file mode 100644 index 00000000..d6d1fcfd --- /dev/null +++ b/packages/axelar-local-dev-sui/__tests__/relayer.spec.ts @@ -0,0 +1,43 @@ +import { SuiNetwork } from '../src/SuiNetwork'; +import { SuiRelayer } from '../src/SuiRelayer'; +import path from 'path'; +import { ethers } from 'ethers'; +import { TransactionBlock } from '@mysten/sui.js/transactions'; + +describe('relayer', () => { + let client: SuiNetwork; + let relayer: SuiRelayer; + + beforeEach(async () => { + client = new SuiNetwork(); + await client.init(); + relayer = new SuiRelayer(client); + + // initialize commands for testing + relayer['commands']['Avalanche'] = []; + }); + + it('should update command list for sui -> evm call_contract transaction', async () => { + expect(relayer['commands']['Avalanche'].length).toBe(0); + + // Deploy a sample module + const response = await client.deploy(path.join(__dirname, '../move/sample')); + + const msg = 'hello from sui'; + const payload = ethers.utils.defaultAbiCoder.encode(['string'], [msg]); + + // Send a callContract transaction + const tx = new TransactionBlock(); + tx.moveCall({ + target: `${response.packages[0].packageId}::hello_world::call`, + arguments: [tx.pure('Avalanche'), tx.pure('0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789'), tx.pure(payload), tx.pure(1)], + }); + await client.execute(tx); + + // Update callContract events + await relayer.updateEvents(); + + // Check if the command is added to the relayer + expect(relayer['commands']['Avalanche'].length).toBe(1); + }); +}); diff --git a/packages/axelar-local-dev-sui/contracts/TestExecutable.sol b/packages/axelar-local-dev-sui/contracts/TestExecutable.sol new file mode 100644 index 00000000..4eb15675 --- /dev/null +++ b/packages/axelar-local-dev-sui/contracts/TestExecutable.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IAxelarGasService } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol'; +import { AxelarExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol'; + +contract TestExecutable is AxelarExecutable { + string public value; + string public sourceChain; + string public sourceAddress; + IAxelarGasService public immutable gasService; + mapping(string => string) public siblings; + + constructor(address gateway_, address gasService_) AxelarExecutable(gateway_) { + gasService = IAxelarGasService(gasService_); + } + + // Call this function on setup to tell this contract who it's sibling contracts are. + function addSibling(string calldata chain_, string calldata address_) external { + siblings[chain_] = address_; + } + + + // Call this function to update the value of this contract along with all its siblings'. + function set(string memory chain, string calldata value_) external payable { + value = value_; + bytes memory payload = abi.encodePacked(value_); + if (msg.value > 0) { + gasService.payNativeGasForContractCall{ value: msg.value }(address(this), chain, siblings[chain], payload, msg.sender); + } + gateway.callContract(chain, siblings[chain], payload); + } + + /* Handles calls created by setAndSend. Updates this contract's value + and gives the token received to the destination specified at the source chain. */ + function _execute( + string calldata sourceChain_, + string calldata sourceAddress_, + bytes calldata payload_ + ) internal override { + (value) = abi.decode(payload_, (string)); + sourceChain = sourceChain_; + sourceAddress = sourceAddress_; + } +} diff --git a/packages/axelar-local-dev-sui/docs/develop_sui_module.md b/packages/axelar-local-dev-sui/docs/develop_sui_module.md new file mode 100644 index 00000000..09b0aed4 --- /dev/null +++ b/packages/axelar-local-dev-sui/docs/develop_sui_module.md @@ -0,0 +1,89 @@ +# Developing the Sui Module + +To develop a module that is compatible with `axelar-local-dev-sui`, follow the guidelines outlined in this document. We will walk you through creating a module similar to the `HelloWorld` module illustrated here. + +Before you begin, ensure that your `Move.toml` file is correctly configured. Here is a template that you might find useful: + +```toml +[package] +name = "sui-example" +version = "0.0.1" + +[dependencies] +Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "77a9e0d"} +axelar = { git = "https://github.com/axelarnetwork/axelar-local-dev.git", subdir = "packages/axelar-local-dev-sui/move/axelar", rev = "ae68c9c" } + +[addresses] +sui_example = "0x0" +sui = "0x2" +``` + +With your `Move.toml` set up, you can proceed with the module development which primarily involves two key steps: + +1. **Sending Messages from Sui to the EVM Chain:** Implement the `call` function to facilitate this. The function should invoke `gateway::call_contract`. Reference the gateway module's implementation [here](../move/axelar/sources/gateway.move) for details. + +2. **Receiving Messages from the EVM Chain:** Implement the `execute` function to enable message reception. It should have parameters arranged in the following order: + +``` +_command_id: vector, _source_chain: String, _source_address: String, payload: vector, ctx: &mut TxContext +``` + +Below, you'll find a sample module, `axelar_sui_sample::hello_world`, which showcases the implementation of these requirements: + +```move +module axelar_sui_sample::hello_world { +use std::string::{utf8, String}; +use sui::object::{Self, ID, UID}; +use sui::transfer; +use sui::hex::{decode}; +use sui::event::emit; +use axelar::gateway; +use sui::tx_context::{TxContext}; + +struct MessageChangeEvent has copy, drop { + id: ID, + updated_message: String, +} + +struct MessageHolder has key { + id: UID, + message: String, +} + +fun init(tx: &mut TxContext) { + transfer::share_object(MessageHolder { + id: object::new(tx), + message: utf8(b"init"), + }); +} + +public fun get_message(messageHolder: &MessageHolder): String { + messageHolder.message +} + +// The "call" entry function is essential, and it must invoke `gateway::call_contract` with the parameters specified below: +public entry fun call(destination_chain: vector, destination_address: vector, payload: vector, _fee_amount: u64) { + gateway::call_contract(destination_chain, destination_address, payload); +} + +// Implement the "execute" entry function with the following exact parameters: +public entry fun execute(_command_id: vector, _source_chain: String, _source_address: String, payload: vector, ctx: &mut TxContext) { + // TODO: Bypass command_id verification with the gateway module for now + + let message = utf8(decode(payload)); + let event = MessageHolder { + id: object::new(ctx), + message + }; + + emit(MessageChangeEvent { + id: object::uid_to_inner(&event.id), + updated_message: event.message, + }); + + transfer::share_object(event); +} +} +``` + +Feel free to refer to the HelloWorld module and adapt this template according to your specific needs and functionalities. diff --git a/packages/axelar-local-dev-sui/docs/evm_to_sui.md b/packages/axelar-local-dev-sui/docs/evm_to_sui.md new file mode 100644 index 00000000..a9999c29 --- /dev/null +++ b/packages/axelar-local-dev-sui/docs/evm_to_sui.md @@ -0,0 +1,133 @@ +## Relay from Evm to Sui + +In this guide, we demonstrate how to facilitate a transaction relay from an EVM network to the Sui network. We will walk through the steps of initializing relayers, deploying contracts and modules, and finally executing a function on the EVM contract and relaying the transaction to the Sui network. + +### Step 1: Import Necessary Packages + +```ts +import { createSuiRelayer, RelayerType, initSui } from '@axelar-network/axelar-local-dev-sui'; +import { EvmRelayer, createNetwork, deployContract } from '@axelar-network/axelar-local-dev +import { ethers } from 'ethers'; +import path from 'path'; +``` + +### Step 2: Initialize Sui and EVM Relayers + +Initialize the Sui and EVM relayers to set up the communication between the networks. + +```ts +const { suiRelayer, suiClient } = await initSui(); +const evmRelayer = new EvmRelayer(); + +// Establish the EVM relayer to Sui relayer connection to enable transaction relays to the Sui network. +evmRelayer.setRelayer(RelayerType.sui, suiRelayer); +``` + +## Step 3: Create an EVM Network + +Create a new EVM network, here named "Evm1", to deploy your contracts. + +```ts +const evmNetwork = await createNetwork({ + name: 'Evm1', +}); +``` + +## Step 4: Deploy a Contract on the EVM Network + +Deploy a contract on the newly created EVM network using the necessary executable file. + +```ts +const Executable = require('path/to/contract_json'); +const evmContract = await deployContract(evmNetwork.userWallets[0], Executable, [ + evmNetwork.gateway.address, + evmNetwork.gasService.address, +]); +``` + +## Step 5: Deploy a Module on the Sui Network + +Specify the path to your folder containing the `Move.toml` file and deploy a module on the Sui network. + +```ts +const pathToModule = '..'; // Specify the path to your move folder containing the `Move.toml` file. +const response = await suiClient.deploy(path.join(__dirname, pathToModule)); + +// Tip: You can extract the transaction digest from the `response` to view deployment details at: https://suiexplorer.com/?network=local +``` + +## Step 6: Execute a Function on the EVM Contract + +Using the [TestExecutable](../contracts/TestExecutable.sol) contract as a reference, execute a function to set a message on the EVM contract and add a sibling. + +```ts +await evmContract.addSibling('sui', `${response.packages[0].packageId}::hello_world`); +await evmContract.set('sui', 'hello from evm', { + value: 10000000, // Note: This is a hardcoded relayer fee; currently, the fee is not checked, so any value can be specified. +}); +``` + +## Step 7: Relay the Transaction to the Sui Network + +To finalize, relay the transaction from the EVM network to the Sui network. + +```ts +await evmRelayer.relay(); +``` + +This completes your guide on relaying transactions from EVM to Sui. Ensure to test your setup adequately to confirm successful configuration and relay transactions. + +## Full Example + +```ts +import { createSuiRelayer, RelayerType, initSui } from '@axelar-network/axelar-local-dev-sui'; +import { EvmRelayer, createNetwork, deployContract } from '@axelar-network/axelar-local-dev'; +import { ethers } from 'ethers'; +import path from 'path'; + +async function main() { + // Initialize SuiRelayer and EvmRelayer + const { suiRelayer, suiClient } = await initSui(); + const evmRelayer = new EvmRelayer(); + + // Set SuiRelayer to EvmRelayer to allow relaying transactions to Sui Network + evmRelayer.setRelayer(RelayerType.sui, suiRelayer); + + // Create an Evm network named "Evm1" + const evmNetwork = await createNetwork({ name: 'Evm1' }); + + // Deploy a contract on Evm1 + const Executable = require('path/to/contract_json'); + const evmContract = await deployContract(evmNetwork.userWallets[0], Executable, [ + evmNetwork.gateway.address, + evmNetwork.gasService.address, + ]); + + // Deploy a module on Sui + const pathToModule = '..'; + const response = await suiClient.deploy(path.join(__dirname, pathToModule)); + + // Execute a function on the Evm contract + await evmContract.addSibling('sui', `${response.packages[0].packageId}::hello_world`); + await evmContract.set('sui', 'hello from evm', { value: 10000000 }); + + // Relay the transaction to Sui + await evmRelayer.relay(); + + // Print message on Sui module + const { data } = await suiClient.queryEvents({ + query: { + MoveModule: { + module: `hello_world`, + package: response.packages[0].packageId, + }, + }, + limit: 1, + }); + + console.log(data[0].parsedJson.updated_message); +} + +// Execute the main function +main().catch(console.error); +``` diff --git a/packages/axelar-local-dev-sui/docs/sui_to_evm.md b/packages/axelar-local-dev-sui/docs/sui_to_evm.md new file mode 100644 index 00000000..9c09e561 --- /dev/null +++ b/packages/axelar-local-dev-sui/docs/sui_to_evm.md @@ -0,0 +1,137 @@ +# Relay from Sui to Evm + +This guide delineates the procedure to orchestrate a transaction relay from the Sui network to an Evm network. Follow the steps below to initialize relayers, deploy modules and contracts, and execute functions on the Sui module before relaying the transaction to the Evm network. + +### Step 1: Import Necessary Modules + +Start by importing the necessary modules that provide the functions and objects you'll use throughout the script. + +```ts +import { createSuiRelayer, RelayerType, initSui } from '@axelar-network/axelar-local-dev-sui'; +import { EvmRelayer, createNetwork, deployContract } from '@axelar-network/axelar-local-dev'; +import { ethers } from 'ethers'; +import path from 'path'; +``` + +### Step 2: Initialize Sui and EVM Relayers + +Next, initialize both the Sui and EVM relayers to facilitate communication between the two networks. + +```ts +const { suiRelayer, suiClient } = await initSui(); +const evmRelayer = new EvmRelayer(); + +// Set the Sui relayer as the relay conduit to the EVM network. +evmRelayer.setRelayer(RelayerType.sui, suiRelayer); +``` + +### Step 3: Set Up an EVM Network + +Set up a new EVM network, which we will refer to as "Evm1", where you will deploy your contracts. + +```ts +const evmNetwork = await createNetwork({ + name: 'Evm1', +}); +``` + +### Step 4: Deploy a Contract on the EVM Network + +Now, deploy a contract on the freshly created EVM network using the appropriate executable file. + +```ts +const Executable = require('path/to/contract_json'); +const evmContract = await deployContract(evmNetwork.userWallets[0], Executable, [ + evmNetwork.gateway.address, + evmNetwork.gasService.address, +]); +``` + +### Step 5: Deploy a Module on the Sui Network + +Proceed to deploy a module on the Sui network. Ensure to specify the path to your folder housing the `Move.toml` file. + +```ts +const pathToModule = '..'; +const response = await suiClient.deploy(path.join(__dirname, pathToModule)); + +// Hint: You can retrieve the transaction digest from the `response` for deployment details at: https://suiexplorer.com/?network=local +``` + +### Step 6: Execute a Function on the Sui Module + +Using the [hello_world](../move/sample/sources/hello_world.move) module as an example, create a transaction block and execute a function call on the Sui module, incorporating necessary arguments. + +```ts +const tx = new TransactionBlock(); +const payload = ethers.utils.defaultAbiCoder.encode(['string'], ['hello from sui']); +tx.moveCall({ + target: `${response.packages[0].packageId}::hello_world::call`, + arguments: [tx.pure(evmNetwork.name), tx.pure(evmContract.address), tx.pure(payload), tx.pure(1)], +}); + +// Transmit the transaction to the Sui network. +await suiClient.execute(tx); +``` + +### Step 7: Relay the Transaction to the EVM Network + +To wrap up, relay the transaction from the Sui network to the EVM network, effectively completing the relay process. + +```ts +await suiRelayer.relay(); +``` + +You have now successfully navigated through the steps necessary for relaying transactions from Sui to EVM. Remember to conduct sufficient tests to verify the successful setup and relay of transactions. + +## Full Example + +```ts +import { createSuiRelayer, RelayerType, initSui } from '@axelar-network/axelar-local-dev-sui'; +import { EvmRelayer, createNetwork, deployContract } from '@axelar-network/axelar-local-dev'; +import { ethers } from 'ethers'; +import path from 'path'; + +async function main() { + // Initialize SuiRelayer and EvmRelayer + const { suiRelayer, suiClient } = await initSui(); + const evmRelayer = new EvmRelayer(); + + // Set SuiRelayer to EvmRelayer to facilitate relaying transactions to the Sui Network + evmRelayer.setRelayer(RelayerType.sui, suiRelayer); + + // Create an Evm network named "Evm1" + const evmNetwork = await createNetwork({ name: 'Evm1' }); + + // Deploy a contract on Evm1 + const Executable = require('path/to/contract_json'); + const evmContract = await deployContract(evmNetwork.userWallets[0], Executable, [ + evmNetwork.gateway.address, + evmNetwork.gasService.address, + ]); + + // Deploy a module on Sui + const pathToModule = '..'; + const response = await suiClient.deploy(path.join(__dirname, pathToModule)); + + // Execute a function on the Sui module + const tx = new TransactionBlock(); + const payload = ethers.utils.defaultAbiCoder.encode(['string'], ['hello from sui']); + tx.moveCall({ + target: `${response.packages[0].packageId}::hello_world::call`, + arguments: [tx.pure(evmNetwork.name), tx.pure(evmContract.address), tx.pure(payload), tx.pure(1)], + }); + + // Send the transaction to the Sui network + await suiClient.execute(tx); + + // Relay the transaction to the Evm1 chain + await suiRelayer.relay(); + + // Print the message from the contract on Evm1 chain + console.log('Updated Message:', await evmContract.value()); +} + +// Execute the main function and catch any errors +main().catch(console.error); +``` diff --git a/packages/axelar-local-dev-sui/hardhat.config.js b/packages/axelar-local-dev-sui/hardhat.config.js new file mode 100644 index 00000000..953a5978 --- /dev/null +++ b/packages/axelar-local-dev-sui/hardhat.config.js @@ -0,0 +1,18 @@ +module.exports = { + solidity: { + version: '0.8.9', + settings: { + evmVersion: process.env.EVM_VERSION || 'london', + optimizer: { + enabled: true, + runs: 1000, + }, + }, + }, + paths: { + sources: './contracts', + }, + mocha: { + timeout: 200000, + }, +}; diff --git a/packages/axelar-local-dev-sui/jest.config.js b/packages/axelar-local-dev-sui/jest.config.js new file mode 100644 index 00000000..4419682a --- /dev/null +++ b/packages/axelar-local-dev-sui/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + transform: { + '^.+\\.ts?$': 'ts-jest', + }, + testRegex: '/__tests__/.*\\.(test|spec)?\\.(ts)$', + transformIgnorePatterns: ['/node_modules/'], + testTimeout: 300000, +}; diff --git a/packages/axelar-local-dev-sui/move/axelar/Move.lock b/packages/axelar-local-dev-sui/move/axelar/Move.lock new file mode 100644 index 00000000..5f3046db --- /dev/null +++ b/packages/axelar-local-dev-sui/move/axelar/Move.lock @@ -0,0 +1,22 @@ +# @generated by Move, please check-in and do not edit manually. + +[move] +version = 0 +manifest_digest = "41863A8E1B55929B6713FF8B6C3AA6096DF15222639F12D05BC7DAB5FB4EC4C5" +deps_digest = "112928C94A84031C09CD9B9D1D44B149B73FC0EEA5FA8D8B2D7CA4D91936335A" + +dependencies = [ + { name = "Sui" }, +] + +[[move.package]] +name = "MoveStdlib" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "77a9e0d", subdir = "crates/sui-framework/packages/move-stdlib" } + +[[move.package]] +name = "Sui" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "77a9e0d", subdir = "crates/sui-framework/packages/sui-framework" } + +dependencies = [ + { name = "MoveStdlib" }, +] diff --git a/packages/axelar-local-dev-sui/move/axelar/Move.toml b/packages/axelar-local-dev-sui/move/axelar/Move.toml new file mode 100644 index 00000000..4bf43836 --- /dev/null +++ b/packages/axelar-local-dev-sui/move/axelar/Move.toml @@ -0,0 +1,10 @@ +[package] +name = "axelar" +version = "0.0.1" + +[dependencies] +Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "77a9e0d"} + +[addresses] +axelar="0x0" +sui = "0x2" diff --git a/packages/axelar-local-dev-sui/move/axelar/sources/gateway.move b/packages/axelar-local-dev-sui/move/axelar/sources/gateway.move new file mode 100644 index 00000000..9a52b612 --- /dev/null +++ b/packages/axelar-local-dev-sui/move/axelar/sources/gateway.move @@ -0,0 +1,23 @@ +// Todo: remove this file once we have a proper gateway module +module axelar::gateway { + use sui::event::emit; + use sui::hash::keccak256; + + struct ContractCall has copy, drop { + source: vector, + destination_chain: vector, + destination_address: vector, + payload: vector, + payload_hash: vector, + } + + public fun call_contract(destination_chain: vector, destination_address: vector, payload: vector) { + emit(ContractCall { + source: b"sui", + destination_chain, + destination_address, + payload, + payload_hash: keccak256(&payload) + }); + } +} diff --git a/packages/axelar-local-dev-sui/move/sample/Move.lock b/packages/axelar-local-dev-sui/move/sample/Move.lock new file mode 100644 index 00000000..e2199033 --- /dev/null +++ b/packages/axelar-local-dev-sui/move/sample/Move.lock @@ -0,0 +1,31 @@ +# @generated by Move, please check-in and do not edit manually. + +[move] +version = 0 +manifest_digest = "40E7CC48C373DB6DD2C189111F63B77B78F93EE3EA2E1256D46DF2BD70069755" +deps_digest = "C6C93A018C0B43F8AA8899B23B42E757DF9300785637696481F59EBFA35625D2" + +dependencies = [ + { name = "Sui" }, + { name = "axelar" }, +] + +[[move.package]] +name = "MoveStdlib" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "77a9e0d", subdir = "crates/sui-framework/packages/move-stdlib" } + +[[move.package]] +name = "Sui" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "77a9e0d", subdir = "crates/sui-framework/packages/sui-framework" } + +dependencies = [ + { name = "MoveStdlib" }, +] + +[[move.package]] +name = "axelar" +source = { local = "../axelar" } + +dependencies = [ + { name = "Sui" }, +] diff --git a/packages/axelar-local-dev-sui/move/sample/Move.toml b/packages/axelar-local-dev-sui/move/sample/Move.toml new file mode 100644 index 00000000..e2a43fdd --- /dev/null +++ b/packages/axelar-local-dev-sui/move/sample/Move.toml @@ -0,0 +1,11 @@ +[package] +name = "axelar_sui_sample" +version = "0.0.1" + +[dependencies] +Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "77a9e0d"} +axelar = { local = "../axelar" } + +[addresses] +axelar_sui_sample = "0x0" +sui = "0x2" diff --git a/packages/axelar-local-dev-sui/move/sample/sources/hello_world.move b/packages/axelar-local-dev-sui/move/sample/sources/hello_world.move new file mode 100644 index 00000000..b9f92000 --- /dev/null +++ b/packages/axelar-local-dev-sui/move/sample/sources/hello_world.move @@ -0,0 +1,51 @@ +module axelar_sui_sample::hello_world { + use std::string::{utf8, String}; + use sui::object::{Self, ID, UID}; + use sui::transfer; + use sui::hex::{decode}; + use sui::event::emit; + use axelar::gateway; + use sui::tx_context::{TxContext}; + + struct MessageChangeEvent has copy, drop { + id: ID, + updated_message: String, + } + + struct MessageHolder has key { + id: UID, + message: String, + } + + fun init(tx: &mut TxContext) { + transfer::share_object(MessageHolder { + id: object::new(tx), + message: utf8(b"init"), + }); + } + + public fun get_message(messageHolder: &MessageHolder): String { + messageHolder.message + } + + public entry fun call(destination_chain: vector, destination_address: vector, payload: vector, _fee_amount: u64) { + gateway::call_contract(destination_chain, destination_address, payload); + } + + public entry fun execute(_command_id: vector, _source_chain: String, _source_address: String, payload: vector, ctx: &mut TxContext) { + // TODO: skip checking command_id with gateway module for now + + let message = utf8(decode(payload)); + let event = MessageHolder { + id: object::new(ctx), + message + }; + + emit(MessageChangeEvent { + id: object::uid_to_inner(&event.id), + updated_message: event.message, + }); + + transfer::share_object(event); + } +} diff --git a/packages/axelar-local-dev-sui/package.json b/packages/axelar-local-dev-sui/package.json new file mode 100644 index 00000000..97ea7a7e --- /dev/null +++ b/packages/axelar-local-dev-sui/package.json @@ -0,0 +1,26 @@ +{ + "name": "@axelar-network/axelar-local-dev-sui", + "version": "2.1.0", + "main": "dist/index.js", + "files": [ + "dist/", + "!dist/types", + "!dist/artifacts" + ], + "scripts": { + "test": "jest", + "clean": "rm -rf src/types dist artifacts", + "prettier": "prettier --write 'src/**/*.ts'", + "build": "npm run clean && npm run build-ts && npm run build-contract", + "build-ts": "tsc", + "build-contract": "hardhat compile", + "build-sui": "cd move && sui move build" + }, + "dependencies": { + "@axelar-network/axelar-local-dev": "2.1.0", + "@mysten/sui.js": "^0.41.0" + }, + "author": "euro@axelar.network", + "license": "ISC", + "description": "" +} diff --git a/packages/axelar-local-dev-sui/src/Command.ts b/packages/axelar-local-dev-sui/src/Command.ts new file mode 100644 index 00000000..6cea6d35 --- /dev/null +++ b/packages/axelar-local-dev-sui/src/Command.ts @@ -0,0 +1,47 @@ +import { ethers } from 'ethers'; +import { CallContractArgs, RelayData } from '@axelar-network/axelar-local-dev'; +import { SuiNetwork } from './SuiNetwork'; +import { TransactionBlock } from '@mysten/sui.js/transactions'; + +const { defaultAbiCoder } = ethers.utils; + +export class Command { + commandId: string; + name: string; + data: any[]; + encodedData: string; + post: ((options: any) => Promise) | undefined; + + constructor( + commandId: string, + name: string, + data: any[], + dataSignature: string[], + post: ((options: any) => Promise) | undefined = undefined, + chain: string | null = null, + ) { + this.commandId = commandId; + this.name = name; + this.data = data; + this.encodedData = chain === 'sui' && name === 'approve_contract_call' ? '' : defaultAbiCoder.encode(dataSignature, data); + this.post = post; + } + + static createContractCallCommand = (commandId: string, suiNetwork: SuiNetwork, relayData: RelayData, args: CallContractArgs) => { + return new Command( + commandId, + 'approve_contract_call', + [args.from, args.sourceAddress, args.destinationContractAddress, args.payloadHash, args.payload], + [], + async () => { + const tx = new TransactionBlock(); + tx.moveCall({ + target: `${args.destinationContractAddress}::execute` as any, + arguments: [tx.pure(commandId), tx.pure(args.from), tx.pure(args.sourceAddress), tx.pure(args.payload.slice(2))], + }); + return suiNetwork.execute(tx); + }, + 'sui', + ); + }; +} diff --git a/packages/axelar-local-dev-sui/src/SuiNetwork.ts b/packages/axelar-local-dev-sui/src/SuiNetwork.ts new file mode 100644 index 00000000..997d8f05 --- /dev/null +++ b/packages/axelar-local-dev-sui/src/SuiNetwork.ts @@ -0,0 +1,198 @@ +import { CoinBalance, SuiEvent, SuiClient, getFullnodeUrl, SuiTransactionBlockResponseOptions } from '@mysten/sui.js/client'; +import { Ed25519Keypair } from '@mysten/sui.js/keypairs/ed25519'; +import { Keypair } from '@mysten/sui.js/cryptography'; +import { requestSuiFromFaucetV0, getFaucetHost } from '@mysten/sui.js/faucet'; +import { TransactionBlock } from '@mysten/sui.js/transactions'; +import { execSync, exec } from 'child_process'; +import { PublishedPackage } from './types'; + +/** + * `SuiNetwork` class provides methods and functionalities to interact with the Sui network. + * It extends the functionalities of `SuiClient`, offering a higher-level abstraction for + * various network operations. + * + * @extends {SuiClient} + * + * @property {Ed25519Keypair} executor - Represents the keypair of the executor. + * @property {string} faucetUrl - URL of the faucet for fetching tokens. + * @property {string} nodeUrl - URL of the node to connect to the Sui network. + * @property {PublishedPackage[]} gatewayObjects - Array to store packages compatible with gateway. + * + * Main Features: + * - Initialize and fund the executor account. + * - Deploy modules to the network. + * - Execute transaction with optional configurations. + * - Query gateway events within a specified time range. + */ +export class SuiNetwork extends SuiClient { + private executor: Ed25519Keypair; + private faucetUrl: string; + public nodeUrl: string; + public gatewayObjects: PublishedPackage[] = []; + + /** + * Constructs an instance of SuiNetwork + * + * @param nodeUrl - Optional node URL; defaults to localnet if not provided + * @param faucetUrl - Optional faucet URL; defaults to localnet if not provided + */ + constructor(nodeUrl?: string, faucetUrl?: string) { + super({ url: nodeUrl || getFullnodeUrl('localnet') }); + this.nodeUrl = nodeUrl || getFullnodeUrl('localnet'); + this.faucetUrl = faucetUrl || getFaucetHost('localnet'); + this.executor = new Ed25519Keypair(); + } + + /** + * Initialize the SuiNetwork by funding the executor account + */ + async init() { + // Fund executor account + await this.fundWallet(this.getExecutorAddress()); + } + + /** + * Request funds for a given wallet from the Sui faucet + * + * @param address - The address of the wallet to fund + * @returns A Promise that resolves once the funding request is made + */ + public fundWallet(address: string) { + return requestSuiFromFaucetV0({ + host: this.faucetUrl, + recipient: address, + }); + } + + /** + * Builds and deploys a module from a specified path + * + * @param modulePath - Path to the module containing a Move.toml file + * @param senderAddress - Optional sender address; defaults to executor address if not provided + * @returns A Promise with transaction details and published packages + */ + public async deploy(modulePath: string, senderAddress: string = this.getExecutorAddress()) { + if (!(await this.suiCommandExist())) { + throw new Error('Please install sui command'); + } + + const { modules, dependencies } = JSON.parse( + execSync(`sui move build --dump-bytecode-as-base64 --path ${modulePath} --with-unpublished-dependencies`, { + encoding: 'utf-8', + stdio: 'pipe', // silent the output + }), + ); + + const tx = new TransactionBlock(); + const [upgradeCap] = tx.publish({ + modules, + dependencies, + }); + tx.transferObjects([upgradeCap], tx.pure(senderAddress)); + + const result = await this.execute(tx); + + const publishedPackages = result.objectChanges + ?.filter((change) => change.type === 'published') + ?.map((change: any) => { + return { + packageId: change.packageId, + modules: change.modules, + deployedAt: Date.now(), + }; + }); + + if (!publishedPackages || publishedPackages.length === 0) { + throw new Error('No published packages'); + } + + // add gateway compatible modules + this.gatewayObjects.push(...publishedPackages.filter((p: any) => p.modules.includes('gateway'))); + + return { + digest: result.digest, + packages: publishedPackages, + }; + } + + /** + * Signs and executes a transaction block + * + * @param tx - The transaction block to execute + * @param keypair - Optional keypair for signing; defaults to executor keypair if not provided + * @param options - Optional settings for the transaction execution response + * @returns A Promise with details of the transaction execution + */ + public async execute(tx: TransactionBlock, keypair: Keypair = this.executor, options?: SuiTransactionBlockResponseOptions) { + // todo: add check for sui command + return this.signAndExecuteTransactionBlock({ + signer: keypair || this.executor, + transactionBlock: tx, + options: { + showObjectChanges: true, + showInput: true, + showEffects: true, + showBalanceChanges: true, + showEvents: true, + showRawInput: true, + ...options, + }, + }); + } + + /** + * Queries for gateway events within a specified time range + * + * @param startTime - Optional start time for the query; defaults to 1 minute ago if not provided + * @param endTime - Optional end time for the query; defaults to current time if not provided + * @returns A Promise with the filtered gateway events + */ + public async queryGatewayEvents(startTime?: string, endTime?: string) { + const now = Date.now(); + const events = await this.queryEvents({ + query: { + TimeRange: { + startTime: startTime || (now - 1000 * 60).toString(), + endTime: endTime || now.toString(), + }, + }, + }); + + return events.data.filter((e) => e.type.includes('gateway::ContractCall')); + } + + /** + * Fetches the address of the executor + * + * @returns The address of the executor + */ + public getExecutorAddress(): string { + return this.executor.toSuiAddress(); + } + + /** + * Fetches the balance of the executor account + * + * @returns A Promise with the balance details of the executor account + */ + public getExecutorBalance(): Promise { + return this.getBalance({ + owner: this.executor.toSuiAddress(), + }); + } + + /** + * Checks if the `sui` command is available in the system + * + * @returns A Promise that resolves to true if the command exists, false otherwise + */ + public async suiCommandExist(): Promise { + try { + const platformCmd = process.platform === 'win32' ? 'where' : 'which'; + await exec(`${platformCmd} sui`); + return true; + } catch (err) { + return false; + } + } +} diff --git a/packages/axelar-local-dev-sui/src/SuiRelayer.ts b/packages/axelar-local-dev-sui/src/SuiRelayer.ts new file mode 100644 index 00000000..b8a23dd2 --- /dev/null +++ b/packages/axelar-local-dev-sui/src/SuiRelayer.ts @@ -0,0 +1,167 @@ +import { arrayify, defaultAbiCoder, hexlify, keccak256 } from 'ethers/lib/utils'; +import { + logger, + getSignedExecuteInput, + RelayCommand, + Network, + networks, + Command, + Relayer, + RelayerType, + CallContractArgs, + RelayData, +} from '@axelar-network/axelar-local-dev'; +import { Command as SuiCommand } from './Command'; +import { SuiNetwork } from './SuiNetwork'; +import { getCommandId } from './utils'; + +const DEFAULT_GAS_LIMIT = BigInt(8e6); + +export class SuiRelayer extends Relayer { + private suiNetwork: SuiNetwork; + private lastQueryMs: number = new Date().getTime(); + private textDecoder = new TextDecoder('utf-8'); + + constructor(suiNetwork: SuiNetwork) { + super(); + this.suiNetwork = suiNetwork; + } + + setRelayer(type: RelayerType, relayer: Relayer) { + if (type === 'near' || type === 'aptos') { + return console.log(`${type} not supported yet`); + } + + this.otherRelayers[type] = relayer; + } + + // no-op since the events will be listened with event subscription. + async updateEvents(): Promise { + await this.updateGasEvents(); + await this.updateCallContractEvents(); + } + + async execute(commands: RelayCommand) { + await this.executeSuiToEvm(commands); + await this.executeEvmToSui(commands); + } + + async executeSuiToEvm(commandList: RelayCommand) { + for (const to of networks) { + const commands = commandList[to.name]; + if (commands.length === 0) continue; + + const execution = await this.executeEvmGateway(to, commands); + await this.executeEvmExecutable(to, commands, execution); + } + } + + private async executeEvmToSui(commands: RelayCommand) { + const toExecute = commands['sui']; + if (!toExecute || toExecute?.length === 0) return; + + await this.executeSuiGateway(toExecute); + await this.executeSuiExecutable(toExecute); + } + + private async executeSuiGateway(commands: Command[]) { + // TODO: Send approve_contract_call tx to Axelar Gateway + } + + private async executeSuiExecutable(commands: Command[]) { + for (const command of commands) { + if (!command.post) continue; + + await command.post({}); + } + } + + private async executeEvmGateway(to: Network, commands: Command[]): Promise { + const data = arrayify( + defaultAbiCoder.encode( + ['uint256', 'bytes32[]', 'string[]', 'bytes[]'], + [to.chainId, commands.map((com) => com.commandId), commands.map((com) => com.name), commands.map((com) => com.encodedData)], + ), + ); + const signedData = await getSignedExecuteInput(data, to.operatorWallet); + + return to.gateway + .connect(to.ownerWallet) + .execute(signedData, { gasLimit: DEFAULT_GAS_LIMIT }) + .then((tx: any) => tx.wait()); + } + + private async executeEvmExecutable(to: Network, commands: Command[], execution: any): Promise { + for (const command of commands) { + if (command.post == null) continue; + + if ( + !execution.events.find((event: any) => { + return event.event === 'Executed' && event.args[0] == command.commandId; + }) + ) + continue; + + try { + const blockLimit = Number((await to.provider.getBlock('latest')).gasLimit); + await command.post({ + gasLimit: BigInt(blockLimit), + }); + } catch (e) { + logger.log(e); + } + } + } + + // TODO: Implement querying gas events in the future when we integrate with the gas module. + private async updateGasEvents() {} + + private convertUint8ArrayToUtf8String(uint8Array: number[]) { + return this.textDecoder.decode(new Uint8Array(uint8Array)); + } + + private async updateCallContractEvents() { + const events = await this.suiNetwork.queryGatewayEvents((this.lastQueryMs + 1).toString()); + this.lastQueryMs = new Date().getTime(); + + for (const event of events) { + const commandId = getCommandId(event.id); + const eventParams = event.parsedJson as any; + + const { + destination_address: destinationAddress, + destination_chain: destinationChain, + payload, + payload_hash: _payloadHash, + } = eventParams; + + // TODO: Investigate why using the payload hash directly causes relay failure. + // Current workaround: use keccak256 hash of the payload. + const payloadHash = keccak256(this.convertUint8ArrayToUtf8String(payload)); + + const contractCallArgs: CallContractArgs = { + from: 'sui', + to: this.convertUint8ArrayToUtf8String(destinationChain), + destinationContractAddress: this.convertUint8ArrayToUtf8String(destinationAddress), + payload: this.convertUint8ArrayToUtf8String(payload), + sourceAddress: event.packageId, + transactionHash: event.id.txDigest, + // payloadHash: hexlify(_payloadHash), + payloadHash, + sourceEventIndex: parseInt(event.id.eventSeq), + }; + + this.relayData.callContract[commandId] = contractCallArgs; + const command = Command.createEVMContractCallCommand(commandId, this.relayData, contractCallArgs); + this.commands[contractCallArgs.to].push(command); + } + } + + createCallContractCommand(commandId: string, relayData: RelayData, contractCallArgs: CallContractArgs): Command { + return SuiCommand.createContractCallCommand(commandId, this.suiNetwork, relayData, contractCallArgs); + } + + createCallContractWithTokenCommand(): Command { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/axelar-local-dev-sui/src/index.ts b/packages/axelar-local-dev-sui/src/index.ts new file mode 100644 index 00000000..40d65a69 --- /dev/null +++ b/packages/axelar-local-dev-sui/src/index.ts @@ -0,0 +1,6 @@ +export * from './SuiNetwork'; +export * from './SuiRelayer'; +export * from './Command'; +export * from './utils'; +export * from './types'; +export * from './setup'; diff --git a/packages/axelar-local-dev-sui/src/setup.ts b/packages/axelar-local-dev-sui/src/setup.ts new file mode 100644 index 00000000..f18b25e0 --- /dev/null +++ b/packages/axelar-local-dev-sui/src/setup.ts @@ -0,0 +1,38 @@ +import { SuiNetwork } from './SuiNetwork'; +import { SuiRelayer } from './SuiRelayer'; + +export let suiNetwork: SuiNetwork; +export let suiRelayer: SuiRelayer; + +/** + * Initializes the Sui network and relayer instances. + * + * @param nodeUrl - The URL of the node, optional parameter. If not provided, SuiNetwork will use its default value. + * @param faucetUrl - The URL of the faucet, optional parameter. If not provided, SuiNetwork will use its default value. + * + * @returns A promise that resolves to an object containing initialized instances of SuiNetwork and SuiRelayer. + * + * @throws Will throw an error if the initialization of SuiNetwork or SuiRelayer fails. + */ +export async function initSui( + nodeUrl?: string, + faucetUrl?: string, +): Promise<{ + suiNetwork: SuiNetwork; + suiRelayer: SuiRelayer; +}> { + try { + suiNetwork = new SuiNetwork(nodeUrl, faucetUrl); + + await suiNetwork.init(); + + suiRelayer = new SuiRelayer(suiNetwork); + + return { suiNetwork, suiRelayer }; + } catch (error) { + console.error('Initialization failed due to the following error:', error); + throw new Error( + `Initialization failed: Please check if the node and faucet URLs are correctly configured and running. You may need to review the logs or documentation for more details.`, + ); + } +} diff --git a/packages/axelar-local-dev-sui/src/types.ts b/packages/axelar-local-dev-sui/src/types.ts new file mode 100644 index 00000000..57228822 --- /dev/null +++ b/packages/axelar-local-dev-sui/src/types.ts @@ -0,0 +1,4 @@ +export interface PublishedPackage { + packageId: string; + modules: string[]; +} diff --git a/packages/axelar-local-dev-sui/src/utils.ts b/packages/axelar-local-dev-sui/src/utils.ts new file mode 100644 index 00000000..157f70e0 --- /dev/null +++ b/packages/axelar-local-dev-sui/src/utils.ts @@ -0,0 +1,6 @@ +import { ethers } from 'ethers'; +import { EventId } from '@mysten/sui.js/client'; + +export const getCommandId = (event: EventId) => { + return ethers.utils.id([event.txDigest, event.eventSeq].join(':')); +}; diff --git a/packages/axelar-local-dev-sui/tsconfig.json b/packages/axelar-local-dev-sui/tsconfig.json new file mode 100644 index 00000000..926963cc --- /dev/null +++ b/packages/axelar-local-dev-sui/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + /* Language and Environment */ + "target": "es6" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "lib": ["es2020", "dom"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, + + /* Modules */ + "module": "CommonJS" /* Specify what module code is generated. */, + + "resolveJsonModule": true, + /* Emit */ + "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, + "declarationMap": true /* Create sourcemaps for d.ts files. */, + "sourceMap": true /* Create source map files for emitted JavaScript files. */, + "rootDirs": ["./src", "src/__test__"] /* Specify the root folder within your source files. */, + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + "strictNullChecks": true /* When type checking, take into account `null` and `undefined`. */, + + /* Completeness */ + "skipDefaultLibCheck": true /* Skip type checking .d.ts files that are included with TypeScript. */, + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "src/__tests__/"] +} diff --git a/packages/axelar-local-dev/package.json b/packages/axelar-local-dev/package.json index 65348e1b..e8857941 100644 --- a/packages/axelar-local-dev/package.json +++ b/packages/axelar-local-dev/package.json @@ -1,6 +1,6 @@ { "name": "@axelar-network/axelar-local-dev", - "version": "2.0.5", + "version": "2.1.0", "description": "", "main": "dist/index.js", "files": [ diff --git a/packages/axelar-local-dev/src/Network.ts b/packages/axelar-local-dev/src/Network.ts index ce56b0af..090d464a 100644 --- a/packages/axelar-local-dev/src/Network.ts +++ b/packages/axelar-local-dev/src/Network.ts @@ -1,5 +1,6 @@ 'use strict'; +import http from 'http'; import { ethers, Wallet, Contract, providers } from 'ethers'; import { logger } from './utils'; import { getSignedExecuteInput, getRandomID, deployContract } from './utils'; @@ -16,9 +17,6 @@ import { AxelarGateway__factory as AxelarGatewayFactory } from './types/factorie import { AxelarGateway } from './types/@axelar-network/axelar-cgp-solidity/contracts/AxelarGateway'; import { AxelarGasService__factory as AxelarGasServiceFactory } from './types/factories/@axelar-network/axelar-cgp-solidity/contracts/gas-service/AxelarGasService__factory'; import { AxelarGasService } from './types/@axelar-network/axelar-cgp-solidity/contracts/gas-service/AxelarGasService'; -import http from 'http'; -import { EvmRelayer } from './relay/EvmRelayer'; -import { evmRelayer } from './relay'; const ADDRESS_ZERO = '0x0000000000000000000000000000000000000000'; const { defaultAbiCoder, arrayify, keccak256, toUtf8Bytes } = ethers.utils; diff --git a/packages/axelar-local-dev/src/exportUtils.ts b/packages/axelar-local-dev/src/exportUtils.ts index beeba654..e0976312 100644 --- a/packages/axelar-local-dev/src/exportUtils.ts +++ b/packages/axelar-local-dev/src/exportUtils.ts @@ -94,11 +94,13 @@ export async function createAndExport(options: CreateLocalOptions = {}) { if (options.afterRelay) { const evmRelayData = _options.relayers.evm?.relayData; const nearRelayData = _options.relayers.near?.relayData; - const aptosRelayDAta = _options.relayers.aptos?.relayData; + const aptosRelayData = _options.relayers.aptos?.relayData; + const suiRelayData = _options.relayers.sui?.relayData; evmRelayData && (await options.afterRelay(evmRelayData)); nearRelayData && (await options.afterRelay(nearRelayData)); - aptosRelayDAta && (await options.afterRelay(aptosRelayDAta)); + aptosRelayData && (await options.afterRelay(aptosRelayData)); + suiRelayData && (await options.afterRelay(suiRelayData)); } relaying = false; }, _options.relayInterval); diff --git a/packages/axelar-local-dev/src/relay/Command.ts b/packages/axelar-local-dev/src/relay/Command.ts index 5aa613cf..bb2c6252 100644 --- a/packages/axelar-local-dev/src/relay/Command.ts +++ b/packages/axelar-local-dev/src/relay/Command.ts @@ -25,7 +25,8 @@ export class Command { this.commandId = commandId; this.name = name; this.data = data; - this.encodedData = chain === 'aptos' && name === 'approve_contract_call' ? '' : defaultAbiCoder.encode(dataSignature, data); + this.encodedData = + (chain === 'aptos' || chain === 'sui') && name === 'approve_contract_call' ? '' : defaultAbiCoder.encode(dataSignature, data); this.post = post; } diff --git a/packages/axelar-local-dev/src/relay/EvmRelayer.ts b/packages/axelar-local-dev/src/relay/EvmRelayer.ts index e819d1a6..55b14812 100644 --- a/packages/axelar-local-dev/src/relay/EvmRelayer.ts +++ b/packages/axelar-local-dev/src/relay/EvmRelayer.ts @@ -18,6 +18,7 @@ const AddressZero = ethers.constants.AddressZero; interface EvmRelayerOptions { nearRelayer?: Relayer; aptosRelayer?: Relayer; + suiRelayer?: Relayer; } export class EvmRelayer extends Relayer { @@ -27,6 +28,7 @@ export class EvmRelayer extends Relayer { super(); this.otherRelayers.near = options.nearRelayer; this.otherRelayers.aptos = options.aptosRelayer; + this.otherRelayers.sui = options.suiRelayer; } setRelayer(type: RelayerType, relayer: Relayer) { @@ -414,8 +416,10 @@ export class EvmRelayer extends Relayer { let command; if (args.destinationChain.toLowerCase() === 'aptos') { command = this.otherRelayers?.aptos?.createCallContractCommand(commandId, this.relayData, contractCallArgs); - } else if (args.destinationChain.toLowerCase() == 'near') { + } else if (args.destinationChain.toLowerCase() === 'near') { command = this.otherRelayers?.near?.createCallContractCommand(commandId, this.relayData, contractCallArgs); + } else if (args.destinationChain.toLowerCase() === 'sui') { + command = this.otherRelayers?.sui?.createCallContractCommand(commandId, this.relayData, contractCallArgs); } else { command = this.createCallContractCommand(commandId, this.relayData, contractCallArgs); } diff --git a/packages/axelar-local-dev/src/relay/Relayer.ts b/packages/axelar-local-dev/src/relay/Relayer.ts index 1e02f7e9..c89deea8 100644 --- a/packages/axelar-local-dev/src/relay/Relayer.ts +++ b/packages/axelar-local-dev/src/relay/Relayer.ts @@ -2,7 +2,12 @@ import { networks } from '../Network'; import { Command } from './Command'; import { CallContractArgs, CallContractWithTokenArgs, RelayCommand, RelayData } from './types'; -export type RelayerType = 'near' | 'aptos' | 'evm'; +export enum RelayerType { + Sui = 'sui', + Evm = 'evm', + Aptos = 'aptos', + Near = 'near', +} export type RelayerMap = Partial> & { [key: string]: Relayer | undefined }; export abstract class Relayer { @@ -35,6 +40,7 @@ export abstract class Relayer { this.commands[to.name] = []; } this.commands['aptos'] = []; + this.commands['sui'] = []; this.commands['near'] = []; // Update all events at the source chains await this.updateEvents(); diff --git a/packages/axelar-local-dev/src/relay/index.ts b/packages/axelar-local-dev/src/relay/index.ts index 5c329493..1ce65aa1 100644 --- a/packages/axelar-local-dev/src/relay/index.ts +++ b/packages/axelar-local-dev/src/relay/index.ts @@ -4,6 +4,7 @@ import { RelayerMap } from './Relayer'; export * from './Command'; export * from './types'; export * from './Relayer'; +export * from './EvmRelayer'; export const evmRelayer = new EvmRelayer();