@wormhole-foundation/sdk-sui-tokenbridge
Version:
SDK for Sui chains, used in conjunction with @wormhole-foundation/sdk
315 lines • 15.9 kB
JavaScript
;
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