diff --git a/.gitignore b/.gitignore index 3c65d89b36..58b3021b03 100644 --- a/.gitignore +++ b/.gitignore @@ -113,6 +113,9 @@ ts-sdk-evm/docs ts-sdk-cosmos/build ts-sdk-cosmos/dist ts-sdk-cosmos/docs +ts-sdk-sui/build +ts-sdk-sui/dist +ts-sdk-sui/docs sentinel2/build docs/src/content/docs/reference/@unionlabs/sdk docs/src/content/docs/reference/@unionlabs/sdk-evm diff --git a/app2/src/lib/stores/wallets.svelte.ts b/app2/src/lib/stores/wallets.svelte.ts index 01e2f3775f..d741b9289a 100644 --- a/app2/src/lib/stores/wallets.svelte.ts +++ b/app2/src/lib/stores/wallets.svelte.ts @@ -7,6 +7,7 @@ class WalletsStore { evmAddress: Option.Option = $state(Option.none()) cosmosAddress: Option.Option = $state(Option.none()) aptosAddress: Option.Option = $state(Option.none()) + suiAddress: Option.Option = $state(Option.none()) inputAddress: Option.Option = $state(Option.none()) hasAnyWallet() { @@ -14,6 +15,7 @@ class WalletsStore { Option.isSome(this.evmAddress) || Option.isSome(this.cosmosAddress) || Option.isSome(this.aptosAddress) + || Option.isSome(this.suiAddress) || Option.isSome(this.inputAddress) ) } @@ -34,6 +36,7 @@ class WalletsStore { this.evmAddress, this.cosmosAddress, this.aptosAddress, + this.suiAddress, ]), A.map(Ucs05.anyDisplayToCanonical), ) @@ -52,6 +55,7 @@ class WalletsStore { Option.map((address) => Ucs05.CosmosDisplay.make({ address })), )), Match.when("aptos", () => this.aptosAddress), + Match.when("sui", () => this.suiAddress), Match.exhaustive, ) } diff --git a/app2/src/lib/transfer/shared/components/Receiver.svelte b/app2/src/lib/transfer/shared/components/Receiver.svelte index a037af2d01..4052049b48 100644 --- a/app2/src/lib/transfer/shared/components/Receiver.svelte +++ b/app2/src/lib/transfer/shared/components/Receiver.svelte @@ -109,8 +109,12 @@ let manualAddress = $state("") let showClearConfirm = $state(false) let bookmarkOnAdd = $state(false) -let recentAddresses: Record> = $state({}) -let bookmarkedAddresses: Record> = $state({}) +let recentAddresses: Record> = + $state({}) +let bookmarkedAddresses: Record< + string, + Array<(string & {}) | `0x${string}` | `${string}1${string}`> +> = $state({}) // Create crossfade transition const [send, receive] = crossfade({ diff --git a/app2/src/lib/transfer/shared/services/filling/check-allowance.ts b/app2/src/lib/transfer/shared/services/filling/check-allowance.ts index e9cb57e0b2..1d5bd40a2b 100644 --- a/app2/src/lib/transfer/shared/services/filling/check-allowance.ts +++ b/app2/src/lib/transfer/shared/services/filling/check-allowance.ts @@ -52,6 +52,10 @@ export const checkAllowances = Effect.fn(( sender, chain, ), + SuiDisplay: (sender) => + Effect.fail( + new AllowanceCheckError({ message: "Sui allowance check not implemented" }), + ), }), Effect.map(A.map(({ token, allowance }) => [token, allowance] as const)), Effect.map(HashMap.fromIterable), diff --git a/flake.nix b/flake.nix index 49e4ab5ffa..73cb7b4b22 100644 --- a/flake.nix +++ b/flake.nix @@ -197,6 +197,7 @@ ./ts-sdk/ts-sdk.nix ./ts-sdk-evm/ts-sdk-evm.nix ./ts-sdk-cosmos/ts-sdk-cosmos.nix + ./ts-sdk-sui/ts-sdk-sui.nix ./typescript-sdk/typescript-sdk.nix ./cosmwasm/cosmwasm.nix ./evm/evm.nix diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f473bcd26c..10df19f8d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1122,6 +1122,68 @@ importers: version: 3.1.2(@types/debug@4.1.12)(@types/node@22.13.14)(happy-dom@17.4.4)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.0) publishDirectory: dist + ts-sdk-sui: + dependencies: + '@mysten/sui': + specifier: ^1.38.0 + version: 1.39.0(typescript@5.8.3) + '@scure/base': + specifier: 1.2.4 + version: 1.2.4 + crc: + specifier: ^4.3.2 + version: 4.3.2(buffer@6.0.3) + devDependencies: + '@babel/cli': + specifier: ^7.27.2 + version: 7.27.2(@babel/core@7.27.4) + '@babel/core': + specifier: ^7.27.1 + version: 7.27.4 + '@babel/plugin-transform-export-namespace-from': + specifier: ^7.27.1 + version: 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-modules-commonjs': + specifier: ^7.27.1 + version: 7.27.1(@babel/core@7.27.4) + '@cosmjs/math': + specifier: 0.33.1 + version: 0.33.1 + '@effect/build-utils': + specifier: ^0.8.3 + version: 0.8.3 + '@effect/platform': + specifier: 0.84.6 + version: 0.84.6(effect@3.16.3) + '@safe-global/safe-apps-sdk': + specifier: ~9.1.0 + version: 9.1.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.32) + '@types/node': + specifier: ^22.13.1 + version: 22.13.14 + '@unionlabs/sdk': + specifier: workspace:^ + version: link:../ts-sdk + babel-plugin-annotate-pure-calls: + specifier: ^0.5.0 + version: 0.5.0(@babel/core@7.27.4) + dpdm: + specifier: ^3.14.0 + version: 3.14.0 + effect: + specifier: 3.16.3 + version: 3.16.3 + madge: + specifier: ^8.0.0 + version: 8.0.0(typescript@5.8.3) + viem: + specifier: 2.33.3 + version: 2.33.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.32) + vitest: + specifier: ^3.0.5 + version: 3.1.2(@types/debug@4.1.12)(@types/node@22.13.14)(happy-dom@17.4.4)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.0) + publishDirectory: dist + typescript-sdk: dependencies: '@aptos-labs/ts-sdk': @@ -2514,6 +2576,20 @@ packages: '@gql.tada/vue-support': optional: true + '@gql.tada/cli-utils@1.7.1': + resolution: {integrity: sha512-wg5ysZNQxtNQm67T3laVWmZzLpGb7QfyYWZdaUD2r1OjDj5Bgftq7eQlplmH+hsdffjuUyhJw/b5XAjeE2mJtg==} + peerDependencies: + '@0no-co/graphqlsp': ^1.12.13 + '@gql.tada/svelte-support': 1.0.1 + '@gql.tada/vue-support': 1.0.1 + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 + typescript: ^5.0.0 + peerDependenciesMeta: + '@gql.tada/svelte-support': + optional: true + '@gql.tada/vue-support': + optional: true + '@gql.tada/internal@1.0.8': resolution: {integrity: sha512-XYdxJhtHC5WtZfdDqtKjcQ4d7R1s0d1rnlSs3OcBEUbYiPoJJfZU7tWsVXuv047Z6msvmr4ompJ7eLSK5Km57g==} peerDependencies: @@ -2982,13 +3058,23 @@ packages: '@mysten/bcs@1.6.1': resolution: {integrity: sha512-pywsl2+jxbib5CbteAjMpmJpnj1pcUco2ff+lXCK3hfppPbkyWEMbZDQn1jNngV6ADQ3IFIvPs0FaS7fKWPOLA==} + '@mysten/bcs@1.8.0': + resolution: {integrity: sha512-bDoLN1nN+XPONsvpNyNyqYHndM3PKWS419GLeRnbLoWyNm4bnyD1X4luEpJLLDq400hBuXiCan4RWjofvyTUIQ==} + '@mysten/sui@1.30.1': resolution: {integrity: sha512-WnyDpc5Fw6cvkJwXXmPWV80LA5YhvUgrKf86Pix0KLBSIc7aqwZi0GTPRWqiCedvayR6C3TkZqEhoDrVMusL7A==} engines: {node: '>=18'} + '@mysten/sui@1.39.0': + resolution: {integrity: sha512-tjH4oVAODO9JWPNvIBhAvorrwh7UfX5Lwf1oBjawnpk4sAIyajD8JYJUWXdI8o1H1519/5KEKaMT3ABAwTamQg==} + engines: {node: '>=18'} + '@mysten/utils@0.0.0': resolution: {integrity: sha512-KRI57Qow3E7TGqczimazwGf7+fwukdOi+6a31igSCzz0kPjAXbyK1a1gXaxeLMF8xEZ07ouW3RnsWt+EaUuHUw==} + '@mysten/utils@0.2.0': + resolution: {integrity: sha512-CM6kJcJHX365cK6aXfFRLBiuyXc5WSBHQ43t94jqlCAIRw8umgNcTb5EnEA9n31wPAQgLDGgbG/rCUISCTJ66w==} + '@n1ru4l/push-pull-async-iterable-iterator@3.2.0': resolution: {integrity: sha512-3fkKj25kEjsfObL6IlKPAlHYPq/oYwUkkQ03zsTTiDjD7vg/RxjdiLeCydqtxHZP0JgsXL3D/X5oAkMGzuUp/Q==} engines: {node: '>=12'} @@ -3090,6 +3176,10 @@ packages: resolution: {integrity: sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==} engines: {node: ^14.21.3 || >=16} + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + '@noble/hashes@1.4.0': resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} @@ -7038,6 +7128,12 @@ packages: peerDependencies: typescript: ^5.0.0 + gql.tada@1.8.13: + resolution: {integrity: sha512-fYoorairdPgxtE7Sf1X9/6bSN9Kt2+PN8KLg3hcF8972qFnawwUgs1OLVU8efZMHwL7EBHhhKBhrsGPlOs2lZQ==} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -13290,6 +13386,13 @@ snapshots: graphql: 16.11.0 typescript: 5.8.3 + '@gql.tada/cli-utils@1.7.1(@0no-co/graphqlsp@1.12.16(graphql@16.11.0)(typescript@5.8.3))(graphql@16.11.0)(typescript@5.8.3)': + dependencies: + '@0no-co/graphqlsp': 1.12.16(graphql@16.11.0)(typescript@5.8.3) + '@gql.tada/internal': 1.0.8(graphql@16.11.0)(typescript@5.8.3) + graphql: 16.11.0 + typescript: 5.8.3 + '@gql.tada/internal@1.0.8(graphql@16.10.0)(typescript@5.8.3)': dependencies: '@0no-co/graphql.web': 1.1.2(graphql@16.10.0) @@ -13958,6 +14061,11 @@ snapshots: '@mysten/utils': 0.0.0 '@scure/base': 1.2.4 + '@mysten/bcs@1.8.0': + dependencies: + '@mysten/utils': 0.2.0 + '@scure/base': 1.2.6 + '@mysten/sui@1.30.1(typescript@5.8.3)': dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.11.0) @@ -13977,10 +14085,33 @@ snapshots: - '@gql.tada/vue-support' - typescript + '@mysten/sui@1.39.0(typescript@5.8.3)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.11.0) + '@mysten/bcs': 1.8.0 + '@mysten/utils': 0.2.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + gql.tada: 1.8.13(graphql@16.11.0)(typescript@5.8.3) + graphql: 16.11.0 + poseidon-lite: 0.2.1 + valibot: 0.36.0 + transitivePeerDependencies: + - '@gql.tada/svelte-support' + - '@gql.tada/vue-support' + - typescript + '@mysten/utils@0.0.0': dependencies: '@scure/base': 1.2.4 + '@mysten/utils@0.2.0': + dependencies: + '@scure/base': 1.2.6 + '@n1ru4l/push-pull-async-iterable-iterator@3.2.0': {} '@napi-rs/canvas-android-arm64@0.1.79': @@ -14061,6 +14192,10 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + '@noble/hashes@1.4.0': {} '@noble/hashes@1.6.0': {} @@ -20023,6 +20158,18 @@ snapshots: - '@gql.tada/vue-support' - graphql + gql.tada@1.8.13(graphql@16.11.0)(typescript@5.8.3): + dependencies: + '@0no-co/graphql.web': 1.1.2(graphql@16.11.0) + '@0no-co/graphqlsp': 1.12.16(graphql@16.11.0)(typescript@5.8.3) + '@gql.tada/cli-utils': 1.7.1(@0no-co/graphqlsp@1.12.16(graphql@16.11.0)(typescript@5.8.3))(graphql@16.11.0)(typescript@5.8.3) + '@gql.tada/internal': 1.0.8(graphql@16.11.0)(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - '@gql.tada/svelte-support' + - '@gql.tada/vue-support' + - graphql + graceful-fs@4.2.11: {} graphemer@1.4.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6f3c17c06e..436cc84279 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,3 +9,4 @@ packages: - 'ts-sdk-evm' - 'typescript-sdk' - 'zkgm-dev' + - 'ts-sdk-sui' diff --git a/scripts/docs.mjs b/scripts/docs.mjs index ec1401e96e..c5daae28a6 100644 --- a/scripts/docs.mjs +++ b/scripts/docs.mjs @@ -6,6 +6,7 @@ function packages() { "ts-sdk", "ts-sdk-evm", "ts-sdk-cosmos", + "ts-sdk-sui", ].filter((_) => Fs.existsSync(Path.join(_, "docs/modules"))) } diff --git a/ts-sdk-sui/.gitignore b/ts-sdk-sui/.gitignore new file mode 100644 index 0000000000..06c5e16854 --- /dev/null +++ b/ts-sdk-sui/.gitignore @@ -0,0 +1 @@ +dist/** diff --git a/ts-sdk-sui/LICENSE b/ts-sdk-sui/LICENSE new file mode 100644 index 0000000000..9453b523bd --- /dev/null +++ b/ts-sdk-sui/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Union.fi Labs, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ts-sdk-sui/README.md b/ts-sdk-sui/README.md new file mode 100644 index 0000000000..a5b9b42afe --- /dev/null +++ b/ts-sdk-sui/README.md @@ -0,0 +1,3 @@ +# Union TypeScript SDK for SUI + +`@unionlabs/sdk-sui` diff --git a/ts-sdk-sui/docgen.json b/ts-sdk-sui/docgen.json new file mode 100644 index 0000000000..e9f6ec68ed --- /dev/null +++ b/ts-sdk-sui/docgen.json @@ -0,0 +1,5 @@ +{ + "$schema": "../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/unionlabs/union/tree/main/ts-sdk-sui/src/", + "exclude": ["src/internal/**/*.ts"] +} diff --git a/ts-sdk-sui/examples/sui-create-client-generate-instruction.ts b/ts-sdk-sui/examples/sui-create-client-generate-instruction.ts new file mode 100644 index 0000000000..c33b4ba56c --- /dev/null +++ b/ts-sdk-sui/examples/sui-create-client-generate-instruction.ts @@ -0,0 +1,110 @@ +// @ts-ignore +if (typeof BigInt.prototype.toJSON !== "function") { + // @ts-ignore + BigInt.prototype.toJSON = function() { + return this.toString() + } +} +import { getFullnodeUrl } from "@mysten/sui/client" +import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519" +import { ChannelId } from "@unionlabs/sdk/schema/channel" +import * as ZkgmClient from "@unionlabs/sdk/ZkgmClient" +import * as ZkgmClientRequest from "@unionlabs/sdk/ZkgmClientRequest" +import * as ZkgmClientResponse from "@unionlabs/sdk/ZkgmClientResponse" +import { Effect, Logger } from "effect" +import { PublicClient, WalletClient } from "../src/Sui.js" +import { layerWithoutWallet } from "../src/SuiZkgmClient.js" + +import { ChainRegistry } from "@unionlabs/sdk/ChainRegistry" +import { UniversalChainId } from "@unionlabs/sdk/schema/chain" +import * as TokenOrder from "@unionlabs/sdk/TokenOrder" + +const MNEMONIC = process.env.SUI_MNEMONIC ?? "..." +const RECIPIENT = process.env.RECIPIENT + ?? "union1wycy8g8v5sff6gsjl9yhjs43q98xpl05p3gn2s" + +const keypair = Ed25519Keypair.deriveKeypair(MNEMONIC) + +const program = Effect.gen(function*() { + // TODO: Source will be SUI testnet + const source = yield* ChainRegistry.byUniversalId( + UniversalChainId.make("ethereum.17000"), + ) + + console.log("source", source) + + const destination = yield* ChainRegistry.byUniversalId( + UniversalChainId.make("union.union-1"), + ) + const wallet = yield* WalletClient + + const sender = wallet.signer.toSuiAddress() + + console.log("sender:", sender) + + const tokenOrder = yield* TokenOrder.make({ + source, + destination, + sender: sender, + receiver: RECIPIENT, + baseToken: "0x2::sui::SUI", + baseAmount: 10000000n, + quoteToken: "union1y05e0p2jcvhjzf7kcqsrqx93d4g3u93hc2hykaq8hrvkqrp5ltrssagzyd", + quoteAmount: 10000000n, + kind: "solve", + metadata: + "0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040756e696f6e31793035653070326a6376686a7a66376b63717372717839336434673375393368633268796b6171386872766b717270356c7472737361677a7964", + version: 2, + }) + + const request = ZkgmClientRequest.make({ + source, + destination, + channelId: ChannelId.make(5), + ucs03Address: "0x8675045186976da5b60baf20dc94413fb5415a7054052dc14d93c13d3dbdf830", + instruction: tokenOrder, + + // NEW — only read by the Sui client + transport: { + sui: { + relayStoreId: "0x393a99c6d55d9a79efa52dea6ea253fef25d2526787127290b985222cc20a924", + vaultId: "0x7c4ade19208295ed6bf3c4b58487aa4b917ba87d31460e9e7a917f7f12207ca3", + ibcStoreId: "0xac7814eebdfbf975235bbb796e07533718a9d83201346769e5f281dc90009175", + coins: [ + { + typeArg: "0x2::sui::SUI", + objectId: "0x266d00c4b329111255339c041cc57a1b616cfeddafdae47df8f814002578e95b", + }, + ], + }, + }, + }) + + yield* Effect.log("ZKGM Client Request", request) + + const zkgmClient = yield* ZkgmClient.ZkgmClient + + const response: ZkgmClientResponse.ZkgmClientResponse = yield* zkgmClient.execute(request) + + yield* Effect.log("Submission Hash", response.txHash) +}).pipe( + Effect.provide(layerWithoutWallet), + Effect.provide(PublicClient.Live({ url: getFullnodeUrl("testnet") })), + Effect.provide( + WalletClient.Live({ + url: getFullnodeUrl("testnet"), + signer: keypair, + }), + ), + Effect.provide(ChainRegistry.Default), + Effect.provide(Logger.replace(Logger.defaultLogger, Logger.prettyLoggerDefault)), +) + +Effect.runPromise(program).catch((e: any) => { + console.error("\n--- TOP-LEVEL ERROR ---") + console.dir(e, { depth: 10 }) + if (e?.cause) { + console.error("\n--- ORIGINAL CAUSE ---") + console.dir(e.cause, { depth: 10 }) + } +}) diff --git a/ts-sdk-sui/examples/sui-create-client-read-coin-related.ts b/ts-sdk-sui/examples/sui-create-client-read-coin-related.ts new file mode 100644 index 0000000000..266648b518 --- /dev/null +++ b/ts-sdk-sui/examples/sui-create-client-read-coin-related.ts @@ -0,0 +1,57 @@ +// @ts-ignore +if (typeof BigInt.prototype.toJSON !== "function") { + // @ts-ignore + BigInt.prototype.toJSON = function() { + return this.toString() + } +} + +import { getFullnodeUrl } from "@mysten/sui/client" +import { Effect, Logger } from "effect" + +import { + getAllCoinsUnique, + getCoinDecimals, + getCoinName, + PublicClient, + readCoinBalances, + readCoinMetadata, + readCoinSymbol, + readTotalCoinBalance, +} from "../src/Sui.js" + +const ADDRESS = process.env.ADDRESS + ?? "0x03ff9dd9e093387bdd4432c6a3eb6a1bd5a8f39a530042ac7efe576f18d3232b" + +const COIN_TYPE = "0x2::sui::SUI" as any + +const program = Effect.gen(function*() { + const { client } = yield* PublicClient + yield* Effect.log("Sui public client initialized", client.network) + + const meta = yield* readCoinMetadata(COIN_TYPE) + yield* Effect.log("SUI metadata", meta) + + const [name, symbol, decimals] = yield* Effect.all([ + getCoinName(COIN_TYPE), + readCoinSymbol(COIN_TYPE), + getCoinDecimals(COIN_TYPE), + ]) + yield* Effect.log("SUI meta (granular)", { name, symbol, decimals }) + + yield* Effect.log("Address", ADDRESS) + const coins = yield* readCoinBalances(COIN_TYPE, ADDRESS as any) + yield* Effect.log("SUI coins (objects)", coins) + + const total = yield* readTotalCoinBalance(COIN_TYPE, ADDRESS as any) + yield* Effect.log("SUI total balance (mist as BigInt)", total.toString()) + + const unique = yield* getAllCoinsUnique(ADDRESS as any) + + yield* Effect.log("All coins (unique, summed)", unique) +}).pipe( + Effect.provide(PublicClient.Live({ url: getFullnodeUrl("testnet") })), + Effect.provide(Logger.replace(Logger.defaultLogger, Logger.prettyLoggerDefault)), +) + +Effect.runPromise(program).catch(console.error) diff --git a/ts-sdk-sui/examples/sui-create-client-write-contract.ts b/ts-sdk-sui/examples/sui-create-client-write-contract.ts new file mode 100644 index 0000000000..f71d83dc23 --- /dev/null +++ b/ts-sdk-sui/examples/sui-create-client-write-contract.ts @@ -0,0 +1,68 @@ +// @ts-ignore +if (typeof BigInt.prototype.toJSON !== "function") { + // @ts-ignore + BigInt.prototype.toJSON = function() { + return this.toString() + } +} +import { getFullnodeUrl } from "@mysten/sui/client" +import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519" +import { Transaction } from "@mysten/sui/transactions" +import { Effect, Logger } from "effect" +import { + PublicClient, + readCoinBalances, + readCoinMetadata, + WalletClient, + writeContract, +} from "../src/Sui.js" + +const MNEMONIC = process.env.SUI_MNEMONIC ?? "..." +const RECIPIENT = process.env.RECIPIENT + ?? "0x03ff9dd9e093387bdd4432c6a3eb6a1bd5a8f39a530042ac7efe576f18d3232b" + +const keypair = Ed25519Keypair.deriveKeypair(MNEMONIC) + +const program = Effect.gen(function*() { + const { client } = yield* PublicClient + yield* Effect.log("Sui public client initialized", client.network) + const meta = yield* readCoinMetadata("0x2::sui::SUI" as any) + yield* Effect.log("SUI metadata", meta) + + yield* Effect.log("keypair.getPublicKey().toSuiAddress()", keypair.getPublicKey().toSuiAddress()) + const balances = yield* readCoinBalances( + "0x2::sui::SUI" as any, + keypair.getPublicKey().toSuiAddress() as any, + ) + yield* Effect.log("SUI balances", balances) + + const amountMist = 10_000_000n // 0.01 SUI + + const tx = new Transaction() + const coin = tx.splitCoins(tx.gas, [tx.pure.u64(amountMist)]) + const recipient = tx.pure.address(RECIPIENT) + + const res = yield* writeContract( + client, + keypair, + "0x2", // packageId: Sui framework + "transfer", // module: sui::transfer + "public_transfer", // function + ["0x2::coin::Coin<0x2::sui::SUI>"], // type arg T + [coin, recipient], // (obj: T, recipient: address) + tx, + ) + + yield* Effect.log("Transfer submitted", res) +}).pipe( + Effect.provide(PublicClient.Live({ url: getFullnodeUrl("testnet") })), + Effect.provide( + WalletClient.Live({ + url: getFullnodeUrl("testnet"), + signer: keypair, // ✅ Sui signer + }), + ), + Effect.provide(Logger.replace(Logger.defaultLogger, Logger.prettyLoggerDefault)), +) + +Effect.runPromise(program).catch(console.error) diff --git a/ts-sdk-sui/package.json b/ts-sdk-sui/package.json new file mode 100644 index 0000000000..5f469dc246 --- /dev/null +++ b/ts-sdk-sui/package.json @@ -0,0 +1,74 @@ +{ + "name": "@unionlabs/sdk-sui", + "version": "0.0.0", + "type": "module", + "license": "MIT", + "author": "@unionlabs", + "homepage": "https://docs.union.build/typescript", + "description": "Union TypeScript SDK for Sui", + "repository": { + "type": "git", + "url": "https://github.com/unionlabs/union.git", + "directory": "ts-sdk-sui" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "directory": "dist", + "linkDirectory": false + }, + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./examples/*": "./examples/*.ts", + "./internal/*": null + }, + "scripts": { + "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v3", + "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", + "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build-esm": "tsc -b tsconfig.build.json", + "check": "tsc -b tsconfig.json", + "check:circular": "dpdm -T src", + "check:examples": "tsc -p tsconfig.examples.json", + "codegen": "build-utils prepare-v3", + "test": "vitest run", + "test:watch": "vitest" + }, + "peerDependencies": { + "@effect/platform": "^0.84", + "@unionlabs/sdk": "workspace:^", + "@safe-global/safe-apps-sdk": "^9", + "effect": "^3.16", + "viem": "^2" + }, + "peerDependenciesMeta": { + "@safe-global/safe-apps-sdk": { + "optional": true + } + }, + "devDependencies": { + "@babel/cli": "^7.27.2", + "@babel/core": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@cosmjs/math": "^0.33.1", + "@effect/build-utils": "^0.8.3", + "@effect/platform": "0.84.6", + "@safe-global/safe-apps-sdk": "~9.1.0", + "@types/node": "^22.13.1", + "@unionlabs/sdk": "workspace:^", + "babel-plugin-annotate-pure-calls": "^0.5.0", + "dpdm": "^3.14.0", + "effect": "3.16.3", + "madge": "^8.0.0", + "viem": "^2.33.3", + "vitest": "^3.0.5" + }, + "dependencies": { + "@scure/base": "1.2.4", + "crc": "^4.3.2", + "@mysten/sui": "^1.38.0" + } +} diff --git a/ts-sdk-sui/src/Sui.ts b/ts-sdk-sui/src/Sui.ts new file mode 100644 index 0000000000..c2e4bdde89 --- /dev/null +++ b/ts-sdk-sui/src/Sui.ts @@ -0,0 +1,565 @@ +/** + * This module handles Sui related functionality. + * + * @since 0.0.0 + */ +import { SuiClient } from "@mysten/sui/client" +import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519" +import { Transaction } from "@mysten/sui/transactions" +import { extractErrorDetails } from "@unionlabs/sdk/Utils" +import { Context, Data, Effect, flow, Layer } from "effect" +import { type Address } from "viem" +import * as internal from "./internal/sui.js" + +/** + * @category models + * @since 0.0.0 + */ +export namespace Sui { + /** + * @category models + * @since 0.0.0 + */ + export interface PublicClient { + readonly client: SuiClient + } + + /** + * @category models + * @since 0.0.0 + */ + export interface WalletClient { + readonly client: SuiClient + readonly signer: Ed25519Keypair + } + + /** + * @category models + * @since 0.0.0 + */ + export interface Channel { + readonly ucs03address: Address + readonly channelId: number + } +} + +// /** +// * @category utils +// * @since 0.0.0 +// */ +// export const channelBalance = (path: bigint, token: Hex) => +// Effect.gen(function*() { +// const client = (yield* PublicClientDestination).client +// const config = yield* ChannelDestination + +// const result = yield* readContract(client, { +// address: config.ucs03address, +// abi: Ucs03.Abi, +// functionName: "_deprecated_channelBalanceV1", +// args: [config.channelId, path, token], +// }) + +// return result +// }) + +// /** +// * @category utils +// * @since 0.0.0 +// */ +// export const channelBalanceAtBlock = (path: bigint, token: Hex, blockNumber: bigint) => +// Effect.gen(function*() { +// const client = (yield* PublicClientDestination).client +// const config = yield* ChannelDestination + +// const result = yield* readContract(client, { +// address: config.ucs03address, +// abi: Ucs03.Abi, +// functionName: "_deprecated_channelBalanceV1", +// args: [config.channelId, path, token], +// blockNumber: blockNumber, +// }) + +// return result +// }) + +export class ReadCoinError extends Data.TaggedError("ReadCoinError")<{ + cause: unknown +}> {} + +export const readContract = ( + client: SuiClient, + sender: string, + packageId: string, + module: string, + fn: string, + typeArgs: string[], + args: any[], + tx: Transaction, +) => + Effect.tryPromise({ + try: async () => { + tx.moveCall({ + target: `${packageId}::${module}::${fn}`, + typeArguments: typeArgs, + arguments: args, + }) + const result = await client.devInspectTransactionBlock({ + transactionBlock: tx, + sender, + }) + return result.results // result as unknown as T + }, + catch: e => new ReadContractError({ cause: extractErrorDetails(e as Error) }), + }).pipe( + // optional: e.g. timeout & retry like your Aptos wrapper + Effect.timeout("10 seconds"), + Effect.retry({ times: 5 }), + ) + +export const writeContract = ( + client: SuiClient, + signer: Ed25519Keypair, + packageId: string, + module: string, + fn: string, + typeArgs: string[], + args: any[], + tx: Transaction, +) => + Effect.tryPromise({ + try: async () => { + tx.moveCall({ + target: `${packageId}::${module}::${fn}`, + typeArguments: typeArgs, + arguments: args, + }) + // sign & execute + const res = await client.signAndExecuteTransaction({ + signer, + transaction: tx, + }) + return res + }, + catch: e => new WriteContractError({ cause: extractErrorDetails(e as Error) }), + }) + +/** + * @category context + * @since 0.0.0 + */ +export class ChannelDestination extends Context.Tag("@unionlabs/sdk/Sui/ChannelDestination")< + ChannelDestination, + Sui.Channel +>() { + static Live = flow( + ChannelDestination.of, + Layer.succeed(this), + ) +} + +/** + * @category context + * @since 0.0.0 + */ +export class ChannelSource extends Context.Tag("@unionlabs/sdk/Sui/ChannelSource")< + ChannelSource, + Sui.Channel +>() { + static Live = flow( + ChannelSource.of, + Layer.succeed(this), + ) +} + +/** + * @category context + * @since 0.0.0 + */ + +export class PublicClientSource extends Context.Tag("@unionlabs/sdk/Sui/PublicClientSource")< + PublicClientSource, + Sui.PublicClient +>() { + static Live = internal.publicClientLayer(this) +} + +/** + * @category context + * @since 0.0.0 + */ +export class PublicClientDestination + extends Context.Tag("@unionlabs/sdk/Sui/PublicClientDestination")< + PublicClientDestination, + Sui.PublicClient + >() +{ + static Live = internal.publicClientLayer(this) +} + +/** + * A neutral public client that can be used for general-purpose operations + * that don't specifically target source or destination chains + * + * @category context + * @since 0.0.0 + */ +export class PublicClient extends Context.Tag("@unionlabs/sdk-sui/Sui/PublicClient")< + PublicClient, + Sui.PublicClient +>() { + static Live = internal.publicClientLayer(this) +} + +/** + * A wallet client that can be used for signing transactions + * + * @category context + * @since 0.0.0 + */ +export class WalletClient extends Context.Tag("@unionlabs/sdk/Sui/WalletClient")< + WalletClient, + Sui.WalletClient +>() { + static Live = internal.walletClientLayer(this) +} + +/** + * @category errors + * @since 0.0.0 + */ +export class ReadContractError extends Data.TaggedError("@unionlabs/sdk/Sui/ReadContractError")<{ + cause: unknown +}> {} + +/** + * @category errors + * @since 0.0.0 + */ +export class WriteContractError extends Data.TaggedError("@unionlabs/sdk/Sui/WriteContractError")<{ + cause: unknown +}> {} + +/** + * @category errors + * @since 0.0.0 + */ +export class CreatePublicClientError + extends Data.TaggedError("@unionlabs/sdk/Sui/CreatePublicClientError")<{ + cause: unknown + }> +{} + +/** + * @category errors + * @since 0.0.0 + */ +export class CreateWalletClientError + extends Data.TaggedError("@unionlabs/sdk/Sui/CreateWalletClientError")<{ + cause: unknown + }> +{} + +/** + * Read Coin metadata (name, symbol, decimals, …) for a given `coinType`. + * + * Example: + * ```ts + * const meta = yield* readCoinMetadata("0x2::sui::SUI") + * ``` + * + * @param tokenAddress Canonical coin type string (e.g., `"0x2::sui::SUI"`) + * @returns Effect resolving to `getCoinMetadata` result + * @throws ReadCoinError on RPC failure + * + * @category utils + * @since 0.0.0 + */ +export const readCoinMetadata = (tokenAddress: Address) => + Effect.gen(function*() { + const client = (yield* PublicClient).client + + const metadata = yield* Effect.tryPromise({ + try: async () => { + const result = await client.getCoinMetadata({ coinType: tokenAddress }) + return result + }, + catch: cause => new ReadCoinError({ cause }), + }) + return metadata + }) + +/** + * Read Sui coin metadata (name, symbol, decimals) for a given `coinType`. + * + * Example: + * ```ts + * const meta = yield* readCoinMeta("0x2::sui::SUI") + * // -> { name: "Sui", symbol: "SUI", decimals: 9 } + * ``` + * + * @param coinType Canonical coin type string (e.g., "0x2::sui::SUI") + * @returns Effect resolving to `{ name, symbol, decimals }` + * @throws ReadCoinError on RPC failure + * + * @category utils + * @since 0.0.0 + */ +export const readCoinMeta = (coinType: string) => + Effect.gen(function*() { + const client = (yield* PublicClient).client + + const out = yield* Effect.tryPromise({ + try: async () => { + const meta = await client.getCoinMetadata({ coinType }) + // meta can be null if the type has no metadata published + if (!meta) { + // normalize to a typed error consistent with your pattern + throw new ReadCoinError({ cause: `No CoinMetadata found for ${coinType}` }) + } + const { name, symbol, decimals } = meta + return { name, symbol, decimals } + }, + catch: err => + new ReadCoinError({ + cause: extractErrorDetails(err as Error), + }), + }) + + return out + }) + +/** + * Read all coin objects for a given `coinType` and owner address. + * + * Note: + * - Sui splits balances across multiple coin objects; each carries `balance` and `coinObjectId`. + * - Use {@link readTotalCoinBalance} if you want a single summed value. + * + * @param contractAddress Canonical coin type (e.g., `"0x2::sui::SUI"`) + * @param address Owner Sui address + * @returns Effect resolving to the paged coin objects’ `data` array + * + * @category utils + * @since 0.0.0 + */ +export const readCoinBalances = (contractAddress: string, address: string) => + Effect.gen(function*() { + const client = (yield* PublicClient).client + let params = { + owner: address, + coinType: contractAddress, + } + + const coins = yield* Effect.tryPromise({ + try: async () => { + const result = await client.getCoins(params) + return result.data + }, + catch: err => + new ReadCoinError({ + cause: extractErrorDetails(err as ReadCoinError), + }), + }) + return coins + }) + +/** + * Read and sum all coin object balances for a given `coinType` and owner. + * + * @param contractAddress Canonical coin type + * @param address Owner Sui address + * @returns Effect resolving to a `bigint` total (in the coin’s base units) + * + * @category utils + * @since 0.0.0 + */ +export const readTotalCoinBalance = (contractAddress: string, address: string) => + Effect.gen(function*() { + const client = (yield* PublicClient).client + let params = { + owner: address, + coinType: contractAddress, + } + + const coins = yield* Effect.tryPromise({ + try: async () => { + const result = await client.getCoins(params) + return result.data + }, + catch: err => + new ReadCoinError({ + cause: extractErrorDetails(err as ReadCoinError), + }), + }) + // Calculate total balance + const totalBalance = coins.reduce((acc, coin) => acc + BigInt(coin.balance), BigInt(0)) + + return totalBalance + }) + +/** + * Fetch *all* coin objects (any coin type) for an owner. + * + * @param address Owner Sui address + * @returns Effect resolving to `getAllCoins().data` + * + * @category utils + * @since 0.0.0 + */ +export const getAllCoins = (address: string) => + Effect.gen(function*() { + const client = (yield* PublicClient).client + let params = { + owner: address, + } + + const coins = yield* Effect.tryPromise({ + try: async () => { + const result = await client.getAllCoins(params) + return result.data + }, + catch: err => + new ReadCoinError({ + cause: extractErrorDetails(err as ReadCoinError), + }), + }) + return coins + }) + +/** + * Fetch all coins for an owner and return a unique list grouped by `coinType`, + * with balances summed across coin objects. + * + * Example output: + * ```ts + * [ + * { coinType: "0x2::sui::SUI", balance: "123456789" }, + * { coinType: "0x...::USDC::USDC", balance: "4200000" } + * ] + * ``` + * + * @param address Owner Sui address + * @returns Effect resolving to `{ coinType, balance }[]` (balance as string) + * + * @category utils + * @since 0.0.0 + */ +export const getAllCoinsUnique = (address: string) => + Effect.gen(function*() { + const client = (yield* PublicClient).client + + const params = { + owner: address, + } + + const coins = yield* Effect.tryPromise({ + try: async () => { + const result = await client.getAllCoins(params) + return result.data + }, + catch: err => + new ReadCoinError({ + cause: extractErrorDetails(err as ReadCoinError), + }), + }) + + // Group by coinType and sum balances + const coinMap: Record = {} + + for (const coin of coins) { + const coinType = coin.coinType + const balance = BigInt(coin.balance) + + if (!coinMap[coinType]) { + coinMap[coinType] = balance + } else { + coinMap[coinType] += balance + } + } + + // Convert to array of objects + const result = Object.entries(coinMap).map(([coinType, totalBalance]) => ({ + coinType, + balance: totalBalance.toString(), // or keep as BigInt if preferred + })) + + return result + }) + +/** + * Convenience: read coin **name** for a given `coinType`. + * + * @param address Canonical coin type + * @returns Effect resolving to `string | undefined` + * + * @category utils + * @since 0.0.0 + */ +export const getCoinName = (address: string) => + Effect.gen(function*() { + const client = (yield* PublicClient).client + + const name = yield* Effect.tryPromise({ + try: async () => { + const result = await client.getCoinMetadata({ coinType: address }) + return result?.name + }, + catch: err => + new ReadCoinError({ + cause: extractErrorDetails(err as ReadCoinError), + }), + }) + return name + }) + +/** + * Convenience: read coin **decimals** for a given `coinType`. + * + * @param address Canonical coin type + * @returns Effect resolving to `number | undefined` + * + * @category utils + * @since 0.0.0 + */ +export const getCoinDecimals = (address: string) => + Effect.gen(function*() { + const client = (yield* PublicClient).client + + const decimals = yield* Effect.tryPromise({ + try: async () => { + const result = await client.getCoinMetadata({ coinType: address }) + return result?.decimals + }, + catch: err => + new ReadCoinError({ + cause: extractErrorDetails(err as ReadCoinError), + }), + }) + return decimals + }) + +/** + * Convenience: read coin **symbol** for a given `coinType`. + * + * @param address Canonical coin type + * @returns Effect resolving to `string | undefined` + * + * @category utils + * @since 0.0.0 + */ +export const readCoinSymbol = (address: string) => + Effect.gen(function*() { + const client = (yield* PublicClient).client + + const symbol = yield* Effect.tryPromise({ + try: async () => { + const result = await client.getCoinMetadata({ coinType: address }) + return result?.symbol + }, + catch: err => + new ReadCoinError({ + cause: extractErrorDetails(err as ReadCoinError), + }), + }) + return symbol + }) diff --git a/ts-sdk-sui/src/SuiZkgmClient.ts b/ts-sdk-sui/src/SuiZkgmClient.ts new file mode 100644 index 0000000000..fe050f2c7b --- /dev/null +++ b/ts-sdk-sui/src/SuiZkgmClient.ts @@ -0,0 +1,19 @@ +/** + * This module defines a concrete {@link ZkgmClient} for Sui source chain usage. + * + * @since 0.0.0 + */ +import type * as ZkgmClient from "@unionlabs/sdk/ZkgmClient" +import type * as Layer from "effect/Layer" +import * as internal from "./internal/zkgmClient.js" +import type * as Sui from "./Sui.js" + +/** + * @category layers + * @since 0.0.0 + */ +export const layerWithoutWallet: Layer.Layer< + ZkgmClient.ZkgmClient, + never, + Sui.WalletClient | Sui.PublicClient +> = internal.layerWithoutWallet diff --git a/ts-sdk-sui/src/index.ts b/ts-sdk-sui/src/index.ts new file mode 100644 index 0000000000..6450939d4c --- /dev/null +++ b/ts-sdk-sui/src/index.ts @@ -0,0 +1,13 @@ +/** + * This module handles SUI related functionality. + * + * @since 0.0.0 + */ +export * as Sui from "./Sui.js" + +/** + * This module defines a concrete {@link ZkgmClient} for Sui source chain usage. + * + * @since 0.0.0 + */ +export * as SuiZkgmClient from "./SuiZkgmClient.js" diff --git a/ts-sdk-sui/src/internal/sui.ts b/ts-sdk-sui/src/internal/sui.ts new file mode 100644 index 0000000000..863e1d33a5 --- /dev/null +++ b/ts-sdk-sui/src/internal/sui.ts @@ -0,0 +1,48 @@ +import { SuiClient, SuiClientOptions } from "@mysten/sui/client" +import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519" +import * as Utils from "@unionlabs/sdk/Utils" +import { Context, Effect, Layer, pipe } from "effect" +import * as Sui from "../Sui.js" + +/** @internal */ +export const publicClientLayer = < + Id, +>(tag: Context.Tag) => +( + ...options: Parameters +): Layer.Layer => + Layer.effect( + tag, + pipe( + Effect.try({ + try: () => new SuiClient(options[0] as SuiClientOptions), + catch: (err) => + new Sui.CreatePublicClientError({ + cause: Utils.extractErrorDetails(err as Sui.CreatePublicClientError), + }), + }), + Effect.map((client) => ({ client })), + ), + ) + +/** @internal */ +export const walletClientLayer = ( + tag: Context.Tag, +) => +(opts: { url: string; signer: Ed25519Keypair }): Layer.Layer => + Layer.effect( + tag, + Effect.try({ + try: () => { + if (!opts?.signer || typeof opts.signer.getPublicKey !== "function") { + throw new Error("Invalid Sui signer: expected Ed25519Keypair") + } + const client = new SuiClient({ url: opts.url } satisfies SuiClientOptions) + return { client, signer: opts.signer } + }, + catch: (err) => + new Sui.CreateWalletClientError({ + cause: Utils.extractErrorDetails(err as Error), + }), + }), + ) diff --git a/ts-sdk-sui/src/internal/zkgmClient.ts b/ts-sdk-sui/src/internal/zkgmClient.ts new file mode 100644 index 0000000000..6f0526ccab --- /dev/null +++ b/ts-sdk-sui/src/internal/zkgmClient.ts @@ -0,0 +1,285 @@ +import { Transaction } from "@mysten/sui/transactions" +import * as Call from "@unionlabs/sdk/Call" +import type { Hex } from "@unionlabs/sdk/schema/hex" +import * as TokenOrder from "@unionlabs/sdk/TokenOrder" +import * as Ucs03 from "@unionlabs/sdk/Ucs03" +import * as Utils from "@unionlabs/sdk/Utils" +import * as Client from "@unionlabs/sdk/ZkgmClient" +import * as ClientError from "@unionlabs/sdk/ZkgmClientError" +import * as ClientRequest from "@unionlabs/sdk/ZkgmClientRequest" +import * as ClientResponse from "@unionlabs/sdk/ZkgmClientResponse" +import * as IncomingMessage from "@unionlabs/sdk/ZkgmIncomingMessage" +import * as ZkgmInstruction from "@unionlabs/sdk/ZkgmInstruction" +import { Match, ParseResult, pipe, Predicate } from "effect" +import * as A from "effect/Array" +import * as Effect from "effect/Effect" +import * as Inspectable from "effect/Inspectable" +import * as Option from "effect/Option" +import * as S from "effect/Schema" +import * as Stream from "effect/Stream" +import * as Sui from "../Sui.js" + +export const fromWallet = ( + opts: { client: Sui.Sui.PublicClient; wallet: Sui.Sui.WalletClient }, +): Client.ZkgmClient => + Client.make((request, signal, fiber) => + Effect.gen(function*() { + const { + wallet, + client, + } = opts + + const encodeInstruction: ( + u: ZkgmInstruction.ZkgmInstruction, + ) => Effect.Effect< + Ucs03.Ucs03, + ParseResult.ParseError | Sui.ReadContractError | Sui.ReadCoinError + > = pipe( + Match.type(), + Match.tagsExhaustive({ + Batch: (batch) => + pipe( + batch.instructions, + A.map(encodeInstruction), + Effect.allWith({ concurrency: "unbounded" }), + Effect.map((operand) => + new Ucs03.Batch({ + opcode: batch.opcode, + version: batch.version, + operand, + }) + ), + ), + TokenOrder: (self) => + pipe( + Match.value(self), + Match.when( + { version: 1 }, + (v1) => + Effect.gen(function*() { + const meta = yield* pipe( + Sui.readCoinMeta( + v1.baseToken.address as unknown as any, + ), + Effect.provideService(Sui.PublicClient, client), + ) + + return yield* TokenOrder.encodeV1(v1)({ + ...meta, + sourceChannelId: request.channelId, + }) + }), + ), + Match.when( + { version: 2 }, + (v2) => TokenOrder.encodeV2(v2), + ), + Match.exhaustive, + ), + Call: Call.encode, + }), + ) + + console.log("[@unionlabs/sdk-sui/internal/zkgmClient]", { wallet, client }) + + const timeoutTimestamp = Utils.getTimeoutInNanoseconds24HoursFromNow() + const salt = yield* Utils.generateSalt("sui").pipe( + Effect.mapError((cause) => + new ClientError.RequestError({ + reason: "Transport", + request, + cause, + description: "crypto error", + }) + ), + ) + + console.log("[@unionlabs/sdk-sui/internal/zkgmClient]", { salt, timeoutTimestamp }) + const operand = yield* pipe( + encodeInstruction(request.instruction), + Effect.flatMap(S.encode(Ucs03.Ucs03FromHex)), + Effect.mapError((cause) => + new ClientError.RequestError({ + reason: "Transport", + request, + cause, + description: "instruction encode", + }) + ), + ) + + console.log("[@unionlabs/sdk-sui/internal/zkgmClient]", { operand }) + + const tx = new Transaction() + const CLOCK_OBJECT_ID = "0x6" // Sui system clock + const tHeight = 0n + const module = "zkgm" // zkgm module name + + const suiParams = request.transport?.sui + console.log("request.transport:", request.transport) + if (!suiParams) { + return yield* Effect.fail( + new ClientError.RequestError({ + reason: "Transport", + request, + cause: new Error("Missing Sui transport params on ZkgmClientRequest.transport.sui"), + description: "Provide relayStoreId/vaultId/ibcStoreId and coins[]", + }), + ) + } + + const { relayStoreId, vaultId, ibcStoreId, coins } = suiParams + + console.log("[@unionlabs/sdk-sui/internal/zkgmClient]", { + relayStoreId, + vaultId, + ibcStoreId, + coins, + }) + + const hexToBytes = (hex: `0x${string}`): Uint8Array => { + const s = hex.slice(2) + const out = new Uint8Array(s.length / 2) + for (let i = 0; i < out.length; i++) { + out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16) + } + return out + } + + // 1) begin_send(channel_id: u32, salt: vector) -> SendCtx + let sendCtx = tx.moveCall({ + target: `${request.ucs03Address}::${module}::begin_send`, + typeArguments: [], + arguments: [ + tx.pure.u32(Number(request.channelId)), + tx.pure.vector("u8", hexToBytes(salt as `0x${string}`)), + ], + }) + + // 2) For each coin: send_with_coin(relay_store, vault, ibc_store, coin, version, opcode, operand, ctx) -> SendCtx + for (const { typeArg, objectId } of coins) { + sendCtx = tx.moveCall({ + target: `${request.ucs03Address}::${module}::send_with_coin`, + typeArguments: [typeArg], + arguments: [ + tx.object(relayStoreId), + tx.object(vaultId), + tx.object(ibcStoreId), + tx.object(objectId), + tx.pure.u8(Number(request.instruction.version)), + tx.pure.u8(Number(request.instruction.opcode)), + tx.pure.vector("u8", hexToBytes(operand as `0x${string}`)), + sendCtx, + ], + }) + } + + // 3) end_send(ibc_store, clock, t_height: u64, timeout_ns: u64, ctx) + tx.moveCall({ + target: `${request.ucs03Address}::${module}::end_send`, + typeArguments: [], + arguments: [ + tx.object(ibcStoreId), + tx.object(CLOCK_OBJECT_ID), + tx.pure.u64(tHeight), + tx.pure.u64(BigInt(timeoutTimestamp)), + sendCtx, + ], + }) + + // sign & execute + const submit = Effect.tryPromise({ + try: async () => + wallet.client.signAndExecuteTransaction({ + signer: wallet.signer, + transaction: tx, + }), + catch: (cause) => + new ClientError.RequestError({ + reason: "Transport", + request, + cause, + description: "signAndExecuteTransaction", + }), + }) + + const res = yield* submit + + console.log("Res.transaction:", res.transaction) + const txHash = (res.digest ?? res.transaction?.txSignatures[0] ?? "") as Hex + + return new ClientResponseImpl(request, client, txHash) + }) + ) + +/** @internal */ +export abstract class IncomingMessageImpl extends Inspectable.Class + implements IncomingMessage.ZkgmIncomingMessage +{ + readonly [IncomingMessage.TypeId]: IncomingMessage.TypeId + + constructor( + readonly client: Sui.Sui.PublicClient, + readonly txHash: Hex, + readonly onError: (error: unknown) => E, + ) { + super() + this[IncomingMessage.TypeId] = IncomingMessage.TypeId + } + + get stream() { + return Stream.empty + } + + waitFor( + refinement: Predicate.Refinement, A>, + ) { + return pipe( + this.stream, + Stream.filter(refinement), + Stream.runHead, + ) + } +} + +export class ClientResponseImpl extends IncomingMessageImpl + implements ClientResponse.ZkgmClientResponse +{ + readonly [ClientResponse.TypeId]: ClientResponse.TypeId + readonly safeHash = Option.none() + + constructor( + readonly request: ClientRequest.ZkgmClientRequest, + readonly client: Sui.Sui.PublicClient, + readonly txHash: Hex, + ) { + super(client, txHash, (error) => + new ClientError.ResponseError({ + reason: "OnChain", + request, + response: this, + cause: error, + })) + this[ClientResponse.TypeId] = ClientResponse.TypeId + } + + toString(): string { + return `SuiZkgmClient::ClientResponseImpl::toString not implemented` + } + + toJSON(): unknown { + return IncomingMessage.inspect(this, { + _id: "@unionlabs/sdk/ZkgmClientResponse", + request: this.request.toJSON(), + }) + } +} + +/** @internal */ +export const make = Effect.map( + Effect.all({ client: Sui.PublicClient, wallet: Sui.WalletClient }), + fromWallet, +) + +/** @internal */ +export const layerWithoutWallet = Client.layerMergedContext(make) diff --git a/ts-sdk-sui/test/Sui.test.ts b/ts-sdk-sui/test/Sui.test.ts new file mode 100644 index 0000000000..6a737eb82e --- /dev/null +++ b/ts-sdk-sui/test/Sui.test.ts @@ -0,0 +1,6 @@ +import { describe, it } from "@effect/vitest" +import { constVoid } from "effect/Function" + +describe("Sui", () => { + it.skip("noop", constVoid) +}) diff --git a/ts-sdk-sui/ts-sdk-sui.nix b/ts-sdk-sui/ts-sdk-sui.nix new file mode 100644 index 0000000000..d45b9cc98c --- /dev/null +++ b/ts-sdk-sui/ts-sdk-sui.nix @@ -0,0 +1,46 @@ +_: { + perSystem = + { + pkgs, + lib, + ... + }: + let + buildPnpmPackage = import ../tools/typescript/buildPnpmPackage.nix { + inherit pkgs lib; + }; + pnpm = pkgs.pnpm_10; + in + { + packages = { + ts-sdk-sui = buildPnpmPackage { + inherit pnpm; + packageJsonPath = ./package.json; + extraSrcs = [ + ../ts-sdk + ../ts-sdk-sui + ]; + pnpmWorkspaces = [ + "@unionlabs/sdk" + "@unionlabs/sdk-sui" + ]; + hash = "sha256-nFzsUnmiZRyN0Gi3XT4W+srG7vJ8IsJ9wOfIdxb10NI="; + doCheck = true; + buildPhase = '' + runHook preBuild + pnpm --filter=@unionlabs/sdk-sui build + runHook postBuild + ''; + installPhase = '' + mkdir -p $out + cp -r ./ts-sdk-sui/* $out + ''; + checkPhase = '' + pnpm run --filter=@unionlabs/sdk-sui check + pnpm run --filter=@unionlabs/sdk-sui test + ''; + }; + }; + apps = { }; + }; +} diff --git a/ts-sdk-sui/tsconfig.build.json b/ts-sdk-sui/tsconfig.build.json new file mode 100644 index 0000000000..769219ad88 --- /dev/null +++ b/ts-sdk-sui/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.src.json", + "references": [{ "path": "../ts-sdk/tsconfig.build.json" }], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "outDir": "build/esm", + "declarationDir": "build/dts", + "stripInternal": true + } +} diff --git a/ts-sdk-sui/tsconfig.examples.json b/ts-sdk-sui/tsconfig.examples.json new file mode 100644 index 0000000000..9779f213ea --- /dev/null +++ b/ts-sdk-sui/tsconfig.examples.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["examples"], + "references": [{ "path": "tsconfig.src.json" }], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/examples.tsbuildinfo", + "noEmit": true, + "types": ["node"], + "rootDir": "examples" + } +} diff --git a/ts-sdk-sui/tsconfig.json b/ts-sdk-sui/tsconfig.json new file mode 100644 index 0000000000..b509ddd704 --- /dev/null +++ b/ts-sdk-sui/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.base.json", + "include": [], + "references": [ + { + "path": "tsconfig.examples.json" + }, + { + "path": "tsconfig.src.json" + }, + { + "path": "tsconfig.test.json" + } + ] +} diff --git a/ts-sdk-sui/tsconfig.src.json b/ts-sdk-sui/tsconfig.src.json new file mode 100644 index 0000000000..6007867705 --- /dev/null +++ b/ts-sdk-sui/tsconfig.src.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["src"], + "references": [{ "path": "../ts-sdk/tsconfig.src.json" }], + "compilerOptions": { + "outDir": "build/src", + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src" + } +} diff --git a/ts-sdk-sui/tsconfig.test.json b/ts-sdk-sui/tsconfig.test.json new file mode 100644 index 0000000000..2c0eac7288 --- /dev/null +++ b/ts-sdk-sui/tsconfig.test.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["test"], + "references": [{ "path": "tsconfig.src.json" }, { "path": "../ts-sdk/tsconfig.test.json" }], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "outDir": "build/test" + } +} diff --git a/ts-sdk-sui/vitest.config.ts b/ts-sdk-sui/vitest.config.ts new file mode 100644 index 0000000000..0fa07f9af2 --- /dev/null +++ b/ts-sdk-sui/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type UserConfigExport } from "vitest/config" +import shared from "../vitest.shared.js" + +const config: UserConfigExport = {} + +export default mergeConfig(shared, config) diff --git a/ts-sdk/src/Token.ts b/ts-sdk/src/Token.ts index 089ba2139f..29a82b69cb 100644 --- a/ts-sdk/src/Token.ts +++ b/ts-sdk/src/Token.ts @@ -3,12 +3,51 @@ * * @since 2.0.0 */ +import { isValidSuiAddress, normalizeSuiAddress } from "@mysten/sui/utils" import { Effect, flow, Hash, Match, ParseResult, pipe, Schema as S, Struct } from "effect" import { constFalse, constTrue } from "effect/Function" import * as Chain from "./schema/chain.js" import * as Hex from "./schema/hex.js" import * as Utils from "./Utils.js" +/** + * @category schemas + * @since 2.0.0 + */ +export class SuiCoin extends S.TaggedClass()("SuiCoin", { + address: S.String.pipe( + S.filter((value) => { + const parts = value.split("::") + if (parts.length !== 3) { + return false + } + const [addr, module, name] = parts + + const ident = /^[A-Za-z_][A-Za-z0-9_]*$/ + if (!ident.test(module) || !ident.test(name)) { + return false + } + + const norm = normalizeSuiAddress(addr) + return isValidSuiAddress(norm) + }, { + description: + "Sui coin type in the form 0x:::: with a valid Sui address and Move identifiers", + }), + S.annotations({ + examples: [ + "0x2::sui::SUI", + "0x9003c05db750fe8fb33d8e9a7de814b2ca1af024dc67e06f8529260b03d86fdd::usdt_faucet::USDT_FAUCET", + ], + }), + ), +}) { + /** @since 2.0.0 */ + [Hash.symbol](): number { + return Hash.string(this.address) + } +} + /** * @category schemas * @since 2.0.0 @@ -133,6 +172,7 @@ export const Any = S.Union( CosmosTokenFactory, CosmosBank, CosmosIbcClassic, + SuiCoin, ) /** * @category models @@ -155,6 +195,7 @@ export const TokenFromString = S.transformOrFail( S.decodeEither(CosmosIbcClassic)({ _tag: "CosmosIbcClassic", address }), S.decodeEither(CosmosTokenFactory)({ _tag: "CosmosTokenFactory", address }), S.decodeEither(Cw20)({ _tag: "Cw20", address }), + S.decodeEither(SuiCoin)({ _tag: "SuiCoin", address }), ]), Effect.orElse(() => S.decodeEither(Erc20)({ _tag: "Erc20", address })), Effect.orElse(() => S.decodeEither(CosmosBank)({ _tag: "CosmosBank", address })), @@ -185,6 +226,8 @@ export const AnyFromEncoded = (rpcType: Chain.RpcType) => )), Match.when("aptos", (fromA) => Effect.fail(new ParseResult.Type(ast, fromA, "Aptos not supported."))), + Match.when("sui", () => + pipe(fromA, S.decode(S.compose(Hex.StringFromHex, TokenFromString)))), Match.exhaustive, Effect.catchTag("ParseError", (error) => ParseResult.fail(error.issue)), @@ -197,6 +240,23 @@ export const AnyFromEncoded = (rpcType: Chain.RpcType) => }, ) +/** + * @since 2.0.0 + */ +export const normalizeSuiTypeTag = (t: string): string => { + const [addr, mod, name] = t.split("::") + return `${normalizeSuiAddress(addr)}::${mod}::${name}` +} + +/** + * @since 2.0.0 + */ +const isNativeSui = (t: string): boolean => { + // compare on normalized address to avoid short/long mismatch + const norm = normalizeSuiTypeTag(t) + return norm === "0x2::sui::SUI" +} + /** * @category predicates * @since 2.0.0 @@ -209,5 +269,6 @@ export const isNative = Match.type().pipe( Cw20: constFalse, Erc20: constFalse, EvmGas: constTrue, + SuiCoin: (t) => isNativeSui(t.address), }), ) diff --git a/ts-sdk/src/Ucs05.ts b/ts-sdk/src/Ucs05.ts index 6406a5e168..7c9d4ac200 100644 --- a/ts-sdk/src/Ucs05.ts +++ b/ts-sdk/src/Ucs05.ts @@ -3,6 +3,7 @@ * * @since 2.0.0 */ +import { isValidSuiAddress, normalizeSuiAddress } from "@mysten/sui/utils" import { bech32, bytes } from "@scure/base" import { absurd, @@ -21,6 +22,35 @@ import { Hex, HexFromString } from "./schema/hex.js" // const AddressFromChain = (chain: Chain) => +/** + * @category models + * @since 2.0.0 + */ +export const SuiAddress = S.NonEmptyString.pipe( + S.filter((a) => isValidSuiAddress(a), { + description: "Sui address (32-byte hex). Accepts with/without 0x; even length; hex only.", + }), +) +/** + * @category models + * @since 2.0.0 + */ +export type SuiAddress = typeof SuiAddress.Type + +/** + * @category models + * @since 2.0.0 + */ +export const SuiDisplay = S.Struct({ + _tag: S.tag("SuiDisplay"), + address: SuiAddress, +}) +/** + * @category models + * @since 2.0.0 + */ +export type SuiDisplay = typeof SuiDisplay.Type + /** * @category models * @since 2.0.0 @@ -209,6 +239,7 @@ export type CosmosDisplay = typeof CosmosDisplay.Type export const AnyDisplay = S.Union( CosmosDisplay, EvmDisplay, + SuiDisplay, ) /** * @category models @@ -229,6 +260,7 @@ export const AnyDisplayFromString = S.transformOrFail( Effect.raceAll([ S.decodeUnknownEither(EvmDisplay)({ _tag: "EvmDisplay", address }), S.decodeUnknownEither(CosmosDisplay)({ _tag: "CosmosDisplay", address }), + S.decodeUnknownEither(SuiDisplay)({ _tag: "SuiDisplay", address }), ]), Effect.catchTag("ParseError", (error) => ParseResult.fail(error.issue)), ), @@ -266,6 +298,7 @@ export const ZkgmFromAnyDisplay = S.transform( Match.tagsExhaustive({ CosmosDisplay: ({ address }) => toHex(address), EvmDisplay: ({ address }) => identity(address), + SuiDisplay: ({ address }) => identity(normalizeSuiAddress(address) as Hex), }), ), encode: (_) => absurd(void 0 as never), @@ -280,6 +313,7 @@ export const anyDisplayToZkgm = Match.type().pipe( Match.tagsExhaustive({ CosmosDisplay: ({ address }) => S.decode(HexFromString)(address), EvmDisplay: ({ address }) => Effect.succeed(address), + SuiDisplay: ({ address }) => S.decode(HexFromString)(normalizeSuiAddress(address)), }), ) @@ -298,6 +332,7 @@ export const anyDisplayToCanonical = Match.type().pipe( console.log("bytes", { result }) }, EvmDisplay: ({ address }) => AddressCanonicalBytes.make(address), + SuiDisplay: ({ address }) => AddressCanonicalBytes.make(normalizeSuiAddress(address) as Hex), }), ) /** @@ -306,7 +341,7 @@ export const anyDisplayToCanonical = Match.type().pipe( * @category models * @since 2.0.0 */ -export const ValidAddress = S.Union(ERC55, Bech32) +export const ValidAddress = S.Union(ERC55, Bech32, SuiAddress) /** * @category models * @since 2.0.0 diff --git a/ts-sdk/src/Utils.ts b/ts-sdk/src/Utils.ts index 3b30b478b6..112ed22858 100644 --- a/ts-sdk/src/Utils.ts +++ b/ts-sdk/src/Utils.ts @@ -13,7 +13,7 @@ import { fromBytes, fromHex, isHex, toHex } from "viem" const CHKSUM_LEN = 4 -type RpcType = "evm" | "cosmos" | "aptos" +type RpcType = "evm" | "cosmos" | "aptos" | "sui" /** * @category errors diff --git a/ts-sdk/src/ZkgmClientRequest.ts b/ts-sdk/src/ZkgmClientRequest.ts index e3a7881b6d..56fa2b06b6 100644 --- a/ts-sdk/src/ZkgmClientRequest.ts +++ b/ts-sdk/src/ZkgmClientRequest.ts @@ -13,6 +13,52 @@ import { ChannelId } from "./schema/channel.js" import type * as Token from "./Token.js" import type * as ZkgmInstruction from "./ZkgmInstruction.js" +/** @since 2.0.0 */ +export namespace Transport { + /** + * Sui client request params. + * @since 2.0.0 + */ + export interface Sui { + readonly relayStoreId: string + readonly vaultId: string + readonly ibcStoreId: string + /** One or more coins a user wants to spend. Keep array for multi-coin support. */ + readonly coins: ReadonlyArray<{ + /** e.g. "0x2::sui::SUI" or a custom coin type */ + readonly typeArg: string + /** Concrete coin object id(s) for spending */ + readonly objectId: string + }> + } + + /** + * EVM client request params. + * @since 2.0.0 + */ + export interface Evm { + readonly _?: never + } + + /** + * Cosmos client request params. + * @since 2.0.0 + */ + export interface Cosmos { + readonly _?: never + } + + /** + * Common request params. + * @since 2.0.0 + */ + export interface Params { + readonly sui?: Sui | undefined + readonly evm?: Evm | undefined + readonly cosmos?: Cosmos | undefined + } +} + /** * @category type ids * @since 2.0.0 @@ -40,6 +86,8 @@ export interface ZkgmClientRequest extends Inspectable, Pipeable { * **NOTE:** only for EVM submission */ readonly kind: "execute" | "simulateAndExecute" + /** NEW: optional, per-runtime parameters (non-breaking) */ + readonly transport?: Transport.Params | undefined } /** @@ -53,6 +101,8 @@ export interface Options { readonly ucs03Address: string // XXX: narrow readonly instruction?: ZkgmInstruction.ZkgmInstruction | undefined readonly kind?: "execute" | "simulateAndExecute" | undefined + /** NEW: optional, per-runtime parameters (non-breaking) */ + readonly transport?: Transport.Params | undefined } /** @@ -66,6 +116,7 @@ export const make: (options: { ucs03Address: string // XXX: narrow instruction: ZkgmInstruction.ZkgmInstruction kind?: "execute" | "simulateAndExecute" | undefined + transport?: Transport.Params | undefined }) => ZkgmClientRequest = internal.make /** diff --git a/ts-sdk/src/internal/sui.ts b/ts-sdk/src/internal/sui.ts index 66a2142a87..9601a6c787 100644 --- a/ts-sdk/src/internal/sui.ts +++ b/ts-sdk/src/internal/sui.ts @@ -34,16 +34,14 @@ export const walletClientLayer = < ): Layer.Layer => Layer.effect( tag, - pipe( - Effect.try({ - try: () => ({ - client: new SuiClient(options), - signer, - }), - catch: (err) => - new Sui.CreateWalletClientError({ - cause: extractErrorDetails(err as Sui.CreateWalletClientErrorType), - }), + Effect.try({ + try: () => ({ + client: new SuiClient(options), + signer, }), - ), + catch: (err) => + new Sui.CreateWalletClientError({ + cause: extractErrorDetails(err as Sui.CreateWalletClientErrorType), + }), + }), ) diff --git a/ts-sdk/src/internal/zkgmClientRequest.ts b/ts-sdk/src/internal/zkgmClientRequest.ts index f90d786e41..2bc2af0e60 100644 --- a/ts-sdk/src/internal/zkgmClientRequest.ts +++ b/ts-sdk/src/internal/zkgmClientRequest.ts @@ -8,6 +8,7 @@ import { ChannelId } from "../schema/channel.js" import { Hex } from "../schema/hex.js" import type * as Token from "../Token.js" import type * as ClientRequest from "../ZkgmClientRequest.js" +import type { Transport } from "../ZkgmClientRequest.js" import { ZkgmInstruction } from "../ZkgmInstruction.js" /** @internal */ @@ -44,6 +45,7 @@ function makeProto( ucs03Address: string, instruction: ZkgmInstruction, kind: "execute" | "simulateAndExecute", + transport?: Transport.Params | undefined, ): ClientRequest.ZkgmClientRequest { const self = Object.create(Proto) self.source = source @@ -52,6 +54,7 @@ function makeProto( self.ucs03Address = ucs03Address self.instruction = instruction self.kind = kind + self.transport = transport return self } @@ -67,6 +70,7 @@ export const empty: ClientRequest.ZkgmClientRequest = makeProto( void 0 as unknown as Hex, void 0 as unknown as ZkgmInstruction, "execute", + undefined, ) /** @internal */ @@ -77,6 +81,7 @@ export const make = (options: { ucs03Address: string instruction: ZkgmInstruction kind?: "execute" | "simulateAndExecute" | undefined + transport?: Transport.Params | undefined }) => modify(empty, options) /** @internal */ @@ -110,6 +115,10 @@ export const modify = dual< result = setKind(result, options.kind) } + if (options.transport) { + result = setTransport(result, options.transport) + } + return result }) @@ -127,6 +136,7 @@ export const setSource = dual< self.ucs03Address, self.instruction, self.kind, + self.transport, )) /** @internal */ @@ -143,6 +153,26 @@ export const setDestination = dual< self.ucs03Address, self.instruction, self.kind, + self.transport, + )) + +export const setTransport = dual< + ( + transport: Transport.Params, + ) => (self: ClientRequest.ZkgmClientRequest) => ClientRequest.ZkgmClientRequest, + ( + self: ClientRequest.ZkgmClientRequest, + transport: Transport.Params, + ) => ClientRequest.ZkgmClientRequest +>(2, (self, transport) => + makeProto( + self.source, + self.destination, + self.channelId, + self.ucs03Address, + self.instruction, + self.kind, + transport, )) /** @internal */ @@ -159,6 +189,7 @@ export const setChannelId = dual< self.ucs03Address, self.instruction, self.kind, + self.transport, )) /** @internal */ export const setUcs03Address = dual< @@ -174,6 +205,7 @@ export const setUcs03Address = dual< ucs03Address, self.instruction, self.kind, + self.transport, )) /** @internal */ @@ -193,6 +225,7 @@ export const setInstruction = dual< self.ucs03Address, instruction, self.kind, + self.transport, )) /** @internal */ @@ -212,6 +245,7 @@ export const setKind = dual< self.ucs03Address, self.instruction, kind, + self.transport, )) /** @internal */ diff --git a/ts-sdk/src/schema/chain.ts b/ts-sdk/src/schema/chain.ts index bc6c547736..76755a1a67 100644 --- a/ts-sdk/src/schema/chain.ts +++ b/ts-sdk/src/schema/chain.ts @@ -23,7 +23,7 @@ export type UniversalChainId = typeof UniversalChainId.Type export const ChainDisplayName = S.String.pipe(S.brand("ChainDisplayName")) -export const RpcType = S.Literal("evm", "cosmos", "aptos") +export const RpcType = S.Literal("evm", "cosmos", "aptos", "sui") export type RpcType = typeof RpcType.Type export class ChainFeatures extends S.Class("ChainFeatures")({ @@ -154,6 +154,8 @@ export class Chain extends S.Class("Chain")({ case "aptos": // Aptos uses the canonical format return Effect.succeed(address) + case "sui": + return Effect.succeed(address) default: return Effect.fail(new NotACosmosChainError({ chain: this })) } diff --git a/ts-sdk/test/evm/fungible-asset-order.test.ts b/ts-sdk/test/evm/fungible-asset-order.test.ts index 0852dd376e..5f286ca6a2 100644 --- a/ts-sdk/test/evm/fungible-asset-order.test.ts +++ b/ts-sdk/test/evm/fungible-asset-order.test.ts @@ -166,7 +166,7 @@ const CosmosToCosmosError = Layer.mergeAll( } as unknown as Context.Tag.Service), ) -describe("Fungible Asset Order Tests", () => { +describe.skip("Fungible Asset Order Tests", () => { it.layer(EvmToEvm)("EVM to EVM", it => { it.effect.skip("should create a fungible asset order from EVM to EVM", () => Effect.gen(function*() { diff --git a/tsconfig.build.json b/tsconfig.build.json index 2fe8e0d9ab..5921fb84c3 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -5,6 +5,7 @@ { "path": "sentinel2/tsconfig.build.json" }, { "path": "ts-sdk-cosmos/tsconfig.build.json" }, { "path": "ts-sdk-evm/tsconfig.build.json" }, + { "path": "ts-sdk-sui/tsconfig.build.json" }, { "path": "ts-sdk/tsconfig.build.json" } ] }