UNPKG

@wormhole-foundation/sdk-aptos-tokenbridge

Version:

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

249 lines 10.9 kB
import { ErrNotWrapped, UniversalAddress, encoding, isNative, keccak256, serialize, sha3_256, toChain, toChainId, toNative, } from "@wormhole-foundation/sdk-connect"; import { APTOS_COIN, APTOS_SEPARATOR, AptosAddress, AptosPlatform, AptosUnsignedTransaction, coalesceModuleAddress, isValidAptosType, } from "@wormhole-foundation/sdk-aptos"; import { serializeForeignAddressSeeds } from "./foreignAddress.js"; import { AptosApiError } from "@aptos-labs/ts-sdk"; export class AptosTokenBridge { network; chain; connection; contracts; chainId; tokenBridgeAddress; constructor(network, chain, connection, contracts) { this.network = network; this.chain = chain; this.connection = connection; this.contracts = contracts; this.chainId = toChainId(chain); const tokenBridgeAddress = contracts.tokenBridge; if (!tokenBridgeAddress) throw new Error(`TokenBridge contract Address for chain ${chain} not found`); this.tokenBridgeAddress = tokenBridgeAddress; } static async fromRpc(connection, config) { const [network, chain] = await AptosPlatform.chainFromRpc(connection); const conf = config[chain]; if (conf.network !== network) throw new Error("Network mismatch " + conf.network + " !== " + network); return new AptosTokenBridge(network, chain, connection, conf.contracts); } async isWrappedAsset(token) { try { await this.getOriginalAsset(token); return true; } catch (_) { return false; } } async getOriginalAsset(token) { const fqt = token.toString().split(APTOS_SEPARATOR); let originInfo = null; try { originInfo = await this.connection.getAccountResource({ accountAddress: fqt[0], resourceType: `${this.tokenBridgeAddress}::state::OriginInfo`, }); } catch (e) { if (e instanceof AptosApiError && e.data?.error_code === "resource_not_found") { // if we can't find the origin info, it means this is not a wrapped asset throw ErrNotWrapped(token.toString()); } throw e; } if (!originInfo) throw ErrNotWrapped(token.toString()); // wrapped asset const chain = toChain(parseInt(originInfo.token_chain.number)); const address = new UniversalAddress(originInfo.token_address.external_address); return { chain, address }; } async getTokenUniversalAddress(token) { return new UniversalAddress(encoding.hex.encode(sha3_256(token.toString()), true)); } async getTokenNativeAddress(originChain, token) { const assetType = originChain === this.chain ? await this.getTypeFromExternalAddress(token.toString()) : await this.getAssetFullyQualifiedType({ chain: originChain, address: token }); if (!assetType) throw new Error("Invalid asset address."); return new AptosAddress(assetType); } async hasWrappedAsset(token) { try { await this.getWrappedAsset(token); return true; } catch (_) { } return false; } async getWrappedAsset(token) { if (isNative(token.address)) throw new Error("native asset cannot be a wrapped asset"); const assetFullyQualifiedType = await this.getAssetFullyQualifiedType(token); if (!assetFullyQualifiedType) throw new Error("Invalid asset address."); // check to see if we can get origin info from asset address await this.connection.getAccountResource({ accountAddress: coalesceModuleAddress(assetFullyQualifiedType), resourceType: `${this.tokenBridgeAddress}::state::OriginInfo`, }); // if successful, we can just return the computed address return toNative(this.chain, assetFullyQualifiedType); } async isTransferCompleted(vaa) { const state = await this.connection.getAccountResource({ accountAddress: this.tokenBridgeAddress, resourceType: `${this.tokenBridgeAddress}::state::State`, }); const handle = state.consumed_vaas.elems.handle; // check if vaa hash is in consumed_vaas try { // when accessing Set<T>, key is type T and value is 0 await this.connection.getTableItem({ handle, data: { key_type: "vector<u8>", value_type: "u8", key: `0x${Buffer.from(keccak256(vaa.hash)).toString("hex")}`, }, }); return true; } catch { return false; } } async getWrappedNative() { return toNative(this.chain, APTOS_COIN); } async *createAttestation(token, payer) { const tokenId = { chain: this.chain, address: new AptosAddress(token) }; const assetType = await this.getAssetFullyQualifiedType(tokenId); if (!assetType) throw new Error("Invalid asset address."); yield this.createUnsignedTx({ function: `${this.tokenBridgeAddress}::attest_token::attest_token_entry`, typeArguments: [assetType], functionArguments: [], }, "Aptos.AttestToken"); } async *submitAttestation(vaa, payer) { yield this.createUnsignedTx({ function: `${this.tokenBridgeAddress}::wrapped::create_wrapped_coin_type`, typeArguments: [], functionArguments: [serialize(vaa)], }, "Aptos.CreateWrappedCoinType"); const assetType = await this.getAssetFullyQualifiedType(vaa.payload.token); if (!assetType) throw new Error("Invalid asset address."); yield this.createUnsignedTx({ function: `${this.tokenBridgeAddress}::wrapped::create_wrapped_coin`, typeArguments: [assetType], functionArguments: [serialize(vaa)], }, "Aptos.CreateWrappedCoin"); } async *transfer(sender, recipient, token, amount, payload) { // TODO const fee = 0n; const nonce = 0n; const fullyQualifiedType = isNative(token) ? APTOS_COIN : token.toString(); const dstAddress = recipient.address.toUniversalAddress().toUint8Array(); const dstChain = toChainId(recipient.chain); if (payload) { yield this.createUnsignedTx({ function: `${this.tokenBridgeAddress}::transfer_tokens::transfer_tokens_with_payload_entry`, typeArguments: [fullyQualifiedType], functionArguments: [amount, dstChain, dstAddress, nonce, payload], }, "Aptos.TransferTokensWithPayload"); } else { yield this.createUnsignedTx({ function: `${this.tokenBridgeAddress}::transfer_tokens::transfer_tokens_entry`, typeArguments: [fullyQualifiedType], functionArguments: [amount, dstChain, dstAddress, fee, nonce], }, "Aptos.TransferTokens"); } } async *redeem(sender, vaa, unwrapNative = true) { const assetType = vaa.payload.token.chain === this.chain ? await this.getTypeFromExternalAddress(vaa.payload.token.address.toString()) : await this.getAssetFullyQualifiedType(vaa.payload.token); if (!assetType) throw new Error("Invalid asset address."); yield this.createUnsignedTx({ function: `${this.tokenBridgeAddress}::complete_transfer::submit_vaa_and_register_entry`, typeArguments: [assetType], functionArguments: [serialize(vaa)], }, "Aptos.CompleteTransfer"); } async getAssetFullyQualifiedType(tokenId) { // native asset if (tokenId.chain === this.chain) { // originAddress should be of form address::module::type if (!isValidAptosType(tokenId.address.toString())) { return null; } return tokenId.address.toString(); } // non-native asset, derive unique address const wrappedAssetAddress = AptosTokenBridge.getForeignAssetAddress(this.chain, this.tokenBridgeAddress, tokenId); return `${wrappedAssetAddress}::coin::T`; } /** * Given a hash, returns the fully qualified type by querying the corresponding TypeInfo. * @param address Hash of fully qualified type * @returns The fully qualified type associated with the given hash */ async getTypeFromExternalAddress(address) { try { // get handle const state = await this.connection.getAccountResource({ accountAddress: this.tokenBridgeAddress, resourceType: `${this.tokenBridgeAddress}::state::State`, }); const { handle } = state.native_infos; // get type info const typeInfo = await this.connection.getTableItem({ handle, data: { key_type: `${this.tokenBridgeAddress}::token_hash::TokenHash`, value_type: "0x1::type_info::TypeInfo", key: { hash: address }, }, }); return typeInfo ? [ typeInfo.account_address, String.fromCharCode(...encoding.hex.decode(typeInfo.module_name)), String.fromCharCode(...encoding.hex.decode(typeInfo.struct_name)), ].join(APTOS_SEPARATOR) : null; } catch { return null; } } /** * Derive the module address for an asset defined by the given origin chain and address. * @param tokenBridgeAddress Address of token bridge (32 bytes) * @param originChain Chain ID of chain that original asset is from * @param originAddress Native address of asset * @returns The module address for the given asset */ static getForeignAssetAddress(chain, tokenBridgeAddress, tokenId) { if (isNative(tokenId.address)) throw new Error("Invalid token address"); const data = serializeForeignAddressSeeds({ chain: tokenId.chain, tokenBridgeAddress: new AptosAddress(tokenBridgeAddress).toUniversalAddress(), tokenId: tokenId.address.toUniversalAddress(), }); return encoding.hex.encode(sha3_256(data), true); } createUnsignedTx(txReq, description, parallelizable = false) { return new AptosUnsignedTransaction(txReq, this.network, this.chain, description, parallelizable); } } //# sourceMappingURL=tokenBridge.js.map