From 01ad39ec231da15599de93ff5418585b71f7df84 Mon Sep 17 00:00:00 2001 From: Kevin Peters Date: Mon, 9 Dec 2024 10:55:15 -0600 Subject: [PATCH] sui: cctp manual support --- connect/src/protocols/cctp/cctpTransfer.ts | 5 +- core/base/package.json | 2 +- core/base/src/constants/circle.ts | 9 +- core/base/src/constants/contracts/circle.ts | 8 +- examples/package.json | 2 +- package-lock.json | 18 +++ package.json | 1 + platforms/solana/package.json | 2 +- platforms/sui/protocols/cctp/package.json | 6 +- .../sui/protocols/cctp/src/circleBridge.ts | 148 ++++++++++-------- platforms/sui/protocols/cctp/src/index.ts | 7 +- platforms/sui/protocols/cctp/src/objects.ts | 33 ++-- sdk/src/platforms/sui.ts | 3 +- 13 files changed, 143 insertions(+), 101 deletions(-) diff --git a/connect/src/protocols/cctp/cctpTransfer.ts b/connect/src/protocols/cctp/cctpTransfer.ts index 63f3d7533..dd0dd637b 100644 --- a/connect/src/protocols/cctp/cctpTransfer.ts +++ b/connect/src/protocols/cctp/cctpTransfer.ts @@ -231,7 +231,7 @@ export class CircleTransfer try { msgIds = await fromChain.parseTransaction(txid); } catch (e: any) { - if (e.message.includes("no bridge messages found")) { + if (e.message.includes("no bridge messages found") || e.message.includes("not found")) { // This means it's a Circle attestation; swallow } else { throw e; @@ -592,7 +592,8 @@ export namespace CircleTransfer { // https://developers.circle.com/stablecoins/docs/required-block-confirmations const eta = - (srcChain.chain === "Polygon" ? 2_000 * 200 : finality.estimateFinalityTime(srcChain.chain)) + guardians.guardianAttestationEta; + (srcChain.chain === "Polygon" ? 2_000 * 200 : finality.estimateFinalityTime(srcChain.chain)) + + guardians.guardianAttestationEta; if (!transfer.automatic) { return { sourceToken: { token: srcToken, amount: transfer.amount }, diff --git a/core/base/package.json b/core/base/package.json index 4922781d8..6632a6847 100644 --- a/core/base/package.json +++ b/core/base/package.json @@ -133,4 +133,4 @@ "prettier": "prettier --write ./src" }, "type": "module" -} +} \ No newline at end of file diff --git a/core/base/src/constants/circle.ts b/core/base/src/constants/circle.ts index 8c2896c97..d2361f6ea 100644 --- a/core/base/src/constants/circle.ts +++ b/core/base/src/constants/circle.ts @@ -1,7 +1,7 @@ -import type { Column, Flatten, MapLevel} from './../utils/index.js'; -import { constMap, zip } from './../utils/index.js'; -import type { Chain } from './chains.js'; -import type { Network } from './networks.js'; +import type { Column, Flatten, MapLevel } from "./../utils/index.js"; +import { constMap, zip } from "./../utils/index.js"; +import type { Chain } from "./chains.js"; +import type { Network } from "./networks.js"; const circleAPIs = [ ["Mainnet", "https://iris-api.circle.com/v1/attestations"], @@ -19,6 +19,7 @@ const usdcContracts = [[ ["Solana", "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"], ["Base", "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"], ["Polygon", "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"], + ["Sui", "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC"], ]], [ "Testnet", [ ["Sepolia", "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"], diff --git a/core/base/src/constants/contracts/circle.ts b/core/base/src/constants/contracts/circle.ts index 92b61ffd9..be226f0fa 100644 --- a/core/base/src/constants/contracts/circle.ts +++ b/core/base/src/constants/contracts/circle.ts @@ -55,8 +55,8 @@ export const circleContracts = [[ wormhole: "0x0FF28217dCc90372345954563486528aa865cDd6", }], [ "Sui", { - tokenMessenger: "", - messageTransmitter: "", + tokenMessenger: "0x2aa6c5d56376c371f88a6cc42e852824994993cb9bab8d3e6450cbe3cb32b94e", + messageTransmitter: "0x08d87d37ba49e785dde270a83f8e979605b03dc552b5548f26fdf2f49bf7ed1b", wormholeRelayer: "", wormhole: "", }], @@ -105,8 +105,8 @@ export const circleContracts = [[ wormhole: "0x2703483B1a5a7c577e8680de9Df8Be03c6f30e3c", }], [ "Sui", { - tokenMessenger: "0x4e16078afc5ebfc244a8107ded4044970df5d84db384e7194b7fc444090683fd", - messageTransmitter: "0x4741a96a5903c80613f2d013492a47741cf10c6246ea38a724d354a09895cf8f", + tokenMessenger: "0x31cc14d80c175ae39777c0238f20594c6d4869cfab199f40b69f3319956b8beb", + messageTransmitter: "0x4931e06dce648b3931f890035bd196920770e913e43e45990b383f6486fdd0a5", wormholeRelayer: "", wormhole: "", }], diff --git a/examples/package.json b/examples/package.json index 2dc9d970f..7893d0773 100644 --- a/examples/package.json +++ b/examples/package.json @@ -54,4 +54,4 @@ "dependencies": { "@wormhole-foundation/sdk": "1.0.3" } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4f15a043c..908cd7c67 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", @@ -2933,6 +2934,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 @@ -9260,6 +9265,19 @@ "node": ">=16" } }, + "platforms/sui/protocols/cctp": { + "name": "@wormhole-foundation/sdk-sui-cctp", + "version": "1.0.3", + "license": "Apache-2.0", + "dependencies": { + "@mysten/sui.js": "^0.50.1", + "@wormhole-foundation/sdk-connect": "1.0.3", + "@wormhole-foundation/sdk-sui": "1.0.3" + }, + "engines": { + "node": ">=16" + } + }, "platforms/sui/protocols/core": { "name": "@wormhole-foundation/sdk-sui-core", "version": "1.0.3", diff --git a/package.json b/package.json index 7e26c10ce..f897665df 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", diff --git a/platforms/solana/package.json b/platforms/solana/package.json index c7584fed9..e1ccc0b11 100644 --- a/platforms/solana/package.json +++ b/platforms/solana/package.json @@ -106,4 +106,4 @@ } } } -} +} \ No newline at end of file diff --git a/platforms/sui/protocols/cctp/package.json b/platforms/sui/protocols/cctp/package.json index a09df5aa2..60ba1e8eb 100644 --- a/platforms/sui/protocols/cctp/package.json +++ b/platforms/sui/protocols/cctp/package.json @@ -1,6 +1,6 @@ { "name": "@wormhole-foundation/sdk-sui-cctp", - "version": "0.14.0", + "version": "1.0.3", "repository": { "type": "git", "url": "git+https://github.com/wormhole-foundation/wormhole-sdk-ts.git" @@ -46,8 +46,8 @@ }, "dependencies": { "@mysten/sui.js": "^0.50.1", - "@wormhole-foundation/sdk-connect": "0.14.0", - "@wormhole-foundation/sdk-sui": "0.14.0" + "@wormhole-foundation/sdk-connect": "1.0.3", + "@wormhole-foundation/sdk-sui": "1.0.3" }, "type": "module", "exports": { diff --git a/platforms/sui/protocols/cctp/src/circleBridge.ts b/platforms/sui/protocols/cctp/src/circleBridge.ts index 0d4032110..315c79b76 100644 --- a/platforms/sui/protocols/cctp/src/circleBridge.ts +++ b/platforms/sui/protocols/cctp/src/circleBridge.ts @@ -1,33 +1,35 @@ 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 { + SuiPlatform, + type SuiChains, + SuiUnsignedTransaction, + uint8ArrayToBCS, +} from "@wormhole-foundation/sdk-sui"; import type { AccountAddress, ChainAddress, ChainsConfig, Network, Platform, -} from '@wormhole-foundation/sdk-connect'; +} from "@wormhole-foundation/sdk-connect"; import { CircleBridge, CircleTransferMessage, circle, Contracts, encoding, -} from '@wormhole-foundation/sdk-connect'; +} 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; +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, @@ -35,29 +37,24 @@ export class SuiCircleBridge readonly provider: SuiClient, readonly contracts: Contracts, ) { - if (network === 'Devnet') - throw new Error('CircleBridge not supported on Devnet'); + if (network === "Devnet") throw new Error("CircleBridge not supported on Devnet"); - const usdcId = circle.usdcContract.get(this.network, this.chain); + 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}`); + throw new Error( + `No USDC contract configured for network=${this.network} chain=${this.chain}`, + ); } - const { - tokenMessengerState, - messageTransmitterState, - usdcTreasury, - } = suiCircleObjects(network as "Mainnet" | "Testnet"); + 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?.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`, - ); + if (!contracts.cctp?.messageTransmitter) + throw new Error(`Circle Message Transmitter contract for domain ${chain} not found`); this.usdcId = usdcId; this.usdcTreasuryId = usdcTreasury; @@ -74,26 +71,27 @@ export class SuiCircleBridge ): AsyncGenerator> { const tx = new TransactionBlock(); - const destinationDomain = circle.circleChainId.get( - this.network, - recipient.chain, - )!; + const destinationDomain = circle.circleChainId.get(this.network, recipient.chain)!; - const [primaryCoin, ...mergeCoins] = await SuiPlatform.getCoins(this.provider, sender, this.usdcId); + const [primaryCoin, ...mergeCoins] = await SuiPlatform.getCoins( + this.provider, + sender, + this.usdcId, + ); if (primaryCoin === undefined) { - throw new Error('No USDC in wallet'); + 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))); + tx.mergeCoins( + primaryCoinInput, + mergeCoins.map((coin) => tx.object(coin.coinObjectId)), + ); } - const [coin] = tx.splitCoins( - primaryCoinInput, - [amount] - ); + const [coin] = tx.splitCoins(primaryCoinInput, [amount]); tx.moveCall({ target: `${this.tokenMessengerId}::deposit_for_burn::deposit_for_burn`, @@ -104,12 +102,12 @@ export class SuiCircleBridge 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 + tx.object(this.usdcTreasuryId), // treasury object Treasury ], typeArguments: [this.usdcId], }); - yield this.createUnsignedTx(tx, "Sui.CircleBridge.Transfer") + yield this.createUnsignedTx(tx, "Sui.CircleBridge.Transfer"); } async isTransferCompleted(message: CircleBridge.Message): Promise { @@ -146,40 +144,67 @@ export class SuiCircleBridge ): AsyncGenerator> { const tx = new TransactionBlock(); + // Add receive_message move call to MessageTransmitter 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 - ] + tx.object(this.messageTransmitterStateId), // message_transmitter state + ], }); - if (!receipt) throw new Error('Failed to produce receipt'); + if (!receipt) throw new Error("Failed to produce receipt"); - const [stampedReceipt] = tx.moveCall({ + // Add handle_receive_message call to TokenMessengerMinter with Receipt from receive_message call + const [stampReceiptTicketWithBurnMessage] = 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'); + if (!stampReceiptTicketWithBurnMessage) + throw new Error("Failed to produce stamp receipt ticket with burn message"); + + // Add deconstruct_stamp_receipt_ticket_with_burn_message call + const [stampReceiptTicket] = tx.moveCall({ + target: `${this.tokenMessengerId}::handle_receive_message::deconstruct_stamp_receipt_ticket_with_burn_message`, + arguments: [stampReceiptTicketWithBurnMessage], + }); + + if (!stampReceiptTicket) throw new Error("Failed to produce stamp receipt ticket"); + + // Add stamp_receipt call + const [stampedReceipt] = tx.moveCall({ + target: `${this.messageTransmitterId}::receive_message::stamp_receipt`, + arguments: [ + stampReceiptTicket, // Receipt ticket returned from deconstruct_stamp_receipt_ticket_with_burn_message call + tx.object(this.messageTransmitterStateId), // message_transmitter state + ], + typeArguments: [ + `${this.tokenMessengerId}::message_transmitter_authenticator::MessageTransmitterAuthenticator`, + ], + }); + + if (!stampedReceipt) throw new Error("Failed to produce stamped receipt"); + // Add complete_receive_message call to MessageTransmitter with StampedReceipt from stamp_receipt call. + // Receipt and StampedReceipt are Hot Potatoes so they must be destroyed for the + // transaction to succeed. 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 - ] + tx.object(this.messageTransmitterStateId), // message_transmitter state + ], }); - yield this.createUnsignedTx(tx, 'Sui.CircleBridge.Redeem'); + yield this.createUnsignedTx(tx, "Sui.CircleBridge.Redeem"); } async parseTransactionDetails(digest: string): Promise { @@ -189,18 +214,18 @@ export class SuiCircleBridge }); if (!tx) { - throw new Error('Transaction not found'); + throw new Error("Transaction not found"); } if (!tx.events) { - throw new Error('Transaction events not found'); + throw new Error("Transaction events not found"); } - const circleMessageSentEvent = (tx.events?.find((event) => - event.type.includes("send_message::MessageSent") - )); + const circleMessageSentEvent = tx.events?.find((event) => + event.type.includes("send_message::MessageSent"), + ); if (!circleMessageSentEvent) { - throw new Error('No MessageSent event found'); + throw new Error("No MessageSent event found"); } const circleMessage = new Uint8Array((circleMessageSentEvent?.parsedJson as any).message); @@ -236,12 +261,7 @@ export class SuiCircleBridge throw new Error(`Network mismatch: ${conf.network} != ${network}`); } - return new SuiCircleBridge( - network as N, - chain, - provider, - conf.contracts, - ); + return new SuiCircleBridge(network as N, chain, provider, conf.contracts); } private createUnsignedTx( diff --git a/platforms/sui/protocols/cctp/src/index.ts b/platforms/sui/protocols/cctp/src/index.ts index f49b2ed69..44400f952 100644 --- a/platforms/sui/protocols/cctp/src/index.ts +++ b/platforms/sui/protocols/cctp/src/index.ts @@ -1,9 +1,6 @@ -// TODO uncomment when enabling Sui CCTP -/* -import { registerProtocol } from '@wormhole-foundation/sdk-connect'; +import { registerProtocol } from "@wormhole-foundation/sdk-connect"; import { SuiCircleBridge } from "./circleBridge.js"; registerProtocol("Sui", "CircleBridge", SuiCircleBridge); -export * from './circleBridge.js'; -*/ +export * from "./circleBridge.js"; diff --git a/platforms/sui/protocols/cctp/src/objects.ts b/platforms/sui/protocols/cctp/src/objects.ts index 94dbe7363..09d457da6 100644 --- a/platforms/sui/protocols/cctp/src/objects.ts +++ b/platforms/sui/protocols/cctp/src/objects.ts @@ -4,20 +4,25 @@ 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 = [ + [ + "Testnet", + { + tokenMessengerState: "0x5252abd1137094ed1db3e0d75bc36abcd287aee4bc310f8e047727ef5682e7c2", + messageTransmitterState: "0x98234bd0fa9ac12cc0a20a144a22e36d6a32f7e0a97baaeaf9c76cdc6d122d2e", + usdcTreasury: "0x7170137d4a6431bf83351ac025baf462909bffe2877d87716374fb42b9629ebe", + }, + ], + [ + "Mainnet", + { + tokenMessengerState: "0x45993eecc0382f37419864992c12faee2238f5cfe22b98ad3bf455baf65c8a2f", + messageTransmitterState: "0xf68268c3d9b1df3215f2439400c1c4ea08ac4ef4bb7d6f3ca6a2a239e17510af", + usdcTreasury: "0x57d6725e7a8b49a7b2a612f6bd66ab5f39fc95332ca48be421c3229d514a6de7", + }, + ], +] as const satisfies MapLevels<[Network, SuiCircleObjects]>; export const suiCircleObjects = constMap(_suiCircleObjects, [0, 1]); diff --git a/sdk/src/platforms/sui.ts b/sdk/src/platforms/sui.ts index a6503b738..224a8fa9a 100644 --- a/sdk/src/platforms/sui.ts +++ b/sdk/src/platforms/sui.ts @@ -9,8 +9,7 @@ const sui: PlatformDefinition = { protocols: { WormholeCore: () => import("@wormhole-foundation/sdk-sui-core"), TokenBridge: () => import("@wormhole-foundation/sdk-sui-tokenbridge"), - // TODO uncomment when enabling Sui CCTP - //CircleBridge: () => import("@wormhole-foundation/sdk-sui-cctp"), + CircleBridge: () => import("@wormhole-foundation/sdk-sui-cctp"), }, getChain: (network, chain, overrides?) => new _sui.SuiChain(