UNPKG

@wormhole-foundation/sdk-sui-tokenbridge

Version:

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

315 lines 15.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SuiAutomaticTokenBridge = 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"); require("@wormhole-foundation/sdk-sui-core"); const utils_js_1 = require("./utils.js"); class SuiAutomaticTokenBridge { network; chain; connection; contracts; tokenBridgeRelayerObjectId; coreBridgeObjectId; tokenBridgeObjectId; fields; constructor(network, chain, connection, contracts) { this.network = network; this.chain = chain; this.connection = connection; this.contracts = contracts; const { tokenBridge, tokenBridgeRelayer, coreBridge } = contracts; if (!tokenBridge || !tokenBridgeRelayer || !coreBridge) throw new Error(`Some object IDs for ${chain} Automatic Token Bridge not found`); this.tokenBridgeRelayerObjectId = tokenBridgeRelayer; this.tokenBridgeObjectId = tokenBridge; this.coreBridgeObjectId = coreBridge; } static async fromRpc(connection, config) { const [network, chain] = await sdk_sui_1.SuiPlatform.chainFromRpc(connection); const conf = config[chain]; if (conf.network !== network) throw new Error(`Network mismatch for chain ${chain}: ${conf.network} != ${network}`); return new SuiAutomaticTokenBridge(network, chain, connection, conf.contracts); } async *transfer(sender, recipient, token, amount, nativeGas) { const tokenAddress = new sdk_sui_1.SuiAddress((0, sdk_connect_1.isNative)(token) ? sdk_sui_1.SuiPlatform.nativeTokenId(this.network, this.chain).address : token); const coinType = tokenAddress.getCoinType(); const { coreBridge: coreBridgePackageId, tokenBridge: tokenBridgePackageId } = await this.getPackageIds(); const tx = new transactions_1.Transaction(); const feeAmount = BigInt(0); // TODO: wormhole fee const [feeCoin] = tx.splitCoins(tx.gas, [tx.pure.u64(feeAmount)]); const [transferCoin] = await (async () => { if ((0, sdk_connect_1.isNative)(token)) { return tx.splitCoins(tx.gas, [tx.pure.u64(amount)]); } else { const coins = await sdk_sui_1.SuiPlatform.getCoins(this.connection, sender, coinType); const [primaryCoin, ...mergeCoins] = coins.filter((coin) => coin.coinType === coinType); if (primaryCoin === undefined) { throw new Error(`Coins array doesn't contain any coins of type ${coinType}`); } 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 [assetInfo] = tx.moveCall({ target: `${tokenBridgePackageId}::state::verified_asset`, arguments: [tx.object(this.tokenBridgeObjectId)], typeArguments: [coinType], }); const suiRelayerPackageId = await this.getPackageId(); const [transferTicket] = tx.moveCall({ target: `${suiRelayerPackageId}::transfer::transfer_tokens_with_relay`, arguments: [ tx.object(this.tokenBridgeRelayerObjectId), transferCoin, assetInfo, tx.pure.u64(nativeGas ?? 0n), tx.pure.u16((0, sdk_connect_1.toChainId)(recipient.chain)), tx.pure.address(sdk_connect_1.encoding.hex.encode(recipient.address.toUint8Array(), true)), tx.pure.u32(123), ], 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), ], }); yield this.createUnsignedTx(tx, "AutomaticTokenBridge.transfer"); } async *redeem(sender, vaa) { const { coreBridge: coreBridgePackageId, tokenBridge: tokenBridgePackageId } = await this.getPackageIds(); const { address: tokenAddress, chain: tokenChain } = vaa.payload.token; const coinType = await (0, utils_js_1.getTokenCoinType)(this.connection, this.tokenBridgeObjectId, tokenAddress.toUniversalAddress().toUint8Array(), (0, sdk_connect_1.toChainId)(tokenChain)); if (!coinType) { throw new Error("Unable to fetch token coinType"); } 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 [redeemerReceipt] = tx.moveCall({ target: `${tokenBridgePackageId}::complete_transfer_with_payload::authorize_transfer`, arguments: [tx.object(this.tokenBridgeObjectId), tokenBridgeMessage], typeArguments: [coinType], }); const packageId = await this.getPackageId(); tx.moveCall({ target: `${packageId}::redeem::complete_transfer`, arguments: [tx.object(this.tokenBridgeRelayerObjectId), redeemerReceipt], typeArguments: [coinType], }); yield this.createUnsignedTx(tx, "AutomaticTokenBridge.redeem"); } async getRelayerFee(destination, token) { const _token = (0, sdk_connect_1.isNative)(token) ? sdk_sui_1.SuiPlatform.nativeTokenId(this.network, this.chain) : token; const tokenInfo = await this.getTokenInfo(_token.toString()); if (tokenInfo === null) { throw new Error("Unsupported token for relay"); } const fields = await this.getFields(); const relayerFees = await this.connection.getDynamicFieldObject({ parentId: this.tokenBridgeRelayerObjectId, name: { type: "vector<u8>", value: Array.from(sdk_connect_1.encoding.bytes.encode("relayer_fees")) }, }); if (!relayerFees.data || !relayerFees.data.content) { if (relayerFees.error) throw new Error("Failed to get relayer fees: " + JSON.stringify(relayerFees.error)); throw new Error("Unable to compute relayer fee"); } const { content } = relayerFees.data; if (!(0, sdk_sui_1.isMoveStructStruct)(content) || !(0, sdk_sui_1.isMoveStructId)(content.fields.id)) { throw new Error("Unable to compute relayer fee"); } const entry = await this.connection.getDynamicFieldObject({ parentId: content.fields.id.id, name: { type: "u16", value: (0, sdk_connect_1.toChainId)(destination) }, }); if (!entry.data || !entry.data.content) { if (entry.error) throw new Error("Failed to get relayer fees: " + JSON.stringify(relayerFees.error)); throw new Error("Unable to compute relayer fee"); } const { content: feeData } = entry.data; if (!(0, sdk_sui_1.isMoveStructStruct)(feeData)) { throw new Error("Unable to compute relayer fee"); } const decimals = await sdk_sui_1.SuiPlatform.getDecimals(this.network, this.chain, this.connection, token.toString()); const swapRate = tokenInfo.swap_rate; const relayerFeePrecision = fields.relayer_fee_precision; const swapRatePrecision = fields.swap_rate_precision; const fee = feeData.fields.value; return ((10n ** BigInt(decimals) * BigInt(fee) * BigInt(swapRatePrecision)) / (BigInt(swapRate) * BigInt(relayerFeePrecision))); } async maxSwapAmount(token) { const _token = (0, sdk_connect_1.isNative)(token) ? sdk_sui_1.SuiPlatform.nativeTokenId(this.network, this.chain) : token; const coinType = _token.toString(); const metadata = await this.connection.getCoinMetadata({ coinType }); if (!metadata) { throw new Error("metadata is null"); } const packageId = await this.getPackageId(); const tx = new transactions_1.Transaction(); tx.moveCall({ // Calculates the max number of tokens the recipient can convert to native // Sui. The max amount of native assets the contract will swap with the // recipient is governed by the `max_native_swap_amount` variable. target: `${packageId}::redeem::calculate_max_swap_amount_in`, arguments: [tx.object(this.tokenBridgeRelayerObjectId), tx.pure.u8(metadata.decimals)], typeArguments: [coinType], }); const result = await this.connection.devInspectTransactionBlock({ transactionBlock: tx, sender: sdk_connect_1.encoding.hex.encode(new Uint8Array(32)), }); if (!result.results || result.results.length == 0 || !result.results[0]?.returnValues || result.results[0]?.returnValues.length !== 1) throw Error("swap rate not set"); // The result is a u64 in little-endian, so we need to reverse it for decode return sdk_connect_1.encoding.bignum.decode(new Uint8Array(result.results[0].returnValues[0][0].toReversed())); } async nativeTokenAmount(token, amount) { const _token = (0, sdk_connect_1.isNative)(token) ? sdk_sui_1.SuiPlatform.nativeTokenId(this.network, this.chain) : token; const coinType = _token.toString(); const metadata = await this.connection.getCoinMetadata({ coinType }); if (!metadata) { throw new Error("metadata is null"); } const packageId = await this.getPackageId(); const tx = new transactions_1.Transaction(); tx.moveCall({ // Calculates the amount of native Sui that the recipient will receive // for swapping the `to_native_amount` of tokens. target: `${packageId}::redeem::calculate_native_swap_amount_out`, arguments: [ tx.object(this.tokenBridgeRelayerObjectId), tx.pure.u64(amount), tx.pure.u8(metadata.decimals), ], typeArguments: [coinType], }); const result = await this.connection.devInspectTransactionBlock({ transactionBlock: tx, sender: sdk_connect_1.encoding.hex.encode(new Uint8Array(32)), }); if (!result.results || result.results.length == 0 || !result.results[0]?.returnValues || result.results[0]?.returnValues.length !== 1) throw Error("swap rate not set"); // The result is a u64 in little-endian, so we need to reverse it for decode return sdk_connect_1.encoding.bignum.decode(new Uint8Array(result.results[0].returnValues[0][0].toReversed())); } async getRegisteredTokens() { const fields = await this.getFields(); const registeredTokensObjectId = fields.registered_tokens.fields.id.id; const allTokensInfo = await this.connection.getDynamicFields({ parentId: registeredTokensObjectId, }); const tokenAddresses = allTokensInfo.data.map((token) => { const { address, module, name } = (0, utils_1.parseStructTag)(token.objectType); return new sdk_sui_1.SuiAddress([address, module, name].join(sdk_sui_1.SUI_SEPARATOR)); }); return tokenAddresses; } async isRegisteredToken(token) { const tokenAddress = new sdk_sui_1.SuiAddress((0, sdk_connect_1.isNative)(token) ? sdk_sui_1.SuiPlatform.nativeTokenId(this.network, this.chain).address : token).unwrap(); try { return (await this.getTokenInfo(tokenAddress)) !== null; } catch (e) { console.error(e); } return false; } async getTokenInfo(coinType) { const fields = await this.getFields(); // Pulling the package id from the registered_tokens field const registeredTokensType = new sdk_sui_1.SuiAddress(fields.registered_tokens.type); const packageId = registeredTokensType.getPackageId(); const registeredTokensObjectId = fields.registered_tokens.fields.id.id; // Get the coin type (sui:SUI or ::coin::COIN) const parsed = new sdk_sui_1.SuiAddress(coinType); const coin = (0, sdk_sui_1.isSameType)(sdk_sui_1.SUI_COIN, parsed.unwrap()) ? sdk_sui_1.SUI_COIN : parsed.getCoinType(); try { // if the token isn't registered, then this will throw const tokenInfo = await this.connection.getDynamicFieldObject({ parentId: registeredTokensObjectId, name: { type: `${packageId}::registered_tokens::Key<${coin}>`, value: { dummy_field: false }, }, }); if (tokenInfo.error) throw new Error("Failed to get token info: " + JSON.stringify(tokenInfo.error)); if (!tokenInfo.data || !tokenInfo.data.content) throw new Error("Failed to get token info: " + JSON.stringify(tokenInfo)); const { content } = tokenInfo.data; if ((0, sdk_sui_1.isMoveStructStruct)(content) && (0, sdk_sui_1.isMoveStructStruct)(content.fields.value)) { return content.fields.value.fields; } return null; } catch (e) { if (e?.code === -32000 && e.message?.includes("RPC Error")) { console.error(e); return null; } throw e; } } async getFields() { if (!this.fields) { const fields = await (0, sdk_sui_1.getObjectFields)(this.connection, this.tokenBridgeRelayerObjectId); if (fields === null) throw new Error("Failed to get fields from token bridge relayer state"); this.fields = fields; } return this.fields; } async getPackageId() { const fields = await this.getFields(); return new sdk_sui_1.SuiAddress(fields.registered_tokens.type).getPackageId(); } async getPackageIds() { const [coreBridge, tokenBridge] = await Promise.all([ (0, sdk_sui_1.getPackageId)(this.connection, this.coreBridgeObjectId), (0, sdk_sui_1.getPackageId)(this.connection, this.tokenBridgeObjectId), ]); return { coreBridge, tokenBridge }; } createUnsignedTx(txReq, description, parallelizable = false) { return new sdk_sui_1.SuiUnsignedTransaction(txReq, this.network, this.chain, description, parallelizable); } } exports.SuiAutomaticTokenBridge = SuiAutomaticTokenBridge; //# sourceMappingURL=automaticTokenBridge.js.map