diff --git a/core/base/src/constants/circle.ts b/core/base/src/constants/circle.ts index 8c065d55a..ed6d6f85e 100644 --- a/core/base/src/constants/circle.ts +++ b/core/base/src/constants/circle.ts @@ -19,6 +19,7 @@ const usdcContracts = [[ ["Solana", "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"], ["Base", "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"], ["Polygon", "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"], + ["Sui", "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC"], ]], [ "Testnet", [ ["Sepolia", "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"], @@ -28,6 +29,7 @@ const usdcContracts = [[ ["Solana", "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"], ["BaseSepolia", "0x036CbD53842c5426634e7929541eC2318f3dCF7e"], ["Polygon", "0x9999f7fea5938fd3b1e26a12c3f2fb024e194f97"], + ["Sui", "0xa1ec7fc00a6f40db9693ad1415d0c193ad3906494428cf252621037bd7117e29::usdc::USDC"], ]], ] as const satisfies MapLevel>; export const usdcContract = constMap(usdcContracts); @@ -43,6 +45,7 @@ const circleDomains = [[ ["Solana", 5], ["Base", 6], ["Polygon", 7], + ["Sui", 8], ]], [ "Testnet", [ ["Sepolia", 0], @@ -52,6 +55,7 @@ const circleDomains = [[ ["Solana", 5], ["BaseSepolia", 6], ["Polygon", 7], + ["Sui", 8], ]], ] as const satisfies MapLevel>; diff --git a/core/base/src/constants/contracts/circle.ts b/core/base/src/constants/contracts/circle.ts index 587ad720f..83da6ba4c 100644 --- a/core/base/src/constants/contracts/circle.ts +++ b/core/base/src/constants/contracts/circle.ts @@ -53,6 +53,12 @@ export const circleContracts = [[ messageTransmitter: "0xF3be9355363857F3e001be68856A2f96b4C39Ba9", wormholeRelayer: "0x4cb69FaE7e7Af841e44E1A1c30Af640739378bb2", wormhole: "0x0FF28217dCc90372345954563486528aa865cDd6", + }], [ + "Sui", { + tokenMessenger: "0x410d70c8baad60f310f45c13b9656ecbfed46fdf970e051f0cac42891a848856", + messageTransmitter: "0x34c884874be4cb4b84e79fa280d7b041f186f4d1ef08be1dc74b20e94376951a", + wormholeRelayer: "", + wormhole: "", }], ]], [ "Testnet", [[ @@ -97,6 +103,12 @@ export const circleContracts = [[ messageTransmitter: "0xe09A679F56207EF33F5b9d8fb4499Ec00792eA73", wormholeRelayer: "0x4cb69FaE7e7Af841e44E1A1c30Af640739378bb2", wormhole: "0x2703483B1a5a7c577e8680de9Df8Be03c6f30e3c", + }], [ + "Sui", { + tokenMessenger: "0x4e16078afc5ebfc244a8107ded4044970df5d84db384e7194b7fc444090683fd", + messageTransmitter: "0x4741a96a5903c80613f2d013492a47741cf10c6246ea38a724d354a09895cf8f", + wormholeRelayer: "", + wormhole: "", }], ]], ] as const satisfies MapLevels<[Network, Chain, CircleContracts]>; diff --git a/examples/package.json b/examples/package.json index 0120edde4..7d6a31bed 100644 --- a/examples/package.json +++ b/examples/package.json @@ -53,4 +53,4 @@ "dependencies": { "@wormhole-foundation/sdk": "0.12.0" } -} \ No newline at end of file +} diff --git a/examples/src/cctp.ts b/examples/src/cctp.ts index 1861fd023..84ca5fc57 100644 --- a/examples/src/cctp.ts +++ b/examples/src/cctp.ts @@ -1,7 +1,8 @@ import type { Network, Signer, TransactionId, Wormhole } from "@wormhole-foundation/sdk"; -import { CircleTransfer, amount, wormhole } from "@wormhole-foundation/sdk"; +import { CircleTransfer, TransferState, amount, wormhole } from "@wormhole-foundation/sdk"; import evm from "@wormhole-foundation/sdk/evm"; import solana from "@wormhole-foundation/sdk/solana"; +import sui from "@wormhole-foundation/sdk/sui"; import type { SignerStuff } from "./helpers/index.js"; import { getSigner } from "./helpers/index.js"; @@ -16,11 +17,11 @@ AutoRelayer takes a 0.1usdc fee when xfering to any chain beside goerli, which i (async function () { // init Wormhole object, passing config for which network // to use (e.g. Mainnet/Testnet) and what Platforms to support - const wh = await wormhole("Testnet", [evm, solana]); + const wh = await wormhole("Testnet", [evm, solana, sui]); // Grab chain Contexts const sendChain = wh.getChain("Avalanche"); - const rcvChain = wh.getChain("Solana"); + const rcvChain = wh.getChain("Sui"); // Get signer from local key but anything that implements // Signer interface (e.g. wrapper around web wallet) should work @@ -28,7 +29,7 @@ AutoRelayer takes a 0.1usdc fee when xfering to any chain beside goerli, which i const destination = await getSigner(rcvChain); // 6 decimals for USDC (except for bsc, so check decimals before using this) - const amt = amount.units(amount.parse("0.2", 6)); + const amt = amount.units(amount.parse("0.01", 6)); // Choose whether or not to have the attestation delivered for you const automatic = false; @@ -105,6 +106,18 @@ async function cctpTransfer( console.log("Completing Transfer"); const dstTxids = await xfer.completeTransfer(dst.signer); console.log(`Completed Transfer: `, dstTxids); + + console.log("Tracking Transfer Progress"); + let receipt = CircleTransfer.getReceipt(xfer); + + for await (receipt of CircleTransfer.track(wh, receipt)) { + console.log("Receipt State:", receipt.state); + if (receipt.state === TransferState.DestinationFinalized) { + console.log("Transfer Confirmed Complete"); + break; + } + } + // EXAMPLE_CCTP_TRANSFER } diff --git a/package-lock.json b/package-lock.json index 7d1359e8e..4f8ad19bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "platforms/sui", "platforms/sui/protocols/core", "platforms/sui/protocols/tokenBridge", + "platforms/sui/protocols/cctp", "platforms/aptos", "platforms/aptos/protocols/core", "platforms/aptos/protocols/tokenBridge", @@ -2931,6 +2932,10 @@ "resolved": "platforms/sui", "link": true }, + "node_modules/@wormhole-foundation/sdk-sui-cctp": { + "resolved": "platforms/sui/protocols/cctp", + "link": true + }, "node_modules/@wormhole-foundation/sdk-sui-core": { "resolved": "platforms/sui/protocols/core", "link": true @@ -9252,6 +9257,19 @@ "node": ">=16" } }, + "platforms/sui/protocols/cctp": { + "name": "@wormhole-foundation/sdk-sui-cctp", + "version": "0.12.0", + "license": "Apache-2.0", + "dependencies": { + "@mysten/sui.js": "^0.50.1", + "@wormhole-foundation/sdk-connect": "0.12.0", + "@wormhole-foundation/sdk-sui": "0.12.0" + }, + "engines": { + "node": ">=16" + } + }, "platforms/sui/protocols/core": { "name": "@wormhole-foundation/sdk-sui-core", "version": "0.12.0", diff --git a/package.json b/package.json index 9deda5860..536e4dbe9 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "platforms/sui", "platforms/sui/protocols/core", "platforms/sui/protocols/tokenBridge", + "platforms/sui/protocols/cctp", "platforms/aptos", "platforms/aptos/protocols/core", "platforms/aptos/protocols/tokenBridge", @@ -67,4 +68,4 @@ "unreleased": [ "tokenRegistry" ] -} \ No newline at end of file +} diff --git a/platforms/evm/protocols/cctp/package.json b/platforms/evm/protocols/cctp/package.json index 9e570fe0e..adfd77d8b 100644 --- a/platforms/evm/protocols/cctp/package.json +++ b/platforms/evm/protocols/cctp/package.json @@ -80,4 +80,4 @@ } } } -} \ No newline at end of file +} diff --git a/platforms/sui/protocols/cctp/package.json b/platforms/sui/protocols/cctp/package.json new file mode 100644 index 000000000..9f3953151 --- /dev/null +++ b/platforms/sui/protocols/cctp/package.json @@ -0,0 +1,75 @@ +{ + "name": "@wormhole-foundation/sdk-sui-cctp", + "version": "0.12.0", + "repository": { + "type": "git", + "url": "git+https://github.com/wormhole-foundation/wormhole-sdk-ts.git" + }, + "bugs": { + "url": "https://github.com/wormhole-foundation/wormhole-sdk-ts/issues" + }, + "homepage": "https://github.com/wormhole-foundation/wormhole-sdk-ts#readme", + "directories": { + "test": "tests" + }, + "license": "Apache-2.0", + "main": "./dist/cjs/index.js", + "types": "./dist/cjs/index.d.ts", + "module": "./dist/esm/index.js", + "description": "SDK for Sui chains, used in conjunction with @wormhole-foundation/sdk", + "files": [ + "dist/esm", + "dist/cjs" + ], + "keywords": [ + "wormhole", + "sdk", + "typescript", + "connect", + "sui" + ], + "engines": { + "node": ">=16" + }, + "sideEffects": [ + "./dist/cjs/index.js", + "./dist/esm/index.js" + ], + "scripts": { + "build:cjs": "tsc -p ./tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json", + "build:esm": "tsc -p ./tsconfig.esm.json", + "build": "npm run build:esm && npm run build:cjs", + "rebuild": "npm run clean && npm run build", + "clean": "rm -rf ./dist && rm -rf ./.turbo", + "lint": "npm run prettier && eslint --fix ./src --ext .ts", + "prettier": "prettier --write ./src" + }, + "dependencies": { + "@mysten/sui.js": "^0.50.1", + "@wormhole-foundation/sdk-connect": "0.12.0", + "@wormhole-foundation/sdk-sui": "0.12.0" + }, + "type": "module", + "exports": { + ".": { + "react-native": { + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js", + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + }, + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + }, + "default": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + } + } + } +} diff --git a/platforms/sui/protocols/cctp/src/circleBridge.ts b/platforms/sui/protocols/cctp/src/circleBridge.ts new file mode 100644 index 000000000..0d4032110 --- /dev/null +++ b/platforms/sui/protocols/cctp/src/circleBridge.ts @@ -0,0 +1,254 @@ +import type { SuiClient } from "@mysten/sui.js/client"; +import { TransactionBlock } from "@mysten/sui.js/transactions"; +import { SuiPlatform, type SuiChains, SuiUnsignedTransaction, uint8ArrayToBCS } from "@wormhole-foundation/sdk-sui"; +import type { + AccountAddress, + ChainAddress, + ChainsConfig, + Network, + Platform, +} from '@wormhole-foundation/sdk-connect'; +import { + CircleBridge, + CircleTransferMessage, + circle, + Contracts, + encoding, +} from '@wormhole-foundation/sdk-connect'; + +import { suiCircleObjects } from "./objects.js"; + + +export class SuiCircleBridge + implements CircleBridge { + + readonly usdcId: string; + readonly usdcTreasuryId: string; + readonly tokenMessengerId: string; + readonly tokenMessengerStateId: string; + readonly messageTransmitterId: string; + readonly messageTransmitterStateId: string; + + constructor( + readonly network: N, + readonly chain: C, + readonly provider: SuiClient, + readonly contracts: Contracts, + ) { + if (network === 'Devnet') + throw new Error('CircleBridge not supported on Devnet'); + + const usdcId = circle.usdcContract.get(this.network, this.chain); + if (!usdcId) { + throw new Error(`No USDC contract configured for network=${this.network} chain=${this.chain}`); + } + + const { + tokenMessengerState, + messageTransmitterState, + usdcTreasury, + } = suiCircleObjects(network as "Mainnet" | "Testnet"); + + if (!contracts.cctp?.tokenMessenger) + throw new Error( + `Circle Token Messenger contract for domain ${chain} not found`, + ); + + if (!contracts.cctp?.messageTransmitter) + throw new Error( + `Circle Message Transmitter contract for domain ${chain} not found`, + ); + + this.usdcId = usdcId; + this.usdcTreasuryId = usdcTreasury; + this.tokenMessengerId = contracts.cctp?.tokenMessenger; + this.messageTransmitterId = contracts.cctp?.messageTransmitter; + this.tokenMessengerStateId = tokenMessengerState; + this.messageTransmitterStateId = messageTransmitterState; + } + + async *transfer( + sender: AccountAddress, + recipient: ChainAddress, + amount: bigint, + ): AsyncGenerator> { + const tx = new TransactionBlock(); + + const destinationDomain = circle.circleChainId.get( + this.network, + recipient.chain, + )!; + + const [primaryCoin, ...mergeCoins] = await SuiPlatform.getCoins(this.provider, sender, this.usdcId); + + if (primaryCoin === undefined) { + throw new Error('No USDC in wallet'); + } + + const primaryCoinInput = tx.object(primaryCoin.coinObjectId); + if (mergeCoins.length > 0) { + tx.mergeCoins(primaryCoinInput, mergeCoins.map((coin) => tx.object(coin.coinObjectId))); + } + + const [coin] = tx.splitCoins( + primaryCoinInput, + [amount] + ); + + tx.moveCall({ + target: `${this.tokenMessengerId}::deposit_for_burn::deposit_for_burn`, + arguments: [ + coin!, + tx.pure.u32(destinationDomain), // destination_domain + tx.pure.address(recipient.address.toUniversalAddress().toString()), // mint_recipient + tx.object(this.tokenMessengerStateId), // token_messenger_minter state + tx.object(this.messageTransmitterStateId), // message_transmitter state + tx.object("0x403"), // deny_list id, fixed address + tx.object(this.usdcTreasuryId) // treasury object Treasury + ], + typeArguments: [this.usdcId], + }); + + yield this.createUnsignedTx(tx, "Sui.CircleBridge.Transfer") + } + + async isTransferCompleted(message: CircleBridge.Message): Promise { + const tx = new TransactionBlock(); + + tx.moveCall({ + target: `${this.messageTransmitterId}::state::is_nonce_used`, + arguments: [ + tx.object(this.messageTransmitterStateId), + tx.pure.u32(message.sourceDomain), + tx.pure.u64(message.nonce), + ], + }); + + const result = await this.provider.devInspectTransactionBlock({ + sender: "0x0000000000000000000000000000000000000000000000000000000000000000", + transactionBlock: tx, + }); + + try { + /* @ts-ignore */ + const isNonceUsed = Boolean(result.results![0].returnValues![0][0][0]); + return isNonceUsed; + } catch (e) { + console.error(`Error reading if nonce was used: ${e}`); + return false; + } + } + + async *redeem( + sender: AccountAddress, + message: CircleBridge.Message, + attestation: string, + ): AsyncGenerator> { + const tx = new TransactionBlock(); + + const [receipt] = tx.moveCall({ + target: `${this.messageTransmitterId}::receive_message::receive_message`, + arguments: [ + tx.pure(uint8ArrayToBCS(CircleBridge.serialize(message))), + tx.pure(uint8ArrayToBCS(encoding.hex.decode(attestation))), + tx.object(this.messageTransmitterStateId) // message_transmitter state + ] + }); + + if (!receipt) throw new Error('Failed to produce receipt'); + + const [stampedReceipt] = tx.moveCall({ + target: `${this.tokenMessengerId}::handle_receive_message::handle_receive_message`, + arguments: [ + receipt, // Receipt object returned from receive_message call + tx.object(this.tokenMessengerStateId), // token_messenger_minter state + tx.object(this.messageTransmitterStateId), // message_transmitter state + tx.object("0x403"), // deny list, fixed address + tx.object(this.usdcTreasuryId), // usdc treasury object Treasury + ], + typeArguments: [this.usdcId], + }); + + if (!stampedReceipt) throw new Error('Failed to produce stamped receipt'); + + tx.moveCall({ + target: `${this.messageTransmitterId}::receive_message::complete_receive_message`, + arguments: [ + stampedReceipt, // Stamped receipt object returned from handle_receive_message call + tx.object(this.messageTransmitterStateId) // message_transmitter state + ] + }); + + yield this.createUnsignedTx(tx, 'Sui.CircleBridge.Redeem'); + } + + async parseTransactionDetails(digest: string): Promise { + const tx = await this.provider.waitForTransactionBlock({ + digest, + options: { showEvents: true, showEffects: true, showInput: true }, + }); + + if (!tx) { + throw new Error('Transaction not found'); + } + if (!tx.events) { + throw new Error('Transaction events not found'); + } + + const circleMessageSentEvent = (tx.events?.find((event) => + event.type.includes("send_message::MessageSent") + )); + + if (!circleMessageSentEvent) { + throw new Error('No MessageSent event found'); + } + + const circleMessage = new Uint8Array((circleMessageSentEvent?.parsedJson as any).message); + + const [msg, hash] = CircleBridge.deserialize(circleMessage); + const { payload: body } = msg; + + const xferSender = body.messageSender; + const xferReceiver = body.mintRecipient; + + const sendChain = circle.toCircleChain(this.network, msg.sourceDomain); + const rcvChain = circle.toCircleChain(this.network, msg.destinationDomain); + + const token = { chain: sendChain, address: body.burnToken }; + + return { + from: { chain: sendChain, address: xferSender }, + to: { chain: rcvChain, address: xferReceiver }, + token: token, + amount: body.amount, + message: msg, + id: { hash }, + }; + } + + static async fromRpc( + provider: SuiClient, + config: ChainsConfig, + ): Promise> { + const [network, chain] = await SuiPlatform.chainFromRpc(provider); + const conf = config[chain]!; + if (conf.network !== network) { + throw new Error(`Network mismatch: ${conf.network} != ${network}`); + } + + return new SuiCircleBridge( + network as N, + chain, + provider, + conf.contracts, + ); + } + + private createUnsignedTx( + txReq: TransactionBlock, + description: string, + parallelizable: boolean = false, + ): SuiUnsignedTransaction { + return new SuiUnsignedTransaction(txReq, this.network, this.chain, description, parallelizable); + } +} diff --git a/platforms/sui/protocols/cctp/src/index.ts b/platforms/sui/protocols/cctp/src/index.ts new file mode 100644 index 000000000..25f4c581a --- /dev/null +++ b/platforms/sui/protocols/cctp/src/index.ts @@ -0,0 +1,6 @@ +import { registerProtocol } from '@wormhole-foundation/sdk-connect'; + +import { SuiCircleBridge } from "./circleBridge.js"; + +registerProtocol("Sui", "CircleBridge", SuiCircleBridge); +export * from './circleBridge.js'; diff --git a/platforms/sui/protocols/cctp/src/objects.ts b/platforms/sui/protocols/cctp/src/objects.ts new file mode 100644 index 000000000..94dbe7363 --- /dev/null +++ b/platforms/sui/protocols/cctp/src/objects.ts @@ -0,0 +1,23 @@ +import { constMap, type MapLevels, type Network } from "@wormhole-foundation/sdk-connect"; + +type SuiCircleObjects = { + tokenMessengerState: string; + messageTransmitterState: string; + usdcTreasury: string; +} + +export const _suiCircleObjects = [[ + "Testnet", { + tokenMessengerState:"0xf410286d2c2d11722e8ef90260b942e8dd598d1b7dc9c72214ef814a4e2220b8", + messageTransmitterState: "0x18855ad15df31f43aa3e5c23433a3c62b15a9297716de66756f06d1464a0a6f7", + usdcTreasury: "0x7170137d4a6431bf83351ac025baf462909bffe2877d87716374fb42b9629ebe", + }, +], [ + "Mainnet", { + tokenMessengerState:"0x9887393d8c9eccad3e25d7ac04d7b5a1fb53b557df2f84e48d2846903b109b32", + messageTransmitterState: "0xd89e73191571cd3de6247ec00d6af48d89c245a7582c39fde20d08456c9b52f8", + usdcTreasury: "0x57d6725e7a8b49a7b2a612f6bd66ab5f39fc95332ca48be421c3229d514a6de7", + } +]] as const satisfies MapLevels<[Network, SuiCircleObjects]>; + +export const suiCircleObjects = constMap(_suiCircleObjects, [0, 1]); diff --git a/platforms/sui/protocols/cctp/tsconfig.cjs.json b/platforms/sui/protocols/cctp/tsconfig.cjs.json new file mode 100644 index 000000000..73a0e681f --- /dev/null +++ b/platforms/sui/protocols/cctp/tsconfig.cjs.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../tsconfig.cjs.json", + "include": ["src"], + "compilerOptions": { + "outDir": "dist/cjs", + "rootDir": "src" + } +} diff --git a/platforms/sui/protocols/cctp/tsconfig.esm.json b/platforms/sui/protocols/cctp/tsconfig.esm.json new file mode 100644 index 000000000..a9c110d37 --- /dev/null +++ b/platforms/sui/protocols/cctp/tsconfig.esm.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../tsconfig.esm.json", + "include": ["src"], + "compilerOptions": { + "outDir": "dist/esm", + "rootDir": "src" + } +} diff --git a/platforms/sui/protocols/cctp/typedoc.json b/platforms/sui/protocols/cctp/typedoc.json new file mode 100644 index 000000000..f2fbd427c --- /dev/null +++ b/platforms/sui/protocols/cctp/typedoc.json @@ -0,0 +1,4 @@ +{ + "extends": ["../../../../typedoc.base.json"], + "entryPoints": ["src/index.ts"], + } \ No newline at end of file diff --git a/platforms/sui/src/platform.ts b/platforms/sui/src/platform.ts index 1d9594638..4ae0f19f8 100644 --- a/platforms/sui/src/platform.ts +++ b/platforms/sui/src/platform.ts @@ -82,7 +82,7 @@ export class SuiPlatform if (fields && "decimals" in fields) return fields["decimals"]; } catch {} - const metadata = await rpc.getCoinMetadata({ coinType: parsedAddress.getCoinType() }); + const metadata = await rpc.getCoinMetadata({ coinType: parsedAddress.toString() }); if (metadata === null) throw new Error(`Can't fetch decimals for token ${parsedAddress.toString()}`); diff --git a/sdk/src/platforms/sui.ts b/sdk/src/platforms/sui.ts index 5eeca1322..224a8fa9a 100644 --- a/sdk/src/platforms/sui.ts +++ b/sdk/src/platforms/sui.ts @@ -9,6 +9,7 @@ const sui: PlatformDefinition = { protocols: { WormholeCore: () => import("@wormhole-foundation/sdk-sui-core"), TokenBridge: () => import("@wormhole-foundation/sdk-sui-tokenbridge"), + CircleBridge: () => import("@wormhole-foundation/sdk-sui-cctp"), }, getChain: (network, chain, overrides?) => new _sui.SuiChain(