UNPKG

@wormhole-foundation/sdk-sui-tokenbridge

Version:

SDK for Sui chains, used in conjunction with @wormhole-foundation/sdk

478 lines 24.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SuiTokenBridge = void 0; const transactions_1 = require("@mysten/sui/transactions"); const utils_1 = require("@mysten/sui/utils"); const sdk_connect_1 = require("@wormhole-foundation/sdk-connect"); const sdk_sui_1 = require("@wormhole-foundation/sdk-sui"); const sdk_sui_2 = require("@wormhole-foundation/sdk-sui"); const utils_js_1 = require("./utils.js"); require("@wormhole-foundation/sdk-sui-core"); class SuiTokenBridge { network; chain; provider; contracts; coreBridgeObjectId; tokenBridgeObjectId; chainId; constructor(network, chain, provider, contracts) { this.network = network; this.chain = chain; this.provider = provider; this.contracts = contracts; this.chainId = sdk_connect_1.nativeChainIds.networkChainToNativeChainId.get(network, chain); const tokenBridgeAddress = this.contracts.tokenBridge; if (!tokenBridgeAddress) throw new Error(`Wormhole Token Bridge contract for domain ${chain} not found`); const coreBridgeAddress = this.contracts.coreBridge; if (!coreBridgeAddress) throw new Error(`Wormhole Token Bridge contract for domain ${chain} not found`); this.tokenBridgeObjectId = tokenBridgeAddress; this.coreBridgeObjectId = coreBridgeAddress; } static async fromRpc(provider, config) { const [network, chain] = await sdk_sui_2.SuiPlatform.chainFromRpc(provider); const conf = config[chain]; if (conf.network !== network) throw new Error(`Network mismatch: ${conf.network} != ${network}`); return new SuiTokenBridge(network, chain, provider, conf.contracts); } async isWrappedAsset(token) { try { await this.getOriginalAsset(token); return true; } catch { return false; } } async getOriginalAsset(token) { let coinType = token.getCoinType(); if (!(0, sdk_sui_2.isValidSuiType)(coinType)) throw new Error(`Invalid Sui type: ${coinType}`); const res = await (0, utils_js_1.getTokenFromTokenRegistry)(this.provider, this.tokenBridgeObjectId, coinType); const fields = (0, sdk_sui_2.getFieldsFromObjectResponse)(res); if (!fields) throw (0, sdk_connect_1.ErrNotWrapped)(coinType); if (!(0, sdk_sui_2.isMoveStructObject)(fields)) throw new Error("Expected fields to be a MoveStruct"); if (!("value" in fields)) throw new Error("Expected a `value` key in fields of MoveStruct"); const val = fields["value"]; if (!(0, sdk_sui_2.isMoveStructStruct)(val)) throw new Error("Expected fields to be a MoveStruct"); // Normalize types const type = (0, sdk_sui_2.trimSuiType)(val.type); coinType = (0, sdk_sui_2.trimSuiType)(coinType); // Check if wrapped or native asset. We check inclusion instead of equality // because it saves us from making an additional RPC call to fetch the package ID. if (type.includes(`wrapped_asset::WrappedAsset<${coinType}>`)) { const info = val.fields["info"]; if (!(0, sdk_sui_2.isMoveStructStruct)(info)) throw new Error("Expected fields to be a MoveStruct"); const address = info.fields["token_address"]; if (!(0, sdk_sui_2.isMoveStructStruct)(address)) throw new Error("Expected fields to be a MoveStruct"); if (!(0, sdk_sui_2.isMoveStructObject)(address.fields)) throw new Error("Expected address data to be a MoveObject"); if (!("value" in address.fields)) throw new Error("Expected a `value` key in fields of MoveStruct"); const addressVal = address.fields["value"]; if (!(0, sdk_sui_2.isMoveStructStruct)(addressVal)) throw new Error("Expected fields to be a MoveStruct"); const universalAddress = new Uint8Array(addressVal.fields["data"]); return { chain: (0, sdk_connect_1.toChain)(Number(info.fields["token_chain"])), address: new sdk_connect_1.UniversalAddress(universalAddress), }; } throw (0, sdk_connect_1.ErrNotWrapped)(coinType); } async getTokenUniversalAddress(token) { let coinType = token.getCoinType(); if (!(0, sdk_sui_2.isValidSuiType)(coinType)) throw new Error(`Invalid Sui type: ${coinType}`); const res = await (0, utils_js_1.getTokenFromTokenRegistry)(this.provider, this.tokenBridgeObjectId, coinType); const fields = (0, sdk_sui_2.getFieldsFromObjectResponse)(res); if (!fields) { throw new Error(`Token of type ${coinType} has not been registered with the token bridge. Has it been attested?`); } if (!(0, sdk_sui_2.isMoveStructObject)(fields)) throw new Error("Expected fields to be a MoveStruct"); if (!("value" in fields)) throw new Error("Expected a `value` key in fields of MoveStruct"); const val = fields["value"]; if (!(0, sdk_sui_2.isMoveStructStruct)(val)) throw new Error("Expected fields to be a MoveStruct"); // Normalize types const type = (0, sdk_sui_2.trimSuiType)(val.type); coinType = (0, sdk_sui_2.trimSuiType)(coinType); // Check if wrapped or native asset. We check inclusion instead of equality // because it saves us from making an additional RPC call to fetch the package ID. if (type.includes(`native_asset::NativeAsset<${coinType}>`)) { // fields.value.fields.token_address.fields.value.fields.data const address = val.fields["token_address"]; if (!(0, sdk_sui_2.isMoveStructStruct)(address)) throw new Error("Expected fields to be a MoveStruct"); if (!("value" in address.fields)) throw new Error("Expected a `value` key in fields of MoveStruct"); const addressVal = address.fields["value"]; if (!(0, sdk_sui_2.isMoveStructStruct)(addressVal)) throw new Error("Expected fields to be a MoveStruct"); const universalAddress = new Uint8Array(addressVal.fields["data"]); return new sdk_connect_1.UniversalAddress(universalAddress); } throw new Error(`Token of type ${coinType} is not a native asset`); } async getTokenNativeAddress(originChain, token) { const address = await (0, utils_js_1.getTokenCoinType)(this.provider, this.tokenBridgeObjectId, token.toUint8Array(), (0, sdk_connect_1.toChainId)(originChain)); if (!address) throw new Error(`Token ${token.toString()} not found in token registry`); return new sdk_sui_1.SuiAddress(address); } async hasWrappedAsset(token) { try { await this.getWrappedAsset(token); return true; } catch (e) { } return false; } async getWrappedAsset(token) { if ((0, sdk_connect_1.isNative)(token.address)) throw new Error("Token Address required, 'native' literal not supported"); const address = await (0, utils_js_1.getTokenCoinType)(this.provider, this.tokenBridgeObjectId, token.address.toUniversalAddress().toUint8Array(), (0, sdk_connect_1.toChainId)(token.chain)); if (!address) throw (0, sdk_connect_1.ErrNotWrapped)((0, sdk_connect_1.canonicalAddress)(token)); return (0, sdk_connect_1.toNative)(this.chain, address); } async isTransferCompleted(vaa) { const tokenBridgeStateFields = await (0, sdk_sui_2.getObjectFields)(this.provider, this.tokenBridgeObjectId); if (!tokenBridgeStateFields) throw new Error("Unable to fetch object fields from token bridge state"); const hashes = tokenBridgeStateFields["consumed_vaas"]?.fields?.hashes; const keyType = (0, sdk_sui_2.getTableKeyType)(hashes?.fields?.items?.type); if (!keyType) throw new Error("Unable to get key type"); const tableObjectId = hashes?.fields?.items?.fields?.id?.id; if (!tableObjectId) throw new Error("Unable to fetch consumed VAAs table"); const response = await this.provider.getDynamicFieldObject({ parentId: tableObjectId, name: { type: keyType, value: { data: [...(0, sdk_connect_1.keccak256)(vaa.hash)], }, }, }); if (!response.error) return true; if (response.error.code === "dynamicFieldNotFound") return false; throw new Error(`Unexpected getDynamicFieldObject response ${response.error}`); } async *createAttestation(token) { const feeAmount = 0n; const nonce = 0; const coinType = token.toString(); const metadata = await this.provider.getCoinMetadata({ coinType }); if (metadata === null || metadata.id === null) throw new Error(`Coin metadata ID for type ${coinType} not found`); const [coreBridgePackageId, tokenBridgePackageId] = await this.getPackageIds(); const tx = new transactions_1.Transaction(); const [feeCoin] = tx.splitCoins(tx.gas, [tx.pure.u64(feeAmount)]); const [messageTicket] = tx.moveCall({ target: `${tokenBridgePackageId}::attest_token::attest_token`, arguments: [tx.object(this.tokenBridgeObjectId), tx.object(metadata.id), tx.pure.u32(nonce)], typeArguments: [coinType], }); tx.moveCall({ target: `${coreBridgePackageId}::publish_message::publish_message`, arguments: [ tx.object(this.coreBridgeObjectId), feeCoin, messageTicket, tx.object(utils_1.SUI_CLOCK_OBJECT_ID), ], }); yield this.createUnsignedTx(tx, "Sui.TokenBridge.CreateAttestation"); } async *submitAttestation(vaa, sender) { const [coreBridgePackageId, tokenBridgePackageId] = await this.getPackageIds(); const senderAddress = sender.toString(); const decimals = Math.min(vaa.payload.decimals, 8); const build = await this.getCoinBuildOutput(coreBridgePackageId, tokenBridgePackageId, decimals); const publishTx = await (0, sdk_sui_2.publishPackage)(build, senderAddress); yield this.createUnsignedTx(publishTx, "Sui.TokenBridge.PrepareCreateWrapped"); // TODO: refactor this to something less embarassing let coinPackageId = ""; let wrappedSetupObjectId = ""; let coinUpgradeCapId = ""; let coinMetadataObjectId = ""; let versionType = ""; let found = false; while (!found) { // wait for the result of the previous tx to fetch the new coinPackageId await new Promise((r) => setTimeout(r, 500)); const txBlocks = await this.provider.queryTransactionBlocks({ filter: { FromAddress: senderAddress }, options: { showObjectChanges: true }, limit: 3, }); // Find the txblock with both the coinPackageId and wrappedType for (const txb of txBlocks.data) { if (!("objectChanges" in txb)) continue; for (const change of txb.objectChanges) { if ((0, sdk_sui_2.isSuiPublishEvent)(change) && change.packageId !== undefined) { coinPackageId = change.packageId; } else if ((0, sdk_sui_2.isSuiCreateEvent)(change) && change.objectType.includes("WrappedAssetSetup")) { wrappedSetupObjectId = change.objectId; // TODO: what versionType = change.objectType.split(", ")[1].replace(">", ""); // ugh } else if ((0, sdk_sui_2.isSuiCreateEvent)(change) && change.objectType.includes("UpgradeCap")) { coinUpgradeCapId = change.objectId; } else if ((0, sdk_sui_2.isSuiCreateEvent)(change) && change.objectType.includes("CoinMetadata")) { coinMetadataObjectId = change.objectId; } } if (coinPackageId !== "" && wrappedSetupObjectId !== "" && coinUpgradeCapId !== "" && coinMetadataObjectId !== "") { found = true; break; } else { coinPackageId = ""; wrappedSetupObjectId = ""; coinUpgradeCapId = ""; coinMetadataObjectId = ""; } } } const coinType = (0, sdk_sui_2.getCoinTypeFromPackageId)(coinPackageId); const createTx = new transactions_1.Transaction(); const [txVaa] = createTx.moveCall({ target: `${coreBridgePackageId}::vaa::parse_and_verify`, arguments: [ createTx.object(this.coreBridgeObjectId), createTx.pure.vector("u8", (0, sdk_connect_1.serialize)(vaa)), createTx.object(utils_1.SUI_CLOCK_OBJECT_ID), ], }); const [message] = createTx.moveCall({ target: `${tokenBridgePackageId}::vaa::verify_only_once`, arguments: [createTx.object(this.tokenBridgeObjectId), txVaa], }); createTx.moveCall({ target: `${tokenBridgePackageId}::create_wrapped::complete_registration`, arguments: [ createTx.object(this.tokenBridgeObjectId), createTx.object(coinMetadataObjectId), createTx.object(wrappedSetupObjectId), createTx.object(coinUpgradeCapId), message, ], typeArguments: [coinType, versionType], }); yield this.createUnsignedTx(createTx, "Sui.TokenBridge.SubmitAttestation"); } async *transfer(sender, recipient, token, amount, payload) { // TODO: const feeAmount = 0n; const relayerFee = 0n; const nonce = 0; const senderAddress = sender.toString(); const coinType = ((0, sdk_connect_1.isNative)(token) ? utils_1.SUI_TYPE_ARG : token).toString(); const coins = await sdk_sui_2.SuiPlatform.getCoins(this.provider, sender, coinType); const [primaryCoin, ...mergeCoins] = coins.filter((coin) => (0, sdk_sui_2.isSameType)(coin.coinType, coinType)); if (primaryCoin === undefined) throw new Error(`Coins array doesn't contain any coins of type ${coinType}`); const [coreBridgePackageId, tokenBridgePackageId] = await this.getPackageIds(); const tx = new transactions_1.Transaction(); const [transferCoin] = (() => { if (coinType === utils_1.SUI_TYPE_ARG) { return tx.splitCoins(tx.gas, [tx.pure.u64(amount)]); } else { const primaryCoinInput = tx.object(primaryCoin.coinObjectId); if (mergeCoins.length) { tx.mergeCoins(primaryCoinInput, mergeCoins.map((coin) => tx.object(coin.coinObjectId))); } return tx.splitCoins(primaryCoinInput, [tx.pure.u64(amount)]); } })(); const [feeCoin] = tx.splitCoins(tx.gas, [tx.pure.u64(feeAmount)]); const [assetInfo] = tx.moveCall({ target: `${tokenBridgePackageId}::state::verified_asset`, arguments: [tx.object(this.tokenBridgeObjectId)], typeArguments: [coinType], }); if (!payload) { const [transferTicket, dust] = tx.moveCall({ target: `${tokenBridgePackageId}::transfer_tokens::prepare_transfer`, arguments: [ assetInfo, transferCoin, tx.pure.u16((0, sdk_connect_1.toChainId)(recipient.chain)), tx.pure.vector("u8", recipient.address.toUint8Array()), tx.pure.u64(relayerFee), tx.pure.u32(nonce), ], typeArguments: [coinType], }); tx.moveCall({ target: `${tokenBridgePackageId}::coin_utils::return_nonzero`, arguments: [dust], typeArguments: [coinType], }); const [messageTicket] = tx.moveCall({ target: `${tokenBridgePackageId}::transfer_tokens::transfer_tokens`, arguments: [tx.object(this.tokenBridgeObjectId), transferTicket], typeArguments: [coinType], }); tx.moveCall({ target: `${coreBridgePackageId}::publish_message::publish_message`, arguments: [ tx.object(this.coreBridgeObjectId), feeCoin, messageTicket, tx.object(utils_1.SUI_CLOCK_OBJECT_ID), ], }); yield this.createUnsignedTx(tx, "Sui.TokenBridge.Transfer"); } else { if (!senderAddress) throw new Error("senderAddress is required for transfer with payload"); // Get or create a new `EmitterCap` let isNewEmitterCap = false; const emitterCap = await (async () => { const objectId = await (0, sdk_sui_2.getOldestEmitterCapObjectId)(this.provider, coreBridgePackageId, senderAddress); if (objectId !== null) { return tx.object(objectId); } else { const [emitterCap] = tx.moveCall({ target: `${coreBridgePackageId}::emitter::new`, arguments: [tx.object(this.coreBridgeObjectId)], }); isNewEmitterCap = true; return emitterCap; } })(); const [transferTicket, dust] = tx.moveCall({ target: `${tokenBridgePackageId}::transfer_tokens_with_payload::prepare_transfer`, arguments: [ emitterCap, assetInfo, transferCoin, tx.pure.u16((0, sdk_connect_1.toChainId)(recipient.chain)), tx.pure.vector("u8", recipient.address.toUint8Array()), tx.pure.vector("u8", payload), tx.pure.u32(nonce), ], typeArguments: [coinType], }); tx.moveCall({ target: `${tokenBridgePackageId}::coin_utils::return_nonzero`, arguments: [dust], typeArguments: [coinType], }); const [messageTicket] = tx.moveCall({ target: `${tokenBridgePackageId}::transfer_tokens_with_payload::transfer_tokens_with_payload`, arguments: [tx.object(this.tokenBridgeObjectId), transferTicket], typeArguments: [coinType], }); tx.moveCall({ target: `${coreBridgePackageId}::publish_message::publish_message`, arguments: [ tx.object(this.coreBridgeObjectId), feeCoin, messageTicket, tx.object(utils_1.SUI_CLOCK_OBJECT_ID), ], }); if (isNewEmitterCap) { tx.transferObjects([emitterCap], tx.pure.address(senderAddress)); } yield this.createUnsignedTx(tx, "Sui.TokenBridge.TransferWithPayload"); } } async *redeem(sender, vaa, unwrapNative = true) { const coinType = await (0, utils_js_1.getTokenCoinType)(this.provider, this.tokenBridgeObjectId, vaa.payload.token.address.toUint8Array(), (0, sdk_connect_1.toChainId)(vaa.payload.token.chain)); if (!coinType) { throw new Error("Unable to fetch token coinType"); } const [coreBridgePackageId, tokenBridgePackageId] = await this.getPackageIds(); const tx = new transactions_1.Transaction(); const [verifiedVAA] = tx.moveCall({ target: `${coreBridgePackageId}::vaa::parse_and_verify`, arguments: [ tx.object(this.coreBridgeObjectId), tx.pure.vector("u8", (0, sdk_connect_1.serialize)(vaa)), tx.object(utils_1.SUI_CLOCK_OBJECT_ID), ], }); const [tokenBridgeMessage] = tx.moveCall({ target: `${tokenBridgePackageId}::vaa::verify_only_once`, arguments: [tx.object(this.tokenBridgeObjectId), verifiedVAA], }); const [relayerReceipt] = tx.moveCall({ target: `${tokenBridgePackageId}::complete_transfer::authorize_transfer`, arguments: [tx.object(this.tokenBridgeObjectId), tokenBridgeMessage], typeArguments: [coinType], }); const [coins] = tx.moveCall({ target: `${tokenBridgePackageId}::complete_transfer::redeem_relayer_payout`, arguments: [relayerReceipt], typeArguments: [coinType], }); tx.moveCall({ target: `${tokenBridgePackageId}::coin_utils::return_nonzero`, arguments: [coins], typeArguments: [coinType], }); yield this.createUnsignedTx(tx, "Sui.TokenBridge.Redeem"); } async getWrappedNative() { return (0, sdk_connect_1.toNative)(this.chain, utils_1.SUI_TYPE_ARG); } async getPackageIds() { // TODO: can these be cached? return Promise.all([ (0, sdk_sui_2.getPackageId)(this.provider, this.coreBridgeObjectId), (0, sdk_sui_2.getPackageId)(this.provider, this.tokenBridgeObjectId), ]); } async getCoinBuildOutput(coreBridgePackageId, tokenBridgePackageId, decimals) { if (decimals > 8) throw new Error("Decimals is capped at 8"); // Construct bytecode, parametrized by token bridge package ID and decimals const strippedTokenBridgePackageId = (await (0, sdk_sui_2.getOriginalPackageId)(this.provider, this.tokenBridgeObjectId))?.replace("0x", ""); if (!strippedTokenBridgePackageId) { throw new Error(`Original token bridge package ID not found for object ID ${this.tokenBridgeObjectId}`); } const bytecodeHex = "a11ceb0b060000000901000a020a14031e1704350405392d07669f01088502600ae502050cea02160004010b010c0205020d000002000201020003030c020001000104020700000700010001090801010c020a050600030803040202000302010702080007080100020800080303090002070801010b020209000901010608010105010b0202080008030209000504434f494e095478436f6e7465787408565f5f305f325f3011577261707065644173736574536574757004636f696e0e6372656174655f777261707065640b64756d6d795f6669656c6404696e697414707265706172655f726567697374726174696f6e0f7075626c69635f7472616e736665720673656e646572087472616e736665720a74785f636f6e746578740f76657273696f6e5f636f6e74726f6c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002" + strippedTokenBridgePackageId + "00020106010000000001090b0031" + decimals.toString(16).padStart(2, "0") + "0a0138000b012e110238010200"; const bytecode = sdk_connect_1.encoding.b64.encode(sdk_connect_1.encoding.hex.decode(bytecodeHex)); return { modules: [bytecode], dependencies: ["0x1", "0x2", tokenBridgePackageId, coreBridgePackageId].map((d) => (0, utils_1.normalizeSuiObjectId)(d)), }; } createUnsignedTx(txReq, description, parallelizable = false) { return new sdk_sui_2.SuiUnsignedTransaction(txReq, this.network, this.chain, description, parallelizable); } } exports.SuiTokenBridge = SuiTokenBridge; //# sourceMappingURL=tokenBridge.js.map