diff --git a/clients/js/jest.config.json b/clients/js/jest.config.json index 19091e9cd87..29e918abbc9 100644 --- a/clients/js/jest.config.json +++ b/clients/js/jest.config.json @@ -3,8 +3,20 @@ "^@certusone/(.*)/lib/esm/(.*)$": "@certusone/$1/lib/cjs/$2" }, "transform": { - "^.+\\.(t|j)sx?$": "ts-jest" + "^.+\\.m?[tj]sx?$": [ + "ts-jest", + { + "isolatedModules": true, + "tsconfig": { + "module": "commonjs", + "moduleResolution": "node", + "esModuleInterop": true, + "allowJs": true + } + } + ] }, + "transformIgnorePatterns": ["node_modules/(?!(?:@mysten|@noble|@scure)/)"], "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", "testPathIgnorePatterns": ["__tests__/utils"], "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"], diff --git a/clients/js/package-lock.json b/clients/js/package-lock.json index 87f4f211d17..4d5c2925b67 100644 --- a/clients/js/package-lock.json +++ b/clients/js/package-lock.json @@ -16,7 +16,7 @@ "@injectivelabs/networks": "^1.10.7", "@injectivelabs/sdk-ts": "^1.10.47", "@injectivelabs/utils": "^1.10.5", - "@mysten/sui": "^1.45.0", + "@mysten/sui": "2.19.0", "@sei-js/core": "^1.3.2", "@solana/spl-token": "^0.3.5", "@solana/web3.js": "^1.22.0", @@ -61,9 +61,9 @@ } }, "node_modules/@0no-co/graphql.web": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.2.0.tgz", - "integrity": "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.3.2.tgz", + "integrity": "sha512-Q1+pRlLhE31GOY/2c9BAEnFTNxO7Awtc6fhhEDlxyCBQ2N0IhD32cPVvPChrK9mwBNSgRdW/sF1kd2e0ojHj1Q==", "license": "MIT", "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" @@ -75,17 +75,17 @@ } }, "node_modules/@0no-co/graphqlsp": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@0no-co/graphqlsp/-/graphqlsp-1.15.2.tgz", - "integrity": "sha512-Ys031WnS3sTQQBtRTkQsYnw372OlW72ais4sp0oh2UMPRNyxxnq85zRfU4PIdoy9kWriysPT5BYAkgIxhbonFA==", + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/@0no-co/graphqlsp/-/graphqlsp-1.17.3.tgz", + "integrity": "sha512-4PPvxDPmbntddpgMyA3VId5/E9YGdRuuS/mW+THOvtTx/C79Pf+lN28LkNNACJrF9L7YACiAJelyOkC6LqUzvw==", "license": "MIT", "dependencies": { - "@gql.tada/internal": "^1.0.0", + "@gql.tada/internal": "^1.2.0", "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" }, "peerDependencies": { "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", - "typescript": "^5.0.0" + "typescript": "^5.0.0 || ^6.0.0" } }, "node_modules/@adraffy/ens-normalize": { @@ -2262,21 +2262,21 @@ } }, "node_modules/@gql.tada/cli-utils": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@gql.tada/cli-utils/-/cli-utils-1.7.2.tgz", - "integrity": "sha512-Qbc7hbLvCz6IliIJpJuKJa9p05b2Jona7ov7+qofCsMRxHRZE1kpAmZMvL8JCI4c0IagpIlWNaMizXEQUe8XjQ==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@gql.tada/cli-utils/-/cli-utils-1.9.2.tgz", + "integrity": "sha512-cVNs4v8ewLRYJfyAsaHbiAmd5Hm+zXEMvMhBksH58ZU87d6f8crsp2CQG6QtIqnJJw1q0CBWfWTZepGWNcL3QA==", "license": "MIT", "dependencies": { - "@0no-co/graphqlsp": "^1.12.13", - "@gql.tada/internal": "1.0.8", + "@0no-co/graphqlsp": "^1.17.3", + "@gql.tada/internal": "1.2.1", "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" }, "peerDependencies": { - "@0no-co/graphqlsp": "^1.12.13", - "@gql.tada/svelte-support": "1.0.1", - "@gql.tada/vue-support": "1.0.1", + "@0no-co/graphqlsp": "^1.16.0", + "@gql.tada/svelte-support": "1.0.3", + "@gql.tada/vue-support": "1.0.3", "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", - "typescript": "^5.0.0" + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@gql.tada/svelte-support": { @@ -2288,16 +2288,16 @@ } }, "node_modules/@gql.tada/internal": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@gql.tada/internal/-/internal-1.0.8.tgz", - "integrity": "sha512-XYdxJhtHC5WtZfdDqtKjcQ4d7R1s0d1rnlSs3OcBEUbYiPoJJfZU7tWsVXuv047Z6msvmr4ompJ7eLSK5Km57g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@gql.tada/internal/-/internal-1.2.1.tgz", + "integrity": "sha512-1kPMv9KRpD6mfVwtXK+iy43U/gi4bpr4ganfhPLD0TjxpbuVJm7CtZ9wMFdwy3FjLBFN2QhwscNnL7lRPHg4vg==", "license": "MIT", "dependencies": { - "@0no-co/graphql.web": "^1.0.5" + "@0no-co/graphql.web": "^1.3.1" }, "peerDependencies": { "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", - "typescript": "^5.0.0" + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/@graphql-typed-document-node/core": { @@ -3517,29 +3517,29 @@ } }, "node_modules/@mysten/sui": { - "version": "1.45.2", - "resolved": "https://registry.npmjs.org/@mysten/sui/-/sui-1.45.2.tgz", - "integrity": "sha512-gftf7fNpFSiXyfXpbtP2afVEnhc7p2m/MEYc/SO5pov92dacGKOpQIF7etZsGDI1Wvhv+dpph+ulRNpnYSs7Bg==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/@mysten/sui/-/sui-2.19.0.tgz", + "integrity": "sha512-dXhYK3pUiaybEB6T9nru+cPrFa61NMSjEknOCoAEV5WCC589iY6bP7EM+H84DBMZttB1LhD3gpYnPntQ4Vvcgg==", "license": "Apache-2.0", "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", - "@mysten/bcs": "1.9.2", - "@mysten/utils": "0.2.0", - "@noble/curves": "=1.9.4", - "@noble/hashes": "^1.8.0", + "@mysten/bcs": "^2.1.0", + "@mysten/utils": "^0.4.0", + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0", "@protobuf-ts/grpcweb-transport": "^2.11.1", "@protobuf-ts/runtime": "^2.11.1", "@protobuf-ts/runtime-rpc": "^2.11.1", - "@scure/base": "^1.2.6", - "@scure/bip32": "^1.7.0", - "@scure/bip39": "^1.6.0", - "gql.tada": "^1.8.13", - "graphql": "^16.11.0", + "@scure/base": "^2.2.0", + "@scure/bip32": "^2.2.0", + "@scure/bip39": "^2.2.0", + "gql.tada": "^1.10.1", + "graphql": "^16.14.2", "poseidon-lite": "0.2.1", - "valibot": "^1.2.0" + "valibot": "^1.4.1" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/@mysten/sui.js": { @@ -3607,91 +3607,91 @@ } }, "node_modules/@mysten/sui/node_modules/@mysten/bcs": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@mysten/bcs/-/bcs-1.9.2.tgz", - "integrity": "sha512-kBk5xrxV9OWR7i+JhL/plQrgQ2/KJhB2pB5gj+w6GXhbMQwS3DPpOvi/zN0Tj84jwPvHMllpEl0QHj6ywN7/eQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@mysten/bcs/-/bcs-2.1.0.tgz", + "integrity": "sha512-rIR/cDAqDfBDxmYEZXppN/on8gmh1OW0dYi/Bk2kMBZcYMTaBaEtEX1VR6g7HicVxuMbFAIP0/SQkdH7J9Y5dQ==", "license": "Apache-2.0", "dependencies": { - "@mysten/utils": "0.2.0", - "@scure/base": "^1.2.6" + "@mysten/utils": "^0.4.0", + "@scure/base": "^2.2.0" } }, "node_modules/@mysten/sui/node_modules/@noble/curves": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.4.tgz", - "integrity": "sha512-2bKONnuM53lINoDrSmK8qP8W271ms7pygDhZt4SiLOoLwBtoHqeCFi6RG42V8zd3mLHuJFhU/Bmaqo4nX0/kBw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz", + "integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==", "license": "MIT", "dependencies": { - "@noble/hashes": "1.8.0" + "@noble/hashes": "2.2.0" }, "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@mysten/sui/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", "license": "MIT", "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@mysten/sui/node_modules/@scure/base": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", - "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz", + "integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==", "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@mysten/sui/node_modules/@scure/bip32": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", - "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.2.0.tgz", + "integrity": "sha512-zFr7t2F+a9+5tB7QbarF2HQNYrgjCNaoLAupZdKkrFMYMozJf5zqH2WJCQibMzm1qQ0QogrxVGO3qXfQDYMaQg==", "license": "MIT", "dependencies": { - "@noble/curves": "~1.9.0", - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" + "@noble/curves": "2.2.0", + "@noble/hashes": "2.2.0", + "@scure/base": "2.2.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@mysten/sui/node_modules/@scure/bip39": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", - "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.2.0.tgz", + "integrity": "sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A==", "license": "MIT", "dependencies": { - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" + "@noble/hashes": "2.2.0", + "@scure/base": "2.2.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@mysten/utils": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@mysten/utils/-/utils-0.2.0.tgz", - "integrity": "sha512-CM6kJcJHX365cK6aXfFRLBiuyXc5WSBHQ43t94jqlCAIRw8umgNcTb5EnEA9n31wPAQgLDGgbG/rCUISCTJ66w==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@mysten/utils/-/utils-0.4.0.tgz", + "integrity": "sha512-nHlXECBl9kE+AHF/aw5a7JVNizae8Kb71A4H18BV/sXd5/l2BaWNGWVatq05fzJo3hF78AWorDxa++1TgcBKWw==", "license": "Apache-2.0", "dependencies": { - "@scure/base": "^1.2.6" + "@scure/base": "^2.2.0" } }, "node_modules/@mysten/utils/node_modules/@scure/base": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", - "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz", + "integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==", "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" @@ -9648,29 +9648,6 @@ "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "optional": true, - "peer": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "optional": true, - "peer": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -11301,22 +11278,22 @@ } }, "node_modules/gql.tada": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/gql.tada/-/gql.tada-1.9.0.tgz", - "integrity": "sha512-1LMiA46dRs5oF7Qev6vMU32gmiNvM3+3nHoQZA9K9j2xQzH8xOAWnnJrLSbZOFHTSdFxqn86TL6beo1/7ja/aA==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/gql.tada/-/gql.tada-1.11.2.tgz", + "integrity": "sha512-oBr7ShA5/TmcwOO7BZgN1SynX2rBU+/ltysB0zXc+NCBF+9YOg6MRzJcTfLjIqDKdcE3LGxpOl0l9hBbxmyzmA==", "license": "MIT", "dependencies": { - "@0no-co/graphql.web": "^1.0.5", - "@0no-co/graphqlsp": "^1.12.13", - "@gql.tada/cli-utils": "1.7.2", - "@gql.tada/internal": "1.0.8" + "@0no-co/graphql.web": "^1.3.2", + "@0no-co/graphqlsp": "^1.17.3", + "@gql.tada/cli-utils": "1.9.2", + "@gql.tada/internal": "1.2.1" }, "bin": { "gql-tada": "bin/cli.js", "gql.tada": "bin/cli.js" }, "peerDependencies": { - "typescript": "^5.0.0" + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/graceful-fs": { @@ -11326,9 +11303,9 @@ "dev": true }, "node_modules/graphql": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", - "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", + "version": "16.14.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", + "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", "license": "MIT", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" @@ -17115,9 +17092,9 @@ } }, "node_modules/valibot": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", - "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.1.tgz", + "integrity": "sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g==", "license": "MIT", "peerDependencies": { "typescript": ">=5" @@ -18174,17 +18151,17 @@ }, "dependencies": { "@0no-co/graphql.web": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.2.0.tgz", - "integrity": "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.3.2.tgz", + "integrity": "sha512-Q1+pRlLhE31GOY/2c9BAEnFTNxO7Awtc6fhhEDlxyCBQ2N0IhD32cPVvPChrK9mwBNSgRdW/sF1kd2e0ojHj1Q==", "requires": {} }, "@0no-co/graphqlsp": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@0no-co/graphqlsp/-/graphqlsp-1.15.2.tgz", - "integrity": "sha512-Ys031WnS3sTQQBtRTkQsYnw372OlW72ais4sp0oh2UMPRNyxxnq85zRfU4PIdoy9kWriysPT5BYAkgIxhbonFA==", + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/@0no-co/graphqlsp/-/graphqlsp-1.17.3.tgz", + "integrity": "sha512-4PPvxDPmbntddpgMyA3VId5/E9YGdRuuS/mW+THOvtTx/C79Pf+lN28LkNNACJrF9L7YACiAJelyOkC6LqUzvw==", "requires": { - "@gql.tada/internal": "^1.0.0", + "@gql.tada/internal": "^1.2.0", "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" } }, @@ -19610,21 +19587,21 @@ } }, "@gql.tada/cli-utils": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@gql.tada/cli-utils/-/cli-utils-1.7.2.tgz", - "integrity": "sha512-Qbc7hbLvCz6IliIJpJuKJa9p05b2Jona7ov7+qofCsMRxHRZE1kpAmZMvL8JCI4c0IagpIlWNaMizXEQUe8XjQ==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@gql.tada/cli-utils/-/cli-utils-1.9.2.tgz", + "integrity": "sha512-cVNs4v8ewLRYJfyAsaHbiAmd5Hm+zXEMvMhBksH58ZU87d6f8crsp2CQG6QtIqnJJw1q0CBWfWTZepGWNcL3QA==", "requires": { - "@0no-co/graphqlsp": "^1.12.13", - "@gql.tada/internal": "1.0.8", + "@0no-co/graphqlsp": "^1.17.3", + "@gql.tada/internal": "1.2.1", "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" } }, "@gql.tada/internal": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@gql.tada/internal/-/internal-1.0.8.tgz", - "integrity": "sha512-XYdxJhtHC5WtZfdDqtKjcQ4d7R1s0d1rnlSs3OcBEUbYiPoJJfZU7tWsVXuv047Z6msvmr4ompJ7eLSK5Km57g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@gql.tada/internal/-/internal-1.2.1.tgz", + "integrity": "sha512-1kPMv9KRpD6mfVwtXK+iy43U/gi4bpr4ganfhPLD0TjxpbuVJm7CtZ9wMFdwy3FjLBFN2QhwscNnL7lRPHg4vg==", "requires": { - "@0no-co/graphql.web": "^1.0.5" + "@0no-co/graphql.web": "^1.3.1" } }, "@graphql-typed-document-node/core": { @@ -20695,71 +20672,71 @@ } }, "@mysten/sui": { - "version": "1.45.2", - "resolved": "https://registry.npmjs.org/@mysten/sui/-/sui-1.45.2.tgz", - "integrity": "sha512-gftf7fNpFSiXyfXpbtP2afVEnhc7p2m/MEYc/SO5pov92dacGKOpQIF7etZsGDI1Wvhv+dpph+ulRNpnYSs7Bg==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/@mysten/sui/-/sui-2.19.0.tgz", + "integrity": "sha512-dXhYK3pUiaybEB6T9nru+cPrFa61NMSjEknOCoAEV5WCC589iY6bP7EM+H84DBMZttB1LhD3gpYnPntQ4Vvcgg==", "requires": { "@graphql-typed-document-node/core": "^3.2.0", - "@mysten/bcs": "1.9.2", - "@mysten/utils": "0.2.0", - "@noble/curves": "=1.9.4", - "@noble/hashes": "^1.8.0", + "@mysten/bcs": "^2.1.0", + "@mysten/utils": "^0.4.0", + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0", "@protobuf-ts/grpcweb-transport": "^2.11.1", "@protobuf-ts/runtime": "^2.11.1", "@protobuf-ts/runtime-rpc": "^2.11.1", - "@scure/base": "^1.2.6", - "@scure/bip32": "^1.7.0", - "@scure/bip39": "^1.6.0", - "gql.tada": "^1.8.13", - "graphql": "^16.11.0", + "@scure/base": "^2.2.0", + "@scure/bip32": "^2.2.0", + "@scure/bip39": "^2.2.0", + "gql.tada": "^1.10.1", + "graphql": "^16.14.2", "poseidon-lite": "0.2.1", - "valibot": "^1.2.0" + "valibot": "^1.4.1" }, "dependencies": { "@mysten/bcs": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@mysten/bcs/-/bcs-1.9.2.tgz", - "integrity": "sha512-kBk5xrxV9OWR7i+JhL/plQrgQ2/KJhB2pB5gj+w6GXhbMQwS3DPpOvi/zN0Tj84jwPvHMllpEl0QHj6ywN7/eQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@mysten/bcs/-/bcs-2.1.0.tgz", + "integrity": "sha512-rIR/cDAqDfBDxmYEZXppN/on8gmh1OW0dYi/Bk2kMBZcYMTaBaEtEX1VR6g7HicVxuMbFAIP0/SQkdH7J9Y5dQ==", "requires": { - "@mysten/utils": "0.2.0", - "@scure/base": "^1.2.6" + "@mysten/utils": "^0.4.0", + "@scure/base": "^2.2.0" } }, "@noble/curves": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.4.tgz", - "integrity": "sha512-2bKONnuM53lINoDrSmK8qP8W271ms7pygDhZt4SiLOoLwBtoHqeCFi6RG42V8zd3mLHuJFhU/Bmaqo4nX0/kBw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz", + "integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==", "requires": { - "@noble/hashes": "1.8.0" + "@noble/hashes": "2.2.0" } }, "@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==" }, "@scure/base": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", - "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz", + "integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==" }, "@scure/bip32": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", - "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.2.0.tgz", + "integrity": "sha512-zFr7t2F+a9+5tB7QbarF2HQNYrgjCNaoLAupZdKkrFMYMozJf5zqH2WJCQibMzm1qQ0QogrxVGO3qXfQDYMaQg==", "requires": { - "@noble/curves": "~1.9.0", - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" + "@noble/curves": "2.2.0", + "@noble/hashes": "2.2.0", + "@scure/base": "2.2.0" } }, "@scure/bip39": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", - "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.2.0.tgz", + "integrity": "sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A==", "requires": { - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" + "@noble/hashes": "2.2.0", + "@scure/base": "2.2.0" } } } @@ -20813,17 +20790,17 @@ } }, "@mysten/utils": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@mysten/utils/-/utils-0.2.0.tgz", - "integrity": "sha512-CM6kJcJHX365cK6aXfFRLBiuyXc5WSBHQ43t94jqlCAIRw8umgNcTb5EnEA9n31wPAQgLDGgbG/rCUISCTJ66w==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@mysten/utils/-/utils-0.4.0.tgz", + "integrity": "sha512-nHlXECBl9kE+AHF/aw5a7JVNizae8Kb71A4H18BV/sXd5/l2BaWNGWVatq05fzJo3hF78AWorDxa++1TgcBKWw==", "requires": { - "@scure/base": "^1.2.6" + "@scure/base": "^2.2.0" }, "dependencies": { "@scure/base": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", - "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz", + "integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==" } } }, @@ -25666,28 +25643,6 @@ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true }, - "encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "optional": true, - "peer": true, - "requires": { - "iconv-lite": "^0.6.2" - }, - "dependencies": { - "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "optional": true, - "peer": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - } - } - }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -27005,14 +26960,14 @@ } }, "gql.tada": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/gql.tada/-/gql.tada-1.9.0.tgz", - "integrity": "sha512-1LMiA46dRs5oF7Qev6vMU32gmiNvM3+3nHoQZA9K9j2xQzH8xOAWnnJrLSbZOFHTSdFxqn86TL6beo1/7ja/aA==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/gql.tada/-/gql.tada-1.11.2.tgz", + "integrity": "sha512-oBr7ShA5/TmcwOO7BZgN1SynX2rBU+/ltysB0zXc+NCBF+9YOg6MRzJcTfLjIqDKdcE3LGxpOl0l9hBbxmyzmA==", "requires": { - "@0no-co/graphql.web": "^1.0.5", - "@0no-co/graphqlsp": "^1.12.13", - "@gql.tada/cli-utils": "1.7.2", - "@gql.tada/internal": "1.0.8" + "@0no-co/graphql.web": "^1.3.2", + "@0no-co/graphqlsp": "^1.17.3", + "@gql.tada/cli-utils": "1.9.2", + "@gql.tada/internal": "1.2.1" } }, "graceful-fs": { @@ -27022,9 +26977,9 @@ "dev": true }, "graphql": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", - "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==" + "version": "16.14.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", + "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==" }, "graphql-tag": { "version": "2.12.6", @@ -31226,9 +31181,9 @@ } }, "valibot": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", - "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.1.tgz", + "integrity": "sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g==", "requires": {} }, "varint": { diff --git a/clients/js/package.json b/clients/js/package.json index 1439efc68e0..03798c4cbcd 100644 --- a/clients/js/package.json +++ b/clients/js/package.json @@ -15,7 +15,7 @@ "repository": "https://github.com/certusone/wormhole/tree/main/clients/js", "scripts": { "start": "npm run build && node ./build/main.js", - "build": "esbuild src/main.ts --bundle --outfile=build/main.js --minify --platform=node --target=node22", + "build": "esbuild src/main.ts --bundle --outfile=build/main.js --minify --platform=node --target=node22 --tsconfig=tsconfig.build.json", "check": "tsc --noEmit", "docs": "npx tsx src/doc.ts", "prepublishOnly": "npm run check", @@ -36,7 +36,7 @@ "@injectivelabs/networks": "^1.10.7", "@injectivelabs/sdk-ts": "^1.10.47", "@injectivelabs/utils": "^1.10.5", - "@mysten/sui": "^1.45.0", + "@mysten/sui": "2.19.0", "@sei-js/core": "^1.3.2", "@solana/spl-token": "^0.3.5", "@solana/web3.js": "^1.22.0", diff --git a/clients/js/src/chains/generic/getOriginalAsset.ts b/clients/js/src/chains/generic/getOriginalAsset.ts index b72088c329b..dc785a7ccb3 100644 --- a/clients/js/src/chains/generic/getOriginalAsset.ts +++ b/clients/js/src/chains/generic/getOriginalAsset.ts @@ -5,10 +5,10 @@ import { getOriginalAssetEth, getOriginalAssetNear, getOriginalAssetSolana, - getOriginalAssetSui, getOriginalAssetTerra, getOriginalAssetXpla, } from "@certusone/wormhole-sdk/lib/esm/token_bridge/getOriginalAsset"; +import { getOriginalAssetSui } from "../../sdk/sui"; import { getOriginalAssetInjective } from "@certusone/wormhole-sdk/lib/esm/token_bridge/injective"; import { impossible } from "../../vaa"; import { getOriginalAssetSei } from "../sei/sdk"; @@ -115,11 +115,11 @@ export const getOriginalAsset = async ( } case "Sui": { const provider = getProviderForChain(chainName, network, { rpc }); - return getOriginalAssetSui( - provider as any, + return (await getOriginalAssetSui( + provider, tokenBridgeAddress, assetAddress - ); + )) as WormholeWrappedInfo; } case "Btc": case "Osmosis": diff --git a/clients/js/src/chains/generic/getWrappedAssetAddress.ts b/clients/js/src/chains/generic/getWrappedAssetAddress.ts index edcf1abcf5e..1d814f0384c 100644 --- a/clients/js/src/chains/generic/getWrappedAssetAddress.ts +++ b/clients/js/src/chains/generic/getWrappedAssetAddress.ts @@ -4,10 +4,10 @@ import { getForeignAssetEth, getForeignAssetNear, getForeignAssetSolana, - getForeignAssetSui, getForeignAssetTerra, getForeignAssetXpla, } from "@certusone/wormhole-sdk/lib/esm/token_bridge/getForeignAsset"; +import { getForeignAssetSui } from "../../sdk/sui"; import { getForeignAssetInjective } from "@certusone/wormhole-sdk/lib/esm/token_bridge/injective"; import { impossible } from "../../vaa"; import { getForeignAssetSei } from "../sei/sdk"; @@ -158,9 +158,9 @@ export const getWrappedAssetAddress = async ( case "Sui": { const provider = getProviderForChain(chainName, network, { rpc }); return getForeignAssetSui( - provider as any, + provider, tokenBridgeAddress, - toChainId(originChain), + toChain(originChain), originAddressUint8Array ); } diff --git a/clients/js/src/chains/generic/provider.ts b/clients/js/src/chains/generic/provider.ts index e1c22734fe0..18b3db4851e 100644 --- a/clients/js/src/chains/generic/provider.ts +++ b/clients/js/src/chains/generic/provider.ts @@ -4,7 +4,8 @@ import { getNetworkEndpoints, } from "@injectivelabs/networks"; import { ChainGrpcWasmApi } from "@injectivelabs/sdk-ts"; -import { SuiClient } from "@mysten/sui/client"; +import { SuiGrpcClient } from "@mysten/sui/grpc"; +import { getSuiNetwork } from "../sui/utils"; import { getCosmWasmClient } from "@sei-js/core"; import { Connection as SolanaConnection } from "@solana/web3.js"; import { LCDClient as TerraLCDClient } from "@terra-money/terra.js"; @@ -39,7 +40,7 @@ export type ChainProvider = T extends "Algorand" : T extends "Solana" ? SolanaConnection : T extends "Sui" - ? SuiClient + ? SuiGrpcClient : T extends "Xpla" ? XplaLCDClient : never; @@ -142,7 +143,10 @@ export const getProviderForChain = ( case "Aptos": return new AptosClient(rpc) as ChainProvider; case "Sui": - return new SuiClient({ url: rpc }) as ChainProvider; + return new SuiGrpcClient({ + network: getSuiNetwork(network), + baseUrl: rpc, + }) as ChainProvider; case "Btc": case "Osmosis": case "Pythnet": diff --git a/clients/js/src/chains/sui/__tests__/publish.test.ts b/clients/js/src/chains/sui/__tests__/publish.test.ts new file mode 100644 index 00000000000..50933434baf --- /dev/null +++ b/clients/js/src/chains/sui/__tests__/publish.test.ts @@ -0,0 +1,31 @@ +import { parseTestPublishDigest } from "../publish"; + +describe("parseTestPublishDigest", () => { + it("extracts the digest from output with leading build lines", () => { + const output = [ + "INCLUDING DEPENDENCY Sui", + "BUILDING token_bridge", + "Skipping dependency verification", + JSON.stringify({ + digest: "BwBvV26C79zyjUxwTCLVyZmjKsBqUGU7a9F5jySoPnVh", + effects: { status: { status: "success" } }, + }), + ].join("\n"); + + expect(parseTestPublishDigest(output)).toBe( + "BwBvV26C79zyjUxwTCLVyZmjKsBqUGU7a9F5jySoPnVh" + ); + }); + + it("throws when there is no JSON object in the output", () => { + expect(() => parseTestPublishDigest("error: command failed")).toThrow( + /No JSON output/ + ); + }); + + it("throws when the JSON has no digest field", () => { + expect(() => + parseTestPublishDigest(JSON.stringify({ effects: {} })) + ).toThrow(/No transaction digest/); + }); +}); diff --git a/clients/js/src/chains/sui/__tests__/sui-utils.test.ts b/clients/js/src/chains/sui/__tests__/sui-utils.test.ts new file mode 100644 index 00000000000..30d5e982356 --- /dev/null +++ b/clients/js/src/chains/sui/__tests__/sui-utils.test.ts @@ -0,0 +1,311 @@ +import { toHex } from "@mysten/sui/utils"; +import { SuiGrpcClient } from "@mysten/sui/grpc"; +import { + getPublishedPackageId, + normalizeSuiAddress, + normalizeSuiType, + toSuiTransactionResult, +} from "../utils"; +import { + CoinTypeKeyBcs, + getOriginalAssetSui, + isValidSuiAddress, + isValidSuiType, + trimSuiType, +} from "../../../sdk/sui"; + +// The gRPC envelope shape consumed by toSuiTransactionResult. Cast at the call +// site since hand-building the SDK's include-parameterized type is unnecessary +// for exercising the mapping. +const grpcTx = (tx: unknown) => + toSuiTransactionResult(tx as Parameters[0]); + +describe("toSuiTransactionResult (gRPC effects -> SuiTransactionResult)", () => { + it("maps changedObjects, owners, created/package flags, and events", () => { + const res = grpcTx({ + digest: "DIGEST1", + status: { success: true, error: null }, + transaction: { sender: "0xsender" }, + objectTypes: { "0xstate": "0xpkg::state::State" }, + effects: { + changedObjects: [ + { + objectId: "0xpkg", + outputOwner: { $kind: "Immutable" }, + idOperation: "Created", + outputState: "PackageWrite", + }, + { + objectId: "0xstate", + outputOwner: { $kind: "Shared" }, + idOperation: "Created", + outputState: "ObjectWrite", + }, + { + objectId: "0xgas", + outputOwner: { $kind: "AddressOwner", AddressOwner: "0xowner" }, + idOperation: "None", + outputState: "ObjectWrite", + }, + ], + }, + events: [ + { + packageId: "0xpkg", + module: "publish_message", + sender: "0xsender", + eventType: "0xpkg::m::E", + json: { a: 1 }, + }, + ], + }); + + expect(res.digest).toBe("DIGEST1"); + expect(res.success).toBe(true); + expect(res.error).toBeUndefined(); + expect(res.sender).toBe("0xsender"); + expect(res.changedObjects).toEqual([ + { + objectId: "0xpkg", + type: undefined, + owner: "Immutable", + created: true, + isPackage: true, + }, + { + objectId: "0xstate", + type: "0xpkg::state::State", + owner: "Shared", + created: true, + isPackage: false, + }, + { + objectId: "0xgas", + type: undefined, + owner: "0xowner", + created: false, + isPackage: false, + }, + ]); + expect(res.events).toEqual([ + { + packageId: "0xpkg", + module: "publish_message", + sender: "0xsender", + eventType: "0xpkg::m::E", + json: { a: 1 }, + }, + ]); + // The single PackageWrite is resolvable as the published package. + expect(getPublishedPackageId(res)).toBe("0xpkg"); + }); + + it("surfaces the failure status message as the error", () => { + const res = grpcTx({ + digest: "DIGEST2", + status: { success: false, error: { message: "MoveAbort" } }, + effects: { changedObjects: [] }, + events: [], + }); + expect(res.success).toBe(false); + expect(res.error).toBe("MoveAbort"); + }); +}); + +describe("Sui type/address helpers", () => { + it("normalizes addresses to 32-byte 0x form", () => { + expect(normalizeSuiAddress("0x2")).toBe(`0x${"0".repeat(63)}2`); + }); + + it("normalizes fully-qualified types", () => { + expect(normalizeSuiType("0x2::sui::SUI")).toBe( + `0x${"0".repeat(63)}2::sui::SUI` + ); + }); + + it("validates Sui addresses and types", () => { + expect(isValidSuiAddress("0x2")).toBe(true); + expect(isValidSuiAddress("not-an-address")).toBe(false); + expect(isValidSuiType("0x2::sui::SUI")).toBe(true); + expect(isValidSuiType("0x2::sui")).toBe(false); + }); + + it("trims leading zeroes in types", () => { + expect(trimSuiType(`0x${"0".repeat(63)}2::sui::SUI`)).toBe("0x2::sui::SUI"); + }); +}); + +describe("CoinTypeKey BCS encoding", () => { + // Ground truth captured from the Sui mainnet token bridge `coin_types` table: + // chain 21 (Sui), 32-byte address, encoded as `15 00` (u16 LE) + `20` (vector + // length) + 32 address bytes. + it("matches the on-chain dynamic-field key bytes", () => { + const addrHex = + "2a62e389553ae6f061970ce1be2607c7f918154532e4296512d5a2c773424ff5"; + const addr = Array.from(Buffer.from(addrHex, "hex")); + const encoded = toHex( + CoinTypeKeyBcs.serialize({ chain: 21, addr }).toBytes() + ); + expect(encoded).toBe(`150020${addrHex}`); + }); +}); + +describe("getOriginalAssetSui", () => { + const mockClient = { + getObject: jest.fn(), + getDynamicField: jest.fn(), + } as unknown as SuiGrpcClient; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("parses a wrapped asset from token registry", async () => { + const tokenBridgeStateId = + "0x26efee2b51c911237888e5dc6702868abca3c7ac12c53f76ef8dc369bad5de42"; + const registryId = + "0x8f52cdc82ea6b81c8bfbffa76a82ee88076b9a2e89c6ba08b5bb7e9a2badc00c"; + const coinType = + "0x1234567890123456789012345678901234567890123456789012345678901234::coin::COIN"; + const tokenAddress = Buffer.from( + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "hex" + ); + + (mockClient.getObject as jest.Mock) + .mockResolvedValueOnce({ + object: { + type: "0x26efee2b51c911237888e5dc6702868abca3c7ac12c53f76ef8dc369bad5de42::token_bridge::State", + json: { + token_registry: { id: registryId }, + }, + }, + }) + .mockResolvedValueOnce({ + object: { + type: `0x26efee2b51c911237888e5dc6702868abca3c7ac12c53f76ef8dc369bad5de42::wrapped_asset::WrappedAsset<${coinType}>`, + json: { + value: { + info: { + token_chain: "7", + token_address: { + value: { + data: Buffer.from(tokenAddress).toString("base64"), + }, + }, + }, + }, + }, + }, + }); + + (mockClient.getDynamicField as jest.Mock).mockResolvedValueOnce({ + dynamicField: { + fieldId: "0x" + "a".repeat(64), + }, + }); + + const result = await getOriginalAssetSui( + mockClient, + tokenBridgeStateId, + coinType + ); + + expect(result).toEqual({ + isWrapped: true, + chainId: 7, + assetAddress: new Uint8Array(tokenAddress), + }); + + expect(mockClient.getObject).toHaveBeenCalledTimes(2); + expect(mockClient.getDynamicField).toHaveBeenCalledTimes(1); + }); + + it("parses a native asset from token registry", async () => { + const tokenBridgeStateId = + "0x26efee2b51c911237888e5dc6702868abca3c7ac12c53f76ef8dc369bad5de42"; + const registryId = + "0x8f52cdc82ea6b81c8bfbffa76a82ee88076b9a2e89c6ba08b5bb7e9a2badc00c"; + const coinType = "0x2::sui::SUI"; + const tokenAddress = Buffer.alloc(32, 0); + + (mockClient.getObject as jest.Mock) + .mockResolvedValueOnce({ + object: { + type: "0x26efee2b51c911237888e5dc6702868abca3c7ac12c53f76ef8dc369bad5de42::token_bridge::State", + json: { + token_registry: { id: registryId }, + }, + }, + }) + .mockResolvedValueOnce({ + object: { + type: `0x26efee2b51c911237888e5dc6702868abca3c7ac12c53f76ef8dc369bad5de42::native_asset::NativeAsset<${coinType}>`, + json: { + value: { + token_address: { + value: { + data: Buffer.from(tokenAddress).toString("base64"), + }, + }, + }, + }, + }, + }); + + (mockClient.getDynamicField as jest.Mock).mockResolvedValueOnce({ + dynamicField: { + fieldId: "0x" + "b".repeat(64), + }, + }); + + const result = await getOriginalAssetSui( + mockClient, + tokenBridgeStateId, + coinType + ); + + expect(result).toEqual({ + isWrapped: false, + chainId: 21, + assetAddress: new Uint8Array(tokenAddress), + }); + }); + + it("throws on invalid coin type", async () => { + const tokenBridgeStateId = + "0x26efee2b51c911237888e5dc6702868abca3c7ac12c53f76ef8dc369bad5de42"; + + await expect( + getOriginalAssetSui(mockClient, tokenBridgeStateId, "invalid::type") + ).rejects.toThrow("Invalid Sui type"); + + expect(mockClient.getObject).not.toHaveBeenCalled(); + }); + + it("throws when token is not registered", async () => { + const tokenBridgeStateId = + "0x26efee2b51c911237888e5dc6702868abca3c7ac12c53f76ef8dc369bad5de42"; + const registryId = + "0x8f52cdc82ea6b81c8bfbffa76a82ee88076b9a2e89c6ba08b5bb7e9a2badc00c"; + const coinType = + "0x1234567890123456789012345678901234567890123456789012345678901234::coin::COIN"; + + (mockClient.getObject as jest.Mock).mockResolvedValueOnce({ + object: { + type: "0x26efee2b51c911237888e5dc6702868abca3c7ac12c53f76ef8dc369bad5de42::token_bridge::State", + json: { + token_registry: { id: registryId }, + }, + }, + }); + + (mockClient.getDynamicField as jest.Mock).mockRejectedValueOnce( + new Error("not found") + ); + + await expect( + getOriginalAssetSui(mockClient, tokenBridgeStateId, coinType) + ).rejects.toThrow("has not been registered with the token bridge"); + }); +}); diff --git a/clients/js/src/chains/sui/index.ts b/clients/js/src/chains/sui/index.ts index 38f130e012d..db4589ac8a4 100644 --- a/clients/js/src/chains/sui/index.ts +++ b/clients/js/src/chains/sui/index.ts @@ -5,3 +5,4 @@ export * from "./publish"; export * from "./submit"; export * from "./types"; export * from "./utils"; +export * from "./wrappedCoinBytecode"; diff --git a/clients/js/src/chains/sui/log.ts b/clients/js/src/chains/sui/log.ts index 887b96b51ae..d46cd3b16cd 100644 --- a/clients/js/src/chains/sui/log.ts +++ b/clients/js/src/chains/sui/log.ts @@ -1,22 +1,25 @@ -import { SuiTransactionBlockResponse } from "@mysten/sui/client"; -import { getCreatedObjects, getPublishedPackageId } from "./utils"; +import { + getCreatedObjects, + getPublishedPackageId, + SuiTransactionResult, +} from "./utils"; export const logTransactionDigest = ( - res: SuiTransactionBlockResponse, + res: SuiTransactionResult, ...args: string[] ) => { console.log("Transaction digest", res.digest, ...args); }; -export const logTransactionSender = (res: SuiTransactionBlockResponse) => { - console.log("Transaction sender", res.transaction?.data?.sender); +export const logTransactionSender = (res: SuiTransactionResult) => { + console.log("Transaction sender", res.sender); }; -export const logPublishedPackageId = (res: SuiTransactionBlockResponse) => { +export const logPublishedPackageId = (res: SuiTransactionResult) => { console.log("Published to", getPublishedPackageId(res)); }; -export const logCreatedObjects = (res: SuiTransactionBlockResponse) => { +export const logCreatedObjects = (res: SuiTransactionResult) => { console.log( "Created objects", JSON.stringify(getCreatedObjects(res), null, 2) diff --git a/clients/js/src/chains/sui/publish.ts b/clients/js/src/chains/sui/publish.ts index 23a4fa1bf6f..d383c35d594 100644 --- a/clients/js/src/chains/sui/publish.ts +++ b/clients/js/src/chains/sui/publish.ts @@ -1,14 +1,15 @@ -import { SuiTransactionBlockResponse } from "@mysten/sui/client"; import { Transaction } from "@mysten/sui/transactions"; -import { fromB64 } from "@mysten/bcs"; +import { fromBase64 } from "@mysten/sui/utils"; import { execSync } from "child_process"; import fs from "fs"; import { SuiBuildOutput } from "./types"; import { executeTransactionBlock, - isSuiPublishEvent, + fetchTransactionResult, + getPublishedPackageId, normalizeSuiAddress, SuiSigner, + SuiTransactionResult, } from "./utils"; import { Network } from "@wormhole-foundation/sdk"; @@ -59,68 +60,62 @@ export const publishPackage = async ( signer: SuiSigner, network: Network, packagePath: string -) => { +): Promise => { if (network === "Devnet") { - // test-publish uses the locally configured CLI signer, not the passed signer - return publishPackageTestPublish(packagePath); - } else { - return publishPackageSDK(signer, network, packagePath); + return publishPackageTestPublish(signer, packagePath); } + return publishPackageSDK(signer, network, packagePath); }; /** - * Use `sui client test-publish` for ephemeral/local deployments. - * This handles dependencies automatically and doesn't require Published.toml manipulation. - * Note: Uses the locally configured Sui CLI signer, not a programmatic signer. + * Extract the transaction digest from `sui client test-publish --json` output. + * The CLI prepends human-readable build lines before the JSON object, so the + * payload is sliced from the first brace. Only the digest is read here; the + * transaction's effects are fetched separately over gRPC. */ -const publishPackageTestPublish = async (packagePath: string) => { - // Use test-publish with --publish-unpublished-deps to handle dependencies - // --build-env testnet tells it to use testnet dependency resolution - const cmd = `sui client test-publish ${packagePath} --publish-unpublished-deps --build-env testnet --json 2>&1`; +export const parseTestPublishDigest = (output: string): string => { + const jsonStart = output.indexOf("{"); + if (jsonStart === -1) { + throw new Error(`No JSON output from test-publish: ${output}`); + } + const digest = JSON.parse(output.slice(jsonStart))?.digest; + if (typeof digest !== "string") { + throw new Error(`No transaction digest in test-publish output: ${output}`); + } + return digest; +}; - console.log(`Running: ${cmd}`); +/** + * Publish via `sui client test-publish` for Devnet/localnet. + * + * The Sui package system resolves local, not-yet-published dependencies (e.g. + * the core bridge imported by the token bridge) per chain id. `test-publish` + * performs that resolution together with `--publish-unpublished-deps` without + * requiring Published.toml management — something a plain SDK `tx.publish` + * cannot reproduce on an ephemeral local network. It signs with the locally + * configured CLI keystore and commits on-chain, so the resulting transaction is + * read back over gRPC rather than parsed from the CLI's deprecated JSON-RPC + * `objectChanges` payload. + */ +const publishPackageTestPublish = async ( + signer: SuiSigner, + packagePath: string +): Promise => { + // `--build-env testnet` selects testnet dependency pins (localnet is not a + // pinned environment); `--publish-unpublished-deps` publishes dependencies + // that are not yet on-chain for this network. + const cmd = `sui client test-publish ${packagePath} --publish-unpublished-deps --build-env testnet --json 2>&1`; + let output: string; try { - const output = execSync(cmd, { encoding: "utf-8" }); - console.log(`test-publish output:\n${output}`); - - // Parse JSON output - const jsonStart = output.indexOf("{"); - if (jsonStart === -1) { - throw new Error(`No JSON output from test-publish: ${output}`); - } - - const result = JSON.parse(output.slice(jsonStart)); - - // Extract published package ID from the result - const publishedChanges = result.objectChanges?.filter( - (change: any) => change.type === "published" - ); - - if (!publishedChanges || publishedChanges.length === 0) { - throw new Error( - `No published package found in test-publish output: ${JSON.stringify( - result.objectChanges, - null, - 2 - )}` - ); - } - - // Find the main package (not dependencies) - it's typically the last one published - const mainPackage = publishedChanges[publishedChanges.length - 1]; - console.log(`Published package ID: ${mainPackage.packageId}`); - - return result; + output = execSync(cmd, { encoding: "utf-8" }); } catch (e: any) { - // Print full error details - console.error(`test-publish error:`); - if (e.stdout) console.error(`stdout: ${e.stdout}`); - if (e.stderr) console.error(`stderr: ${e.stderr}`); - if (e.status) console.error(`exit code: ${e.status}`); - console.error(`message: ${e.message}`); - throw new Error(`test-publish failed: ${e.message}`); + const detail = [e.stdout, e.stderr, e.message].filter(Boolean).join("\n"); + throw new Error(`test-publish failed:\n${detail}`); } + + const digest = parseTestPublishDigest(output); + return fetchTransactionResult(signer.client, digest); }; /** @@ -139,7 +134,7 @@ const publishPackageSDK = async ( const tx = new Transaction(); const [upgradeCap] = tx.publish({ - modules: build.modules.map((m) => Array.from(fromB64(m))), + modules: build.modules.map((m) => Array.from(fromBase64(m))), dependencies: build.dependencies.map((d) => normalizeSuiAddress(d)), }); @@ -150,17 +145,10 @@ const publishPackageSDK = async ( const res = await executeTransactionBlock(signer, tx); - console.log(`Transaction status: ${JSON.stringify(res.effects?.status)}`); - - const publishEvents = res.objectChanges?.filter(isSuiPublishEvent) ?? []; - if (publishEvents.length !== 1) { - throw new Error( - "No publish event found in transaction:" + - JSON.stringify(res.objectChanges, null, 2) - ); - } + console.log(`Transaction status: ${res.success ? "success" : res.error}`); - console.log(`Published package ID: ${publishEvents[0].packageId}`); + // getPublishedPackageId throws if there isn't exactly one published package. + console.log(`Published package ID: ${getPublishedPackageId(res)}`); return res; }; diff --git a/clients/js/src/chains/sui/registrations.ts b/clients/js/src/chains/sui/registrations.ts index 936e99a6921..8d8e0c62c65 100644 --- a/clients/js/src/chains/sui/registrations.ts +++ b/clients/js/src/chains/sui/registrations.ts @@ -1,5 +1,6 @@ -import { getObjectFields } from "@certusone/wormhole-sdk/lib/esm/sui"; +import type { SuiClientTypes } from "@mysten/sui/client"; import { NETWORKS } from "../../consts/networks"; +import { getObjectFields } from "../../sdk/sui"; import { getProvider } from "./utils"; import { ChainId, @@ -27,29 +28,38 @@ export async function queryRegistrationsSui( throw new Error(`Invalid module: ${module}`); } - const state = await getObjectFields(provider as any, state_object_id); - const emitterRegistryId = state!.emitter_registry.fields.id.id; - - // TODO: handle pagination - // - recursive: https://github.com/wormhole-foundation/wormhole/blob/7608b2b740df5d4c2551daaf4d620eac81c07790/sdk/js/src/sui/utils.ts#L175 - // - iterative: https://github.com/wormhole-foundation/wormhole/blob/7608b2b740df5d4c2551daaf4d620eac81c07790/sdk/js/src/sui/utils.ts#L199 - const emitterRegistry = await provider.getDynamicFields({ - parentId: emitterRegistryId, - }); + const state = await getObjectFields(provider, state_object_id); + const emitterRegistryId = state!.emitter_registry.id; const results: { [key: string]: string } = {}; - for (let idx = 0; idx < emitterRegistry.data.length; idx++) { - const chainId = emitterRegistry.data[idx].name.value as ChainId; - for (const { objectId } of emitterRegistry.data.slice(idx, idx + 1)) { - const emitter = (await provider.getObject({ - id: objectId, - options: { showContent: true }, - })) as any; - const emitterAddress: Uint8Array = - emitter.data?.content?.fields.value.fields.value.fields.data; - const emitterAddrStr = Buffer.from(emitterAddress).toString("hex"); - results[chainIdToChain(chainId)] = emitterAddrStr; + let cursor: string | null = null; + let hasNextPage = true; + while (hasNextPage) { + const page: SuiClientTypes.ListDynamicFieldsResponse = + await provider.listDynamicFields({ + parentId: emitterRegistryId, + cursor, + }); + for (const field of page.dynamicFields) { + const entry = await provider.getObject({ + objectId: field.fieldId, + include: { json: true }, + }); + const json = entry.object.json as any; + const chainId = json?.name as ChainId; + // The gRPC JSON representation encodes the `vector` emitter address as + // base64 under `value.value.data`. + const dataB64: string | undefined = json?.value?.value?.data; + if (chainId === undefined || dataB64 === undefined) { + continue; + } + results[chainIdToChain(chainId)] = Buffer.from( + dataB64, + "base64" + ).toString("hex"); } + hasNextPage = page.hasNextPage; + cursor = page.cursor; } return results; diff --git a/clients/js/src/chains/sui/submit.ts b/clients/js/src/chains/sui/submit.ts index 7e0d5439787..67c79e5f377 100644 --- a/clients/js/src/chains/sui/submit.ts +++ b/clients/js/src/chains/sui/submit.ts @@ -1,25 +1,25 @@ -import { - getWrappedCoinType, - uint8ArrayToBCS, -} from "@certusone/wormhole-sdk/lib/esm/sui"; -import { - createWrappedOnSui, - createWrappedOnSuiPrepare, -} from "@certusone/wormhole-sdk/lib/esm/token_bridge/createWrapped"; import { Transaction } from "@mysten/sui/transactions"; +import { + fromBase64, + normalizeStructTag, + parseStructTag, +} from "@mysten/sui/utils"; import { Payload, impossible } from "../../vaa"; import { assertSuccess, executeTransactionBlock, + getOriginalPackageId, getPackageId, getProvider, + getPublishedPackageId, getSigner, - isSuiCreateEvent, - isSuiPublishEvent, + getUpgradeCapObjectId, + normalizeSuiAddress, registerChain, setMaxGasBudgetDevnet, SUI_CLOCK_OBJECT_ID, } from "./utils"; +import { SuiGrpcClient } from "@mysten/sui/grpc"; import { Chain, Network, @@ -29,6 +29,7 @@ import { deserialize, } from "@wormhole-foundation/sdk"; import { getForeignAssetSui } from "../../sdk/sui"; +import { buildWrappedCoinBytecode } from "./wrappedCoinBytecode"; export const submit = async ( payload: Payload, @@ -58,7 +59,7 @@ export const submit = async ( target: `${corePackageId}::vaa::parse_and_verify`, arguments: [ tx.object(coreObjectId), - tx.pure("vector", [...uint8ArrayToBCS(vaa)]), + tx.pure("vector", [...vaa]), tx.object(SUI_CLOCK_OBJECT_ID), ], }); @@ -139,28 +140,23 @@ export const submit = async ( throw new Error("Updating wrapped asset not supported on Sui"); } else { // Coin doesn't exist, so create wrapped asset + const signerAddress = signer.keypair.getPublicKey().toSuiAddress(); + console.log("[1/2] Creating wrapped asset..."); const prepareTx = await createWrappedOnSuiPrepare( - client as any, + client, coreBridgeStateObjectId, tokenBridgeStateObjectId, decimals, - signer.keypair.getPublicKey().toSuiAddress() - ); - setMaxGasBudgetDevnet(network, prepareTx as any); - const prepareRes = await executeTransactionBlock( - signer, - prepareTx as any + signerAddress ); + setMaxGasBudgetDevnet(network, prepareTx); + const prepareRes = await executeTransactionBlock(signer, prepareTx); console.log(` Digest ${prepareRes.digest}`); assertSuccess(prepareRes, "Prepare registration failed."); - // Get the coin package ID from the publish event - const coinPackageId = - prepareRes.objectChanges?.find(isSuiPublishEvent)?.packageId; - if (!coinPackageId) { - throw new Error("Publish coin failed."); - } + // Get the coin package ID from the published package. + const coinPackageId = getPublishedPackageId(prepareRes); console.log(` Published to ${coinPackageId}`); console.log(` Type ${getWrappedCoinType(coinPackageId)}`); @@ -173,31 +169,33 @@ export const submit = async ( } console.log("\n[2/2] Registering asset..."); - const wrappedAssetSetup = prepareRes.objectChanges - ?.filter(isSuiCreateEvent) - .find((e) => - /create_wrapped::WrappedAssetSetup/.test(e.objectType) - ); - if (!wrappedAssetSetup) { + const wrappedAssetSetup = prepareRes.changedObjects.find( + (o) => + o.created && + o.type !== undefined && + /create_wrapped::WrappedAssetSetup/.test(o.type) + ); + if (!wrappedAssetSetup || !wrappedAssetSetup.type) { throw new Error( - "Wrapped asset setup not found. Object changes: " + - JSON.stringify(prepareRes.objectChanges) + "Wrapped asset setup not found. Changed objects: " + + JSON.stringify(prepareRes.changedObjects) ); } const completeTx = await createWrappedOnSui( - client as any, + client, coreBridgeStateObjectId, tokenBridgeStateObjectId, - signer.keypair.getPublicKey().toSuiAddress(), + signerAddress, coinPackageId, - wrappedAssetSetup.objectType, + wrappedAssetSetup.objectId, + wrappedAssetSetup.type, vaa ); - setMaxGasBudgetDevnet(network, completeTx as any); + setMaxGasBudgetDevnet(network, completeTx); const completeRes = await executeTransactionBlock( signer, - completeTx as any + completeTx ); assertSuccess(completeRes, "Complete registration failed."); console.log(` Digest ${completeRes.digest}`); @@ -230,7 +228,6 @@ export const submit = async ( throw Error("Can't complete payload 3 transfer from CLI"); default: impossible(payload); - break; } break; @@ -245,3 +242,136 @@ export const submit = async ( const sleep = (ms: number): Promise => { return new Promise((resolve) => setTimeout(resolve, ms)); }; + +const getWrappedCoinType = (coinPackageId: string): string => + `${coinPackageId}::coin::COIN`; + +/** + * Build the publish payload for a Wormhole wrapped-coin module. The module + * bytecode is a fixed template (see {@link buildWrappedCoinBytecode}) parametrized + * only by the original token bridge package ID and the (capped) decimals. + */ +const getCoinBuildOutput = async ( + client: SuiGrpcClient, + coreBridgePackageId: string, + tokenBridgePackageId: string, + tokenBridgeStateObjectId: string, + decimals: number +): Promise<{ modules: string[]; dependencies: string[] }> => { + const originalTokenBridgePackageId = await getOriginalPackageId( + client, + tokenBridgeStateObjectId + ); + return { + modules: [buildWrappedCoinBytecode(originalTokenBridgePackageId, decimals)], + dependencies: ["0x1", "0x2", tokenBridgePackageId, coreBridgePackageId].map( + (d) => normalizeSuiAddress(d) + ), + }; +}; + +/** + * Step 1 of wrapped asset creation: publish a coin package whose decimals match + * the attested token. The resulting `WrappedAssetSetup` and coin `UpgradeCap` + * are transferred to the signer for use in `createWrappedOnSui`. + */ +const createWrappedOnSuiPrepare = async ( + client: SuiGrpcClient, + coreBridgeStateObjectId: string, + tokenBridgeStateObjectId: string, + decimals: number, + signerAddress: string +): Promise => { + const [coreBridgePackageId, tokenBridgePackageId] = await Promise.all([ + getPackageId(client, coreBridgeStateObjectId), + getPackageId(client, tokenBridgeStateObjectId), + ]); + const build = await getCoinBuildOutput( + client, + coreBridgePackageId, + tokenBridgePackageId, + tokenBridgeStateObjectId, + decimals + ); + + const tx = new Transaction(); + const [upgradeCap] = tx.publish({ + modules: build.modules.map((m) => Array.from(fromBase64(m))), + dependencies: build.dependencies, + }); + tx.transferObjects([upgradeCap], signerAddress); + return tx; +}; + +/** + * Step 2 of wrapped asset creation: verify the attestation VAA and complete the + * registration using the `WrappedAssetSetup` and coin `UpgradeCap` produced by + * `createWrappedOnSuiPrepare`. + */ +const createWrappedOnSui = async ( + client: SuiGrpcClient, + coreBridgeStateObjectId: string, + tokenBridgeStateObjectId: string, + signerAddress: string, + coinPackageId: string, + wrappedAssetSetupObjectId: string, + wrappedAssetSetupType: string, + attestVAA: Buffer +): Promise => { + const [coreBridgePackageId, tokenBridgePackageId] = await Promise.all([ + getPackageId(client, coreBridgeStateObjectId), + getPackageId(client, tokenBridgeStateObjectId), + ]); + + const coinType = getWrappedCoinType(coinPackageId); + const coinMetadataObjectId = (await client.getCoinMetadata({ coinType })) + ?.coinMetadata?.id; + if (!coinMetadataObjectId) { + throw new Error( + `Coin metadata object not found for coin type ${coinType}.` + ); + } + + const coinUpgradeCapObjectId = await getUpgradeCapObjectId( + client, + signerAddress, + coinPackageId + ); + if (!coinUpgradeCapObjectId) { + throw new Error( + `Coin upgrade cap not found for ${coinType} under owner ${signerAddress}. You must call 'createWrappedOnSuiPrepare' first.` + ); + } + + const tx = new Transaction(); + const [vaa] = tx.moveCall({ + target: `${coreBridgePackageId}::vaa::parse_and_verify`, + arguments: [ + tx.object(coreBridgeStateObjectId), + tx.pure("vector", [...attestVAA]), + tx.object(SUI_CLOCK_OBJECT_ID), + ], + }); + const [message] = tx.moveCall({ + target: `${tokenBridgePackageId}::vaa::verify_only_once`, + arguments: [tx.object(tokenBridgeStateObjectId), vaa], + }); + + // WrappedAssetSetup is parametrized by ; the version + // type is the second type argument. + const versionType = normalizeStructTag( + parseStructTag(wrappedAssetSetupType).typeParams[1] + ); + tx.moveCall({ + target: `${tokenBridgePackageId}::create_wrapped::complete_registration`, + arguments: [ + tx.object(tokenBridgeStateObjectId), + tx.object(coinMetadataObjectId), + tx.object(wrappedAssetSetupObjectId), + tx.object(coinUpgradeCapObjectId), + message, + ], + typeArguments: [coinType, versionType], + }); + return tx; +}; diff --git a/clients/js/src/chains/sui/transfer.ts b/clients/js/src/chains/sui/transfer.ts index e55bcf7753a..2d40fb5f07d 100644 --- a/clients/js/src/chains/sui/transfer.ts +++ b/clients/js/src/chains/sui/transfer.ts @@ -1,10 +1,13 @@ -import { transferFromSui } from "@certusone/wormhole-sdk/lib/esm/token_bridge/transfer"; +import { Transaction } from "@mysten/sui/transactions"; import { executeTransactionBlock, + getPackageId, getProvider, getSigner, setMaxGasBudgetDevnet, + SUI_CLOCK_OBJECT_ID, } from "./utils"; +import { getObjectFields } from "../../sdk/sui"; import { Chain, Network, @@ -13,6 +16,8 @@ import { } from "@wormhole-foundation/sdk-base"; import { tryNativeToUint8Array } from "../../sdk/array"; +const SUI_TYPE_ARG = "0x2::sui::SUI"; + export async function transferSui( dstChain: Chain, dstAddress: string, @@ -29,27 +34,103 @@ export async function transferSui( if (!token_bridge) { throw new Error("Token bridge object ID is undefined"); } + const client = getProvider(network, rpc); const signer = getSigner(client, network); const owner = signer.keypair.getPublicKey().toSuiAddress(); - const coinType = tokenAddress === "native" ? "0x2::sui::SUI" : tokenAddress; - const coins = ( - await client.getCoins({ - owner, - coinType, - }) - ).data; - const tx = await transferFromSui( - client as any, - core, - token_bridge, - coins, - coinType, - BigInt(amount), - chainToChainId(dstChain), - tryNativeToUint8Array(dstAddress, chainToChainId(dstChain)) - ); - setMaxGasBudgetDevnet(network, tx as any); - const result = await executeTransactionBlock(signer, tx as any); + const coinType = tokenAddress === "native" ? SUI_TYPE_ARG : tokenAddress; + const recipientChainId = chainToChainId(dstChain); + const recipient = tryNativeToUint8Array(dstAddress, recipientChainId); + const amt = BigInt(amount); + + // Collect the sender's coins of the transfer type so they can be merged. + const coinObjectIds: string[] = []; + let cursor: string | null = null; + let hasNextPage = true; + while (hasNextPage) { + const page = await client.listCoins({ owner, coinType, cursor }); + for (const coin of page.objects) { + coinObjectIds.push(coin.objectId); + } + hasNextPage = page.hasNextPage; + cursor = page.cursor; + } + if (coinObjectIds.length === 0) { + throw new Error(`No coins of type ${coinType} found for ${owner}`); + } + + const [coreBridgePackageId, tokenBridgePackageId] = await Promise.all([ + getPackageId(client, core), + getPackageId(client, token_bridge), + ]); + + const coreFields = await getObjectFields(client, core); + const messageFee = BigInt(coreFields?.fee_collector?.fee_amount ?? 0); + + const tx = new Transaction(); + + const [transferCoin] = (() => { + if (coinType === SUI_TYPE_ARG) { + return tx.splitCoins(tx.gas, [tx.pure("u64", amt)]); + } + const [primaryCoinId, ...mergeCoinIds] = coinObjectIds; + const primaryCoinInput = tx.object(primaryCoinId); + if (mergeCoinIds.length) { + tx.mergeCoins( + primaryCoinInput, + mergeCoinIds.map((id) => tx.object(id)) + ); + } + return tx.splitCoins(primaryCoinInput, [tx.pure("u64", amt)]); + })(); + + const [feeCoin] = tx.splitCoins(tx.gas, [tx.pure("u64", messageFee)]); + + const [assetInfo] = tx.moveCall({ + target: `${tokenBridgePackageId}::state::verified_asset`, + arguments: [tx.object(token_bridge)], + typeArguments: [coinType], + }); + + // Random 32-bit transfer nonce. + const nonce = Math.floor(Math.random() * 0xffffffff); + + const [transferTicket, dust] = tx.moveCall({ + target: `${tokenBridgePackageId}::transfer_tokens::prepare_transfer`, + arguments: [ + assetInfo, + transferCoin, + tx.pure("u16", recipientChainId), + tx.pure("vector", [...recipient]), + tx.pure("u64", BigInt(0)), + tx.pure("u32", nonce), + ], + typeArguments: [coinType], + }); + + tx.moveCall({ + target: `${tokenBridgePackageId}::coin_utils::return_nonzero`, + arguments: [dust], + typeArguments: [coinType], + }); + + const [messageTicket] = tx.moveCall({ + target: `${tokenBridgePackageId}::transfer_tokens::transfer_tokens`, + arguments: [tx.object(token_bridge), transferTicket], + typeArguments: [coinType], + }); + + tx.moveCall({ + target: `${coreBridgePackageId}::publish_message::publish_message`, + arguments: [ + tx.object(core), + feeCoin, + messageTicket, + tx.object(SUI_CLOCK_OBJECT_ID), + ], + }); + + setMaxGasBudgetDevnet(network, tx); + const result = await executeTransactionBlock(signer, tx); console.log(JSON.stringify(result)); } diff --git a/clients/js/src/chains/sui/utils.ts b/clients/js/src/chains/sui/utils.ts index 49ee02855c2..c233d135454 100644 --- a/clients/js/src/chains/sui/utils.ts +++ b/clients/js/src/chains/sui/utils.ts @@ -1,10 +1,10 @@ -import { SuiClient, SuiTransactionBlockResponse } from "@mysten/sui/client"; +import { SuiGrpcClient } from "@mysten/sui/grpc"; +import type { SuiClientTypes } from "@mysten/sui/client"; import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519"; import { Transaction } from "@mysten/sui/transactions"; -import { fromB64, toB64 } from "@mysten/bcs"; +import { fromBase64 } from "@mysten/sui/utils"; import { NETWORKS } from "../../consts"; import { Payload, VAA, parse, serialiseVAA } from "../../vaa"; -import { SuiRpcValidationError } from "./error"; import { Network } from "@wormhole-foundation/sdk"; import { isValidSuiAddress } from "../../sdk/sui"; @@ -15,93 +15,241 @@ const UPGRADE_CAP_TYPE = "0x2::package::UpgradeCap"; // Re-export for other modules export { SUI_CLOCK_OBJECT_ID }; -// Type for signer - combines keypair and client +// Type for signer - combines keypair, gRPC client, and the selected network +// (used to verify the endpoint matches before signing). export interface SuiSigner { keypair: Ed25519Keypair; - client: SuiClient; + client: SuiGrpcClient; + network: Network; } +/** + * Normalized view of an object touched by a transaction. The gRPC transport + * exposes mutations as `effects.changedObjects` plus an `objectId -> type` map, + * which we flatten here so callers don't need to reassemble the two. + */ +export interface SuiChangedObject { + objectId: string; + type?: string; + owner: string; + created: boolean; + isPackage: boolean; +} + +/** + * Normalized transaction result. The gRPC client returns a discriminated + * `{ Transaction | FailedTransaction }` envelope whose shape differs from the + * legacy JSON-RPC `SuiTransactionBlockResponse`; this is the single shape the + * rest of the CLI consumes. + */ +export interface SuiTransactionResult { + digest: string; + success: boolean; + error?: string; + sender?: string; + changedObjects: SuiChangedObject[]; + events: { + packageId: string; + module: string; + sender: string; + eventType: string; + json: Record | null; + }[]; +} + +export const getSuiNetwork = ( + network?: Network +): "mainnet" | "testnet" | "localnet" => { + switch (network) { + case "Mainnet": + return "mainnet"; + case "Testnet": + return "testnet"; + // Wormhole's "Devnet" is a local Sui node, which Sui itself labels + // "localnet". An unset network (RPC supplied without --network) gets the + // same conservative label, since the gRPC client uses `network` only as an + // advisory hint and routes on `baseUrl`. + case "Devnet": + case undefined: + return "localnet"; + default: + throw new Error(`Unsupported Sui network: ${network}`); + } +}; + +/** + * Known Sui chain identifiers (the genesis checkpoint digest, fixed per chain) + * used to ensure the selected `--network` and the possibly-overridden `--rpc` + * endpoint actually agree before any transaction is signed. Devnet/localnet + * identifiers are genesis-specific, so those networks are not asserted. + */ +const SUI_CHAIN_IDENTIFIERS: Partial> = { + Mainnet: "4btiuiMPvEENsttpZC7CZ53DruC3MAgfznDbASZ7DR6S", + Testnet: "69WiPg3DAQiwdxfncX6wYQ2siKwAe6L9BZthQea3JNMD", +}; + +const verifiedClients = new WeakSet(); + +/** + * Guard against a network/endpoint mismatch (e.g. a Mainnet governance action + * pointed at a Testnet or forked RPC). Fetches the endpoint's chain identifier + * and throws if it does not match the expected `network`. No-op for networks + * without a known identifier (Devnet/localnet). Result is cached per client. + */ +export const assertSuiNetwork = async ( + client: SuiGrpcClient, + network: Network +): Promise => { + const expected = SUI_CHAIN_IDENTIFIERS[network]; + if (!expected || verifiedClients.has(client)) { + return; + } + const { chainIdentifier } = await client.core.getChainIdentifier(); + if (chainIdentifier !== expected) { + throw new Error( + `Refusing to proceed: the Sui RPC reports chain identifier ${chainIdentifier}, ` + + `but ${network} is expected to be ${expected}. ` + + `Check that --rpc matches --network ${network}.` + ); + } + verifiedClients.add(client); +}; + +const ownerToString = (owner: { + $kind: string; + AddressOwner?: string; + ObjectOwner?: string; +}): string => { + if (owner.$kind === "AddressOwner" && owner.AddressOwner) { + return owner.AddressOwner; + } + if (owner.$kind === "ObjectOwner" && owner.ObjectOwner) { + return owner.ObjectOwner; + } + if (owner.$kind === "Shared") return "Shared"; + if (owner.$kind === "Immutable") return "Immutable"; + return "Unknown"; +}; + export const assertSuccess = ( - res: SuiTransactionBlockResponse, + res: SuiTransactionResult, error: string ): void => { - if (res?.effects?.status?.status !== "success") { + if (!res.success) { throw new Error(`${error} Response: ${JSON.stringify(res)}`); } }; +// Fields requested from the gRPC transport whenever a transaction result is +// flattened into a `SuiTransactionResult`. `objectTypes` is required to populate +// the per-object `type`, which the effects' `changedObjects` do not carry. +const TX_RESULT_INCLUDE = { + effects: true, + events: true, + objectTypes: true, + transaction: true, +} as const; + +// The gRPC transaction envelope (the `Transaction`/`FailedTransaction` arm of a +// `TransactionResult`), parameterized by the fields we request so the +// include-gated members (`effects`, `objectTypes`, ...) are present on the type. +type GrpcTransaction = SuiClientTypes.Transaction; + +/** + * Flatten a gRPC transaction envelope into the CLI's `SuiTransactionResult`. + * The gRPC transport exposes mutations as `effects.changedObjects` (with + * `outputState`/`idOperation` enums) plus a separate `objectId -> type` map; + * this reassembles the two into the single shape the rest of the CLI consumes. + */ +export const toSuiTransactionResult = ( + tx: GrpcTransaction +): SuiTransactionResult => { + const objectTypes = tx.objectTypes ?? {}; + return { + digest: tx.digest, + success: tx.status.success, + error: tx.status.success + ? undefined + : tx.status.error?.message ?? JSON.stringify(tx.status.error), + sender: tx.transaction?.sender ?? undefined, + changedObjects: (tx.effects?.changedObjects ?? []).map((o) => ({ + objectId: o.objectId, + type: objectTypes[o.objectId], + owner: o.outputOwner ? ownerToString(o.outputOwner) : "Unknown", + created: o.idOperation === "Created", + isPackage: o.outputState === "PackageWrite", + })), + events: (tx.events ?? []).map((e) => ({ + packageId: e.packageId, + module: e.module, + sender: e.sender, + eventType: e.eventType, + json: e.json, + })), + }; +}; + export const executeTransactionBlock = async ( signer: SuiSigner, transaction: Transaction -): Promise => { +): Promise => { + await assertSuiNetwork(signer.client, signer.network); const res = await signer.client.signAndExecuteTransaction({ signer: signer.keypair, transaction, - options: { - showInput: true, - showEffects: true, - showEvents: true, - showObjectChanges: true, - }, + include: TX_RESULT_INCLUDE, }); - return res; + const tx = res.Transaction ?? res.FailedTransaction; + if (!tx) { + throw new Error( + `Unexpected empty transaction response: ${JSON.stringify(res)}` + ); + } + + return toSuiTransactionResult(tx); }; -export const findOwnedObjectByType = async ( - client: SuiClient, - owner: string, - type: string, - cursor?: string -): Promise => { - const res = await client.getOwnedObjects({ - owner, - filter: undefined, // Filter must be undefined to avoid 504 responses - cursor: cursor || undefined, - options: { - showType: true, - }, +/** + * Read an already-committed transaction's effects over gRPC and flatten them + * into a `SuiTransactionResult`. Used for transactions executed outside the SDK + * (e.g. `sui client test-publish`, which signs with the local CLI keystore and + * commits on-chain) so their results can be consumed without parsing the CLI's + * deprecated JSON-RPC `objectChanges` payload. + */ +export const fetchTransactionResult = async ( + client: SuiGrpcClient, + digest: string +): Promise => { + // The transaction may not be queryable the instant the CLI returns; block + // until the node has indexed it before reading effects. + await client.waitForTransaction({ digest }); + const res = await client.getTransaction({ + digest, + include: TX_RESULT_INCLUDE, }); - if (!res || !res.data) { - throw new SuiRpcValidationError(res); + const tx = res.Transaction ?? res.FailedTransaction; + if (!tx) { + throw new Error(`Transaction ${digest} not found`); } - const object = res.data.find((d) => d.data?.type === type); - - if (!object && res.hasNextPage) { - return findOwnedObjectByType(client, owner, type, res.nextCursor as string); - } else if (!object && !res.hasNextPage) { - return null; - } else { - return object?.data?.objectId ?? null; - } + return toSuiTransactionResult(tx); }; export const getCreatedObjects = ( - res: SuiTransactionBlockResponse + res: SuiTransactionResult ): { type: string; objectId: string; owner: string }[] => - res.objectChanges?.filter(isSuiCreateEvent).map((e) => { - let owner: string; - if (typeof e.owner === "string") { - owner = e.owner; - } else if ("AddressOwner" in e.owner) { - owner = e.owner.AddressOwner; - } else if ("ObjectOwner" in e.owner) { - owner = e.owner.ObjectOwner; - } else { - owner = "Shared"; - } - - return { - owner, - type: e.objectType, - objectId: e.objectId, - }; - }) ?? []; + res.changedObjects + .filter((o) => o.created && !o.isPackage && o.type) + .map((o) => ({ + type: o.type as string, + objectId: o.objectId, + owner: o.owner, + })); export const getOwnedObjectId = async ( - client: SuiClient, + client: SuiGrpcClient, owner: string, packageId: string, moduleName: string, @@ -116,80 +264,80 @@ export const getOwnedObjectId = async ( ); } - try { - const res = await client.getOwnedObjects({ - owner, - filter: { StructType: type }, - options: { - showContent: true, - }, - }); - if (!res || !res.data) { - throw new SuiRpcValidationError(res); + const objectIds: string[] = []; + let cursor: string | null = null; + let hasNextPage = true; + while (hasNextPage) { + const res: SuiClientTypes.ListOwnedObjectsResponse = + await client.listOwnedObjects({ owner, type, cursor }); + for (const o of res.objects) { + if (o.type === type) { + objectIds.push(o.objectId); + } } + hasNextPage = res.hasNextPage; + cursor = res.cursor; + } - const objects = res.data.filter((o) => o.data?.objectId); - if (objects.length === 1) { - return objects[0].data?.objectId ?? null; - } else if (objects.length > 1) { - const objectsStr = JSON.stringify(objects, null, 2); - throw new Error( - `Found multiple objects owned by ${owner} of type ${type}. This may mean that we've received an unexpected response from the Sui RPC and \`worm\` logic needs to be updated to handle this. Objects: ${objectsStr}` - ); - } else { - return null; - } - } catch (error) { - // Handle 504 error by using findOwnedObjectByType method - const is504HttpError = `${error}`.includes("504 Gateway Time-out"); - if (error && is504HttpError) { - return findOwnedObjectByType(client, owner, type); - } else { - throw error; - } + if (objectIds.length === 1) { + return objectIds[0]; + } else if (objectIds.length > 1) { + throw new Error( + `Found multiple objects owned by ${owner} of type ${type}. This may mean that we've received an unexpected response from the Sui RPC and \`worm\` logic needs to be updated to handle this. Objects: ${JSON.stringify( + objectIds, + null, + 2 + )}` + ); } + return null; }; // TODO(kp): remove this once it's in the sdk export const getPackageId = async ( - client: SuiClient, + client: SuiGrpcClient, objectId: string ): Promise => { - let currentPackage; - let nextCursor: string | null | undefined; - do { - const dynamicFields = await client.getDynamicFields({ - parentId: objectId, - cursor: nextCursor, - }); - currentPackage = dynamicFields.data.find((field) => + let cursor: string | null = null; + let hasNextPage = true; + while (hasNextPage) { + const res: SuiClientTypes.ListDynamicFieldsResponse = + await client.listDynamicFields({ parentId: objectId, cursor }); + const currentPackage = res.dynamicFields.find((field) => field.name.type.endsWith("CurrentPackage") ); - nextCursor = dynamicFields.hasNextPage ? dynamicFields.nextCursor : null; - } while (nextCursor && !currentPackage); - if (!currentPackage) { - throw new Error("CurrentPackage not found"); + if (currentPackage) { + const obj = await client.getObject({ + objectId: currentPackage.fieldId, + include: { json: true }, + }); + const packageId = (obj.object.json as any)?.value?.package; + if (!packageId) { + throw new Error("Unable to get current package"); + } + return packageId; + } + hasNextPage = res.hasNextPage; + cursor = res.cursor; } - const obj = await client.getObject({ - id: currentPackage.objectId, - options: { - showContent: true, - }, - }); - const content = obj.data?.content; - const packageId = - content && "fields" in content - ? (content.fields as any).value?.fields?.package - : null; - if (!packageId) { - throw new Error("Unable to get current package"); - } + throw new Error("CurrentPackage not found"); +}; - return packageId; +/** + * Returns the original (type-origin) package ID for a state object. Move type + * origins are stable across upgrades, so this differs from `getPackageId`, which + * returns the current/upgraded package. + */ +export const getOriginalPackageId = async ( + client: SuiGrpcClient, + stateObjectId: string +): Promise => { + const res = await client.getObject({ objectId: stateObjectId }); + return res.object.type.split("::")[0]; }; -export const getProvider = (network?: Network, rpc?: string): SuiClient => { +export const getProvider = (network?: Network, rpc?: string): SuiGrpcClient => { if (!network && !rpc) { throw new Error("Must provide network or RPC to initialize provider"); } @@ -199,25 +347,23 @@ export const getProvider = (network?: Network, rpc?: string): SuiClient => { throw new Error(`No default RPC found for Sui ${network}`); } - return new SuiClient({ url: rpc }); + return new SuiGrpcClient({ network: getSuiNetwork(network), baseUrl: rpc }); }; -export const getPublishedPackageId = ( - res: SuiTransactionBlockResponse -): string => { - const publishEvents = res.objectChanges?.filter(isSuiPublishEvent) ?? []; - if (publishEvents.length !== 1) { +export const getPublishedPackageId = (res: SuiTransactionResult): string => { + const packages = res.changedObjects.filter((o) => o.isPackage); + if (packages.length !== 1) { throw new Error( - "Unexpected number of publish events found:" + - JSON.stringify(publishEvents, null, 2) + "Unexpected number of published packages found:" + + JSON.stringify(packages, null, 2) ); } - return publishEvents[0].packageId; + return packages[0].objectId; }; export const getSigner = ( - client: SuiClient, + client: SuiGrpcClient, network: Network, customPrivateKey?: string ): SuiSigner => { @@ -229,7 +375,7 @@ export const getSigner = ( let bytes = privateKey.startsWith("0x") ? Buffer.from(privateKey.slice(2), "hex") - : fromB64(privateKey); + : fromBase64(privateKey); if (bytes.length === 33) { // remove the first flag byte after checking it is indeed the Ed25519 scheme flag 0x00 if (bytes[0] !== 0) { @@ -237,8 +383,8 @@ export const getSigner = ( } bytes = bytes.subarray(1); } - const keypair = Ed25519Keypair.fromSecretKey(bytes); - return { keypair, client }; + const keypair = Ed25519Keypair.fromSecretKey(Uint8Array.from(bytes)); + return { keypair, client, network }; }; /** @@ -248,44 +394,49 @@ export const getSigner = ( * Structs created by the Sui framework such as `UpgradeCap`s all have the same * type (e.g. `0x2::package::UpgradeCap`) and have a special field, `package`, * we can use to differentiate them. - * @param client Sui RPC client + * @param client Sui gRPC client * @param owner Address of the current owner of the `UpgradeCap` * @param packageId ID of the package that the `UpgradeCap` was created for * @returns The object ID of the `UpgradeCap` if it exists, otherwise `null` */ export const getUpgradeCapObjectId = async ( - client: SuiClient, + client: SuiGrpcClient, owner: string, packageId: string ): Promise => { - const res = await client.getOwnedObjects({ - owner, - filter: { StructType: UPGRADE_CAP_TYPE }, - options: { - showContent: true, - }, - }); - if (!res || !res.data) { - throw new SuiRpcValidationError(res); + const objectIds: string[] = []; + let cursor: string | null = null; + let hasNextPage = true; + while (hasNextPage) { + const res: SuiClientTypes.ListOwnedObjectsResponse<{ json: true }> = + await client.listOwnedObjects({ + owner, + type: UPGRADE_CAP_TYPE, + cursor, + include: { json: true }, + }); + for (const o of res.objects) { + const pkg = (o.json as any)?.package; + if (pkg === packageId) { + objectIds.push(o.objectId); + } + } + hasNextPage = res.hasNextPage; + cursor = res.cursor; } - const objects = res.data.filter( - (o) => - o.data?.objectId && - o.data?.content?.dataType === "moveObject" && - (o.data?.content?.fields as any)?.package === packageId - ); - if (objects.length === 1) { - // We've found the object we're looking for - return objects[0].data?.objectId ?? null; - } else if (objects.length > 1) { - const objectsStr = JSON.stringify(objects, null, 2); + if (objectIds.length === 1) { + return objectIds[0]; + } else if (objectIds.length > 1) { throw new Error( - `Found multiple upgrade capabilities owned by ${owner} from package ${packageId}. Objects: ${objectsStr}` + `Found multiple upgrade capabilities owned by ${owner} from package ${packageId}. Objects: ${JSON.stringify( + objectIds, + null, + 2 + )}` ); - } else { - return null; } + return null; }; export const isSameType = (a: string, b: string) => { @@ -296,20 +447,6 @@ export const isSameType = (a: string, b: string) => { } }; -export const isSuiCreateEvent = < - T extends NonNullable[number], - K extends Extract ->( - event: T -): event is K => event?.type === "created"; - -export const isSuiPublishEvent = < - T extends NonNullable[number], - K extends Extract ->( - event: T -): event is K => event?.type === "published"; - // Normalize Sui address export const normalizeSuiAddress = (address: string): string => { // Remove 0x prefix, pad to 64 chars, add 0x back @@ -329,7 +466,7 @@ export const normalizeSuiType = (type: string): string => { }; export const registerChain = async ( - client: SuiClient, + client: SuiGrpcClient, network: Network, vaa: Buffer, coreBridgeStateObjectId: string, diff --git a/clients/js/src/chains/sui/wrappedCoinBytecode.ts b/clients/js/src/chains/sui/wrappedCoinBytecode.ts new file mode 100644 index 00000000000..ca6068c42d8 --- /dev/null +++ b/clients/js/src/chains/sui/wrappedCoinBytecode.ts @@ -0,0 +1,51 @@ +/** + * Precompiled bytecode for the Wormhole wrapped-coin Move module. + * + * Creating a wrapped asset on Sui publishes a fresh single-module coin package. + * Rather than invoke the Move compiler at attestation time, we ship the compiled + * module and splice in the only two values it is parametrized by: the original + * token bridge package ID (into the module's address table) and the coin + * decimals (into the `init` function). The bytecode is otherwise fixed. + * + * Ported verbatim from `@certusone/wormhole-sdk` (`sdk/js/src/sui/publish.ts`). + * To regenerate if the module ever changes: compile the `wrapped_coin` Move + * template, hex-encode the module, and re-split the output around the token + * bridge address and the decimals byte into the three fragments below. + */ + +/** Decimals are capped at 8 for Sui wrapped assets. */ +const MAX_WRAPPED_COIN_DECIMALS = 8; + +/** Compiled module up to (and excluding) the spliced token bridge package ID. */ +const WRAPPED_COIN_BYTECODE_PREFIX = + "a11ceb0b060000000901000a020a14031e1704350405392d07669f01088502600ae502050cea02160004010b010c0205020d000002000201020003030c020001000104020700000700010001090801010c020a050600030803040202000302010702080007080100020800080303090002070801010b020209000901010608010105010b0202080008030209000504434f494e095478436f6e7465787408565f5f305f325f3011577261707065644173736574536574757004636f696e0e6372656174655f777261707065640b64756d6d795f6669656c6404696e697414707265706172655f726567697374726174696f6e0f7075626c69635f7472616e736665720673656e646572087472616e736665720a74785f636f6e746578740f76657273696f6e5f636f6e74726f6c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002"; + +/** Fixed bytes between the token bridge package ID and the decimals byte. */ +const WRAPPED_COIN_BYTECODE_BEFORE_DECIMALS = "00020106010000000001090b0031"; + +/** Fixed bytes following the decimals byte (the remainder of `init`). */ +const WRAPPED_COIN_BYTECODE_AFTER_DECIMALS = "0a0138000b012e110238010200"; + +/** + * Assemble the base64-encoded wrapped-coin module for a given token bridge + * package and decimals. + * + * @param originalTokenBridgePackageId The original (type-origin) token bridge + * package ID, with or without a `0x` prefix. + * @param decimals Desired coin decimals; capped at {@link MAX_WRAPPED_COIN_DECIMALS}. + * @returns The compiled module bytecode, base64-encoded. + */ +export const buildWrappedCoinBytecode = ( + originalTokenBridgePackageId: string, + decimals: number +): string => { + const strippedPackageId = originalTokenBridgePackageId.replace(/^0x/, ""); + const cappedDecimals = Math.min(decimals, MAX_WRAPPED_COIN_DECIMALS); + const bytecodeHex = + WRAPPED_COIN_BYTECODE_PREFIX + + strippedPackageId + + WRAPPED_COIN_BYTECODE_BEFORE_DECIMALS + + cappedDecimals.toString(16).padStart(2, "0") + + WRAPPED_COIN_BYTECODE_AFTER_DECIMALS; + return Buffer.from(bytecodeHex, "hex").toString("base64"); +}; diff --git a/clients/js/src/cmds/sui/deploy.ts b/clients/js/src/cmds/sui/deploy.ts index d579e80b9cf..5aab2dcc015 100644 --- a/clients/js/src/cmds/sui/deploy.ts +++ b/clients/js/src/cmds/sui/deploy.ts @@ -1,4 +1,3 @@ -import { SuiTransactionBlockResponse } from "@mysten/sui/client"; import fs from "fs"; import yargs from "yargs"; import { @@ -9,6 +8,7 @@ import { logTransactionDigest, logTransactionSender, publishPackage, + SuiTransactionResult, } from "../../chains/sui"; import { DEBUG_OPTIONS, @@ -65,7 +65,7 @@ export const deploy = async ( packageDir: string, rpc?: string, privateKey?: string -): Promise => { +): Promise => { rpc = rpc ?? NETWORKS[network].Sui.rpc; const provider = getProvider(network, rpc); const signer = getSigner(provider, network, privateKey); diff --git a/clients/js/src/cmds/sui/init.ts b/clients/js/src/cmds/sui/init.ts index 1935a0bf5ce..b16b04cea83 100644 --- a/clients/js/src/cmds/sui/init.ts +++ b/clients/js/src/cmds/sui/init.ts @@ -1,4 +1,3 @@ -import { SuiTransactionBlockResponse } from "@mysten/sui/client"; import { Transaction } from "@mysten/sui/transactions"; import yargs from "yargs"; import { @@ -13,6 +12,7 @@ import { logTransactionDigest, logTransactionSender, setMaxGasBudgetDevnet, + SuiTransactionResult, } from "../../chains/sui"; import { DEBUG_OPTIONS, @@ -220,7 +220,7 @@ export const initExampleApp = async ( wormholeStateObjectId: string, rpc?: string, privateKey?: string -): Promise => { +): Promise => { rpc = rpc ?? NETWORKS[network].Sui.rpc; const client = getProvider(network, rpc); const signer = getSigner(client, network, privateKey); @@ -242,7 +242,7 @@ export const initTokenBridge = async ( governanceContract: string, rpc?: string, privateKey?: string -): Promise => { +): Promise => { rpc = rpc ?? NETWORKS[network].Sui.rpc; const client = getProvider(network, rpc); const signer = getSigner(client, network, privateKey); @@ -302,7 +302,7 @@ export const initWormhole = async ( governanceContract: string, rpc?: string, privateKey?: string -): Promise => { +): Promise => { rpc = rpc ?? NETWORKS[network].Sui.rpc; const client = getProvider(network, rpc); const signer = getSigner(client, network, privateKey); diff --git a/clients/js/src/cmds/sui/publishMessage.ts b/clients/js/src/cmds/sui/publishMessage.ts index 9058f8a391a..b994fe79db5 100644 --- a/clients/js/src/cmds/sui/publishMessage.ts +++ b/clients/js/src/cmds/sui/publishMessage.ts @@ -86,7 +86,7 @@ export const addPublishMessageCommands: YargsAddCommandsFn = ( const event = res.events?.find( (e) => normalizeSuiAddress(e.packageId) === normalizeSuiAddress(packageId) && - e.type.includes("publish_message::WormholeMessage") + e.eventType.includes("publish_message::WormholeMessage") ); if (!event) { throw new Error( @@ -97,13 +97,16 @@ export const addPublishMessageCommands: YargsAddCommandsFn = ( logTransactionDigest(res); logTransactionSender(res); + // In the gRPC event JSON, `payload` (vector) is base64 while + // `sender` (ID) is a 0x-prefixed hex string. + const parsed = event.json as any; console.log("Publish message succeeded:", { sender: event.sender, - type: event.type, - payload: Buffer.from((event.parsedJson as any)?.payload).toString(), - emitter: Buffer.from((event.parsedJson as any)?.sender).toString("hex"), - sequence: (event.parsedJson as any)?.sequence, - nonce: (event.parsedJson as any)?.nonce, + type: event.eventType, + payload: Buffer.from(parsed.payload, "base64").toString(), + emitter: parsed.sender.replace(/^0x/, ""), + sequence: parsed.sequence, + nonce: parsed.nonce, }); } ); diff --git a/clients/js/src/cmds/sui/setup.ts b/clients/js/src/cmds/sui/setup.ts index 47664eac2bf..7152eaaf0bc 100644 --- a/clients/js/src/cmds/sui/setup.ts +++ b/clients/js/src/cmds/sui/setup.ts @@ -1,5 +1,5 @@ import { parseTokenBridgeRegisterChainVaa } from "@certusone/wormhole-sdk/lib/esm/vaa/tokenBridge"; -import { SuiClient } from "@mysten/sui/client"; +import { SuiGrpcClient } from "@mysten/sui/grpc"; import { Transaction } from "@mysten/sui/transactions"; import dotenv from "dotenv"; import fs from "fs"; @@ -233,9 +233,9 @@ export const addSetupCommands: YargsAddCommandsFn = (y: typeof yargs) => ); const getEmitterCapObjectId = async ( - client: SuiClient, + client: SuiGrpcClient, tokenBridgeStateObjectId: string -): Promise => { +): Promise => { const fields = await getObjectFields(client, tokenBridgeStateObjectId); - return fields?.emitter_cap?.fields?.id?.id; + return fields?.emitter_cap?.id; }; diff --git a/clients/js/src/cmds/sui/utils.ts b/clients/js/src/cmds/sui/utils.ts index 979cadb6f91..f0fc25fe0dc 100644 --- a/clients/js/src/cmds/sui/utils.ts +++ b/clients/js/src/cmds/sui/utils.ts @@ -1,4 +1,4 @@ -import { PaginatedObjectsResponse } from "@mysten/sui/client"; +import type { SuiClientTypes } from "@mysten/sui/client"; import yargs from "yargs"; import { getPackageId, getProvider } from "../../chains/sui"; import { NETWORKS, NETWORK_OPTIONS, RPC_OPTIONS } from "../../consts"; @@ -25,21 +25,19 @@ export const addUtilsCommands: YargsAddCommandsFn = (y: typeof yargs) => const owner = argv.owner; const client = getProvider(network, rpc); - const objects: PaginatedObjectsResponse["data"] = []; + const objects: SuiClientTypes.ListOwnedObjectsResponse["objects"] = []; - let cursor: PaginatedObjectsResponse["nextCursor"] | undefined = - undefined; - while (true) { - const res: PaginatedObjectsResponse = await client.getOwnedObjects({ - owner, - cursor, - }); - objects.push(...res.data); - if (res.hasNextPage) { - cursor = res.nextCursor; - } else { - break; - } + let cursor: string | null = null; + let hasNextPage = true; + while (hasNextPage) { + const res: SuiClientTypes.ListOwnedObjectsResponse = + await client.listOwnedObjects({ + owner, + cursor, + }); + objects.push(...res.objects); + hasNextPage = res.hasNextPage; + cursor = res.cursor; } console.log("Network", network); @@ -90,18 +88,24 @@ export const addUtilsCommands: YargsAddCommandsFn = (y: typeof yargs) => const network = getNetwork(argv.network); const rpc = argv.rpc ?? NETWORKS[network].Sui.rpc; const provider = getProvider(network, rpc); + const res = await provider.getTransaction({ + digest: argv["transaction-digest"], + include: { + transaction: true, + effects: true, + events: true, + objectTypes: true, + }, + }); console.log( JSON.stringify( - await provider.getTransactionBlock({ - digest: argv["transaction-digest"], - options: { - showInput: true, - showEffects: true, - showEvents: true, - showObjectChanges: true, - }, - }), - null, + res.Transaction ?? res.FailedTransaction, + (_key, value) => + value instanceof Uint8Array + ? Buffer.from(value).toString("base64") + : typeof value === "bigint" + ? value.toString() + : value, 2 ) ); diff --git a/clients/js/src/sdk/sui.ts b/clients/js/src/sdk/sui.ts index 729fe0e43b0..525429a5460 100644 --- a/clients/js/src/sdk/sui.ts +++ b/clients/js/src/sdk/sui.ts @@ -1,9 +1,27 @@ -import { SuiClient, SuiObjectResponse } from "@mysten/sui/client"; +import { SuiGrpcClient } from "@mysten/sui/grpc"; import { Chain, chainToChainId } from "@wormhole-foundation/sdk"; import { normalizeSuiAddress } from "../chains/sui/utils"; +// BCS schema for the `token_registry::CoinTypeKey` dynamic-field key used to look +// up a wrapped/native coin type by (chain, address). Field order must match the +// Move struct: `chain: u16` then `addr: vector` (a left-padded 32-byte +// external address). Verified against the on-chain table on Sui mainnet. +let CoinTypeKeyBcsCache: any; +export const CoinTypeKeyBcs = { + serialize(data: { chain: number; addr: number[] | Uint8Array }) { + if (!CoinTypeKeyBcsCache) { + const { bcs } = require("@mysten/sui/bcs"); + CoinTypeKeyBcsCache = bcs.struct("CoinTypeKey", { + chain: bcs.u16(), + addr: bcs.vector(bcs.u8()), + }); + } + return CoinTypeKeyBcsCache.serialize(data); + }, +} as any; + export async function getForeignAssetSui( - client: SuiClient, + client: SuiGrpcClient, tokenBridgeStateObjectId: string, originChain: Chain, originAddress: Uint8Array @@ -17,55 +35,147 @@ export async function getForeignAssetSui( ); } +/** Marker key value (`struct Key has copy, drop, store { dummy_field: bool }`). */ +let RegistryKeyBcsCache: any; +function getRegistryKeyBcs() { + if (!RegistryKeyBcsCache) { + const { bcs } = require("@mysten/sui/bcs"); + RegistryKeyBcsCache = bcs.struct("Key", { dummy_field: bcs.bool() }); + } + return RegistryKeyBcsCache; +} + +export interface SuiOriginalAssetInfo { + isWrapped: boolean; + chainId: number; + assetAddress: Uint8Array; +} + +/** + * Reverse lookup for a Sui coin type: determine whether it is a Wormhole-wrapped + * asset and, if so, its origin chain and address. Reads the + * `token_registry::Key` dynamic field and inspects whether the stored + * value is a `WrappedAsset` or `NativeAsset`. + */ +export const getOriginalAssetSui = async ( + client: SuiGrpcClient, + tokenBridgeStateObjectId: string, + coinType: string +): Promise => { + if (!isValidSuiType(coinType)) { + throw new Error(`Invalid Sui type: ${coinType}`); + } + + const state = await client.getObject({ + objectId: tokenBridgeStateObjectId, + include: { json: true }, + }); + const registryId = (state.object.json as any)?.token_registry?.id; + if (!registryId) { + throw new Error("Unable to fetch token registry object ID"); + } + const originalPackageId = state.object.type.split("::")[0]; + + let fieldId: string; + try { + const res = await client.getDynamicField({ + parentId: registryId, + name: { + type: `${originalPackageId}::token_registry::Key<${coinType}>`, + bcs: getRegistryKeyBcs().serialize({ dummy_field: false }).toBytes(), + }, + }); + fieldId = res.dynamicField.fieldId; + } catch { + throw new Error( + `Token of type ${coinType} has not been registered with the token bridge` + ); + } + + const obj = await client.getObject({ + objectId: fieldId, + include: { json: true }, + }); + const type = obj.object.type; + const value = (obj.object.json as any)?.value; + + if (type.includes("wrapped_asset::WrappedAsset<")) { + return { + isWrapped: true, + chainId: Number(value.info.token_chain), + assetAddress: new Uint8Array( + Buffer.from(value.info.token_address.value.data, "base64") + ), + }; + } else if (type.includes("native_asset::NativeAsset<")) { + return { + isWrapped: false, + chainId: chainToChainId("Sui"), + assetAddress: new Uint8Array( + Buffer.from(value.token_address.value.data, "base64") + ), + }; + } + + throw new Error(`Unrecognized token metadata type ${type} for ${coinType}`); +}; + export const getTokenCoinType = async ( - client: SuiClient, + client: SuiGrpcClient, tokenBridgeStateObjectId: string, tokenAddress: Uint8Array, tokenChain: number ): Promise => { - const tokenBridgeStateFields = await getObjectFields( - client, - tokenBridgeStateObjectId - ); - if (!tokenBridgeStateFields) { - throw new Error("Unable to fetch object fields from token bridge state"); - } + const state = await client.getObject({ + objectId: tokenBridgeStateObjectId, + include: { json: true }, + }); - const coinTypes = tokenBridgeStateFields?.token_registry?.fields?.coin_types; - const coinTypesObjectId = coinTypes?.fields?.id?.id; + const fields = state.object.json as any; + const coinTypesObjectId = fields?.token_registry?.coin_types?.id; if (!coinTypesObjectId) { throw new Error("Unable to fetch coin types"); } - const keyType = getTableKeyType(coinTypes?.type); - if (!keyType) { - throw new Error("Unable to get key type"); - } + // The dynamic-field key type is declared in the *original* package that + // defines `token_registry`, which is the package embedded in the state + // object's type (Move type origins are stable across upgrades), not the + // upgraded package returned by `getPackageId`. + const originalPackageId = state.object.type.split("::")[0]; + const keyType = `${originalPackageId}::token_registry::CoinTypeKey`; + const keyBcs = CoinTypeKeyBcs.serialize({ + chain: tokenChain, + addr: Array.from(tokenAddress), + }).toBytes(); - const response = await client.getDynamicFieldObject({ - parentId: coinTypesObjectId, - name: { - type: keyType, - value: { - addr: [...tokenAddress], - chain: tokenChain, - }, - }, - }); - if (response.error) { - if (response.error.code === "dynamicFieldNotFound") { + try { + const res = await client.getDynamicField({ + parentId: coinTypesObjectId, + name: { type: keyType, bcs: keyBcs }, + }); + // The dynamic-field value is an `ascii::String` holding the coin type. + const { bcs } = require("@mysten/sui/bcs"); + const coinType = bcs.string().parse(res.dynamicField.value.bcs); + return coinType ? trimSuiType(ensureHexPrefix(coinType)) : null; + } catch (e) { + // The gRPC transport reports a missing dynamic field as a plain + // "Object 0x... not found" error, meaning the asset is not registered. + // Anything else (transport failure, malformed data) must propagate. + if (e instanceof Error && e.message.endsWith("not found")) { return null; } - throw new Error( - `Unexpected getDynamicFieldObject response ${response.error}` - ); + throw e; } - const fields = getFieldsFromObjectResponse(response); - return fields?.value ? trimSuiType(ensureHexPrefix(fields.value)) : null; }; +/** + * Fetch an object's Move struct fields as JSON. The gRPC `json` representation + * is flattened relative to the legacy JSON-RPC `content.fields` shape: there are + * no nested `.fields` wrappers and a UID `id.id` collapses to a plain `id` + * string. Returns `null` if the object has no Move struct content. + */ export const getObjectFields = async ( - client: SuiClient, + client: SuiGrpcClient, objectId: string ): Promise | null> => { if (!isValidSuiAddress(objectId)) { @@ -73,19 +183,10 @@ export const getObjectFields = async ( } const res = await client.getObject({ - id: objectId, - options: { - showContent: true, - }, + objectId, + include: { json: true }, }); - return getFieldsFromObjectResponse(res); -}; - -export const getFieldsFromObjectResponse = (object: SuiObjectResponse) => { - const content = object.data?.content; - return content && content.dataType === "moveObject" - ? (content.fields as any) - : null; + return (res.object.json as Record | null) ?? null; }; export function ensureHexPrefix(x: string): string { diff --git a/clients/js/tsconfig.build.json b/clients/js/tsconfig.build.json new file mode 100644 index 00000000000..006f49af7c6 --- /dev/null +++ b/clients/js/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "comment": "Used only by the esbuild bundler. The base tsconfig maps @mysten/sui subpaths to their .d.mts type declarations so `tsc` can resolve the ESM-only v2 package under classic node resolution; esbuild must NOT follow those mappings (it would try to bundle the declaration files), so paths are cleared here. esbuild resolves the package natively via its exports map.", + "compilerOptions": { + "paths": {} + } +} diff --git a/clients/js/tsconfig.json b/clients/js/tsconfig.json index ce7a76b2bb5..866a9952b25 100644 --- a/clients/js/tsconfig.json +++ b/clients/js/tsconfig.json @@ -8,7 +8,26 @@ "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "strict": true + "strict": true, + "paths": { + "@mysten/sui/grpc": ["./node_modules/@mysten/sui/dist/grpc/index.d.mts"], + "@mysten/sui/graphql": [ + "./node_modules/@mysten/sui/dist/graphql/index.d.mts" + ], + "@mysten/sui/transactions": [ + "./node_modules/@mysten/sui/dist/transactions/index.d.mts" + ], + "@mysten/sui/keypairs/ed25519": [ + "./node_modules/@mysten/sui/dist/keypairs/ed25519/index.d.mts" + ], + "@mysten/sui/utils": [ + "./node_modules/@mysten/sui/dist/utils/index.d.mts" + ], + "@mysten/sui/bcs": ["./node_modules/@mysten/sui/dist/bcs/index.d.mts"], + "@mysten/sui/client": [ + "./node_modules/@mysten/sui/dist/client/index.d.mts" + ] + } }, "include": ["src", "types"] }