diff --git a/sdk/js/src/aptos/api/tokenBridge.ts b/sdk/js/src/aptos/api/tokenBridge.ts index 015057041..fa19ed86b 100644 --- a/sdk/js/src/aptos/api/tokenBridge.ts +++ b/sdk/js/src/aptos/api/tokenBridge.ts @@ -122,6 +122,28 @@ export const completeTransferWithPayload = ( throw new Error("Completing transfers with payload is not yet supported in the sdk"); }; +/** + * Construct a payload for a transaction that registers a coin defined by the given origin chain + * ID and address to the sender's account. + * + * The bytecode was compiled from the following Move code: + * ```move + * script { + * use aptos_framework::coin; + * use aptos_framework::signer; + * + * fun main(user: &signer) { + * if (!coin::is_account_registered(signer::address_of(user))) { + * coin::register(user); + * }; + * } + * } + * ``` + * @param tokenBridgeAddress Address of token bridge + * @param originChain Origin chain ID of asset + * @param originAddress Asset address on origin chain + * @returns Transaction payload + */ export const registerCoin = ( tokenBridgeAddress: string, originChain: ChainId | ChainName, diff --git a/sdk/js/src/token_bridge/__tests__/aptos-integration.ts b/sdk/js/src/token_bridge/__tests__/aptos-integration.ts index c9c48c5dd..1b09a7d9d 100644 --- a/sdk/js/src/token_bridge/__tests__/aptos-integration.ts +++ b/sdk/js/src/token_bridge/__tests__/aptos-integration.ts @@ -30,8 +30,8 @@ import { hexToUint8Array, redeemOnAptos, redeemOnEth, - signAndSubmitEntryFunction, - signAndSubmitScript, + generateSignAndSubmitEntryFunction, + generateSignAndSubmitScript, TokenImplementation__factory, transferFromAptos, transferFromEth, @@ -85,7 +85,7 @@ describe("Aptos SDK tests", () => { CHAIN_ID_APTOS, COIN_TYPE ); - let tx = (await signAndSubmitEntryFunction( + let tx = (await generateSignAndSubmitEntryFunction( client, sender, attestPayload @@ -143,7 +143,7 @@ describe("Aptos SDK tests", () => { CHAIN_ID_ETH, tryNativeToUint8Array(recipientAddress, CHAIN_ID_ETH) ); - tx = (await signAndSubmitEntryFunction( + tx = (await generateSignAndSubmitEntryFunction( client, sender, transferPayload @@ -251,7 +251,7 @@ describe("Aptos SDK tests", () => { attestVAA ); try { - await signAndSubmitEntryFunction( + await generateSignAndSubmitEntryFunction( client, recipient, createWrappedCoinTypePayload @@ -273,7 +273,7 @@ describe("Aptos SDK tests", () => { attestVAA ); try { - await signAndSubmitEntryFunction( + await generateSignAndSubmitEntryFunction( client, recipient, createWrappedCoinPayload @@ -359,7 +359,7 @@ describe("Aptos SDK tests", () => { // register token on aptos const script = registerCoin(aptosTokenBridge, CHAIN_ID_ETH, TEST_ERC20); - await signAndSubmitScript(client, recipient, script); + await generateSignAndSubmitScript(client, recipient, script); // redeem on aptos const balanceBeforeTransferAptos = ethers.BigNumber.from( @@ -370,7 +370,7 @@ describe("Aptos SDK tests", () => { aptosTokenBridge, transferVAA ); - await signAndSubmitEntryFunction(client, recipient, redeemPayload); + await generateSignAndSubmitEntryFunction(client, recipient, redeemPayload); expect( await getIsTransferCompletedAptos(client, aptosTokenBridge, transferVAA) ).toBe(true); diff --git a/sdk/js/src/token_bridge/attest.ts b/sdk/js/src/token_bridge/attest.ts index 1290d3449..e4332b5c4 100644 --- a/sdk/js/src/token_bridge/attest.ts +++ b/sdk/js/src/token_bridge/attest.ts @@ -323,8 +323,13 @@ export async function attestNearFromNear( }; } -// TODO: do we want to pass in a single assetAddress (instead of tokenChain and tokenAddress) as -// with other APIs above and let user derive the wrapped asset address themselves? +/** + * Attest given token from Aptos. + * @param tokenBridgeAddress Address of token bridge + * @param tokenChain Origin chain ID + * @param tokenAddress Address of token on origin chain + * @returns Transaction payload + */ export function attestFromAptos( tokenBridgeAddress: string, tokenChain: ChainId, diff --git a/sdk/js/src/token_bridge/createWrapped.ts b/sdk/js/src/token_bridge/createWrapped.ts index 245235234..24f183abf 100644 --- a/sdk/js/src/token_bridge/createWrapped.ts +++ b/sdk/js/src/token_bridge/createWrapped.ts @@ -109,13 +109,35 @@ export async function createWrappedOnNear( return msgs; } +/** + * Constructs payload to create wrapped asset type. The type is of form `{{address}}::coin::T`, + * where address is `sha256_hash(tokenBridgeAddress | chainID | "::" | originAddress | 0xFF)`. + * + * Note that the typical createWrapped call is broken into two parts on Aptos because we must first + * create the CoinType that is used by `create_wrapped_coin`. Since it's not possible to + * create a resource and use it in the same transaction, this is broken into separate transactions. + * @param tokenBridgeAddress Address of token bridge + * @param attestVAA Bytes of attest VAA + * @returns Transaction payload + */ export function createWrappedTypeOnAptos( tokenBridgeAddress: string, - signedVAA: Uint8Array + attestVAA: Uint8Array ): Types.EntryFunctionPayload { - return createWrappedCoinTypeAptos(tokenBridgeAddress, signedVAA); + return createWrappedCoinTypeAptos(tokenBridgeAddress, attestVAA); } +/** + * Constructs payload to create wrapped asset. + * + * Note that this function is typically called in tandem with `createWrappedTypeOnAptos` because + * we must first create the CoinType that is used by `create_wrapped_coin`. Since it's + * not possible to create a resource and use it in the same transaction, this is broken into + * separate transactions. + * @param tokenBridgeAddress Address of token bridge + * @param attestVAA Bytes of attest VAA + * @returns Transaction payload + */ export function createWrappedOnAptos( tokenBridgeAddress: string, attestVAA: Uint8Array diff --git a/sdk/js/src/token_bridge/getForeignAsset.ts b/sdk/js/src/token_bridge/getForeignAsset.ts index dba94787c..90f0f10ab 100644 --- a/sdk/js/src/token_bridge/getForeignAsset.ts +++ b/sdk/js/src/token_bridge/getForeignAsset.ts @@ -191,6 +191,14 @@ export async function getForeignAssetNear( return ret !== "" ? ret : null; } +/** + * Get native module address of asset given its origin info. + * @param client Client used to transfer data to/from Aptos node + * @param tokenBridgeAddress Address of token bridge + * @param originChain Chain ID of chain asset is originally from + * @param originAddress Asset address on origin chain + * @returns Asset module address on Aptos + */ export async function getForeignAssetAptos( client: AptosClient, tokenBridgeAddress: string, @@ -198,14 +206,21 @@ export async function getForeignAssetAptos( originAddress: string ): Promise { const originChainId = coalesceChainId(originChain); - const assetAddress = getForeignAssetAddress(tokenBridgeAddress, originChainId, originAddress); + const assetAddress = getForeignAssetAddress( + tokenBridgeAddress, + originChainId, + originAddress + ); if (!assetAddress) { return null; } try { // check if asset exists and throw if it doesn't - await client.getAccountResource(assetAddress, `0x1::coin::CoinInfo<${ensureHexPrefix(assetAddress)}::coin::T>`); + await client.getAccountResource( + assetAddress, + `0x1::coin::CoinInfo<${ensureHexPrefix(assetAddress)}::coin::T>` + ); return assetAddress; } catch (e) { return null; diff --git a/sdk/js/src/token_bridge/getIsTransferCompleted.ts b/sdk/js/src/token_bridge/getIsTransferCompleted.ts index 1971d32be..9ec6c58f3 100644 --- a/sdk/js/src/token_bridge/getIsTransferCompleted.ts +++ b/sdk/js/src/token_bridge/getIsTransferCompleted.ts @@ -249,10 +249,17 @@ export async function getIsTransferCompletedNear( )[1]; } +/** + * Determine whether or not the transfer in the given VAA has completed on Aptos. + * @param client Client used to transfer data to/from Aptos node + * @param tokenBridgeAddress Address of token bridge + * @param transferVAA Bytes of transfer VAA + * @returns True if transfer is completed + */ export async function getIsTransferCompletedAptos( client: AptosClient, tokenBridgeAddress: string, - signedVAA: Uint8Array, + transferVAA: Uint8Array, ): Promise { // get handle tokenBridgeAddress = ensureHexPrefix(tokenBridgeAddress); @@ -262,13 +269,13 @@ export async function getIsTransferCompletedAptos( const handle = state.consumed_vaas.elems.handle; // check if vaa hash is in consumed_vaas - const signedVAAHash = await getSignedVAAHash(signedVAA); + const transferVAAHash = await getSignedVAAHash(transferVAA); try { // when accessing Set, key is type T and value is 0 await client.getTableItem(handle, { key_type: "vector", value_type: "u8", - key: signedVAAHash, + key: transferVAAHash, }); return true; } catch { diff --git a/sdk/js/src/token_bridge/getIsWrappedAsset.ts b/sdk/js/src/token_bridge/getIsWrappedAsset.ts index 6dd873bf6..d688087fa 100644 --- a/sdk/js/src/token_bridge/getIsWrappedAsset.ts +++ b/sdk/js/src/token_bridge/getIsWrappedAsset.ts @@ -116,7 +116,13 @@ export function getIsWrappedAssetNear( return asset.endsWith("." + tokenBridge); } -// TODO: do we need to check if token is registered in bridge? +/** + * Determines whether or not given address is wrapped or native to Aptos. + * @param client Client used to transfer data to/from Aptos node + * @param tokenBridgeAddress Address of token bridge + * @param assetAddress Module address of asset + * @returns True if asset is wrapped + */ export async function getIsWrappedAssetAptos( client: AptosClient, tokenBridgeAddress: string, diff --git a/sdk/js/src/token_bridge/getOriginalAsset.ts b/sdk/js/src/token_bridge/getOriginalAsset.ts index 4af02861a..ef943ec3c 100644 --- a/sdk/js/src/token_bridge/getOriginalAsset.ts +++ b/sdk/js/src/token_bridge/getOriginalAsset.ts @@ -313,6 +313,13 @@ export async function getOriginalAssetNear( return retVal; } +/** + * Gets the origin chain ID and address of an asset on Aptos, given its fully qualified type. + * @param client Client used to transfer data to/from Aptos node + * @param tokenBridgeAddress Address of token bridge + * @param fullyQualifiedType Fully qualified type of asset + * @returns Original chain ID and address of asset + */ export async function getOriginalAssetAptos( client: AptosClient, tokenBridgeAddress: string, diff --git a/sdk/js/src/token_bridge/redeem.ts b/sdk/js/src/token_bridge/redeem.ts index 2df68ca30..849056997 100644 --- a/sdk/js/src/token_bridge/redeem.ts +++ b/sdk/js/src/token_bridge/redeem.ts @@ -385,6 +385,14 @@ export async function redeemOnNear( return options; } +/** + * Register the token specified in the given VAA in the transfer recipient's account if necessary + * and complete the transfer. + * @param client Client used to transfer data to/from Aptos node + * @param tokenBridgeAddress Address of token bridge + * @param transferVAA Bytes of transfer VAA + * @returns Transaction payload + */ export function redeemOnAptos( client: AptosClient, tokenBridgeAddress: string, diff --git a/sdk/js/src/token_bridge/transfer.ts b/sdk/js/src/token_bridge/transfer.ts index 3c274c1fb..90118c2c7 100644 --- a/sdk/js/src/token_bridge/transfer.ts +++ b/sdk/js/src/token_bridge/transfer.ts @@ -961,6 +961,17 @@ export async function transferNearFromNear( }; } +/** + * Transfer an asset on Aptos to another chain. + * @param tokenBridgeAddress Address of token bridge + * @param fullyQualifiedType Full qualified type of asset to transfer + * @param amount Amount to send to recipient + * @param recipientChain Target chain + * @param recipient Recipient's address on target chain + * @param relayerFee Fee to pay relayer + * @param payload Payload3 data, leave undefined for basic token transfers + * @returns Transaction payload + */ export function transferFromAptos( tokenBridgeAddress: string, fullyQualifiedType: string, diff --git a/sdk/js/src/utils/aptos.ts b/sdk/js/src/utils/aptos.ts index 386072cc9..6ab81bb3f 100644 --- a/sdk/js/src/utils/aptos.ts +++ b/sdk/js/src/utils/aptos.ts @@ -4,7 +4,20 @@ import { ChainId, CHAIN_ID_APTOS, ensureHexPrefix, hex } from "../utils"; import { AptosAccount, AptosClient, TxnBuilderTypes, Types } from "aptos"; import { State } from "../aptos/types"; -export const signAndSubmitEntryFunction = ( +/** + * Generate, sign, and submit a transaction calling the given entry function with the given + * arguments. Prevents transaction submission and throws if the transaction fails. + * + * This is separated from `generateSignAndSubmitScript` because it makes use of `AptosClient`'s + * `generateTransaction` which pulls ABIs from the node and uses them to encode arguments + * automatically. + * @param client Client used to transfer data to/from Aptos node + * @param sender Account that will submit transaction + * @param payload Payload containing unencoded fully qualified entry function, types, and arguments + * @param opts Override default transaction options + * @returns Data from transaction after is has been successfully submitted to mempool + */ +export const generateSignAndSubmitEntryFunction = ( client: AptosClient, sender: AptosAccount, payload: Types.EntryFunctionPayload, @@ -32,7 +45,20 @@ export const signAndSubmitEntryFunction = ( ); }; -export const signAndSubmitScript = async ( +/** + * Generate, sign, and submit a transaction containing given bytecode. Prevents transaction + * submission and throws if the transaction fails. + * + * Unlike `generateSignAndSubmitEntryFunction`, this function must construct a `RawTransaction` + * manually because `generateTransaction` does not have support for scripts for which there are + * no corresponding on-chain ABIs. Type/argument encoding is left to the caller. + * @param client Client used to transfer data to/from Aptos node + * @param sender Account that will submit transaction + * @param payload Payload containing compiled bytecode and encoded types/arguments + * @param opts Override default transaction options + * @returns Data from transaction after is has been successfully submitted to mempool + */ +export const generateSignAndSubmitScript = async ( client: AptosClient, sender: AptosAccount, payload: TxnBuilderTypes.TransactionPayloadScript, @@ -66,31 +92,16 @@ export const signAndSubmitScript = async ( return signAndSubmitTransaction(client, sender, rawTx); }; -const signAndSubmitTransaction = async ( - client: AptosClient, - sender: AptosAccount, - rawTx: TxnBuilderTypes.RawTransaction -): Promise => { - // simulate transaction - await client.simulateTransaction(sender, rawTx).then((sims) => - sims.forEach((tx) => { - if (!tx.success) { - throw new Error( - `Transaction failed: ${tx.vm_status}\n${JSON.stringify(tx, null, 2)}` - ); - } - }) - ); - - // sign & submit transaction - return client - .signTransaction(sender, rawTx) - .then((signedTx) => client.submitTransaction(signedTx)) - .then((pendingTx) => client.waitForTransactionWithResult(pendingTx.hash)); -}; - +/** + * Derives the fully qualified type of the 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; if origin chain ID is 22 (Aptos), this is the + * asset's fully qualified type + * @returns The fully qualified type on Aptos for the given asset + */ export const getAssetFullyQualifiedType = ( - tokenBridgeAddress: string, // 32 bytes + tokenBridgeAddress: string, originChain: ChainId, originAddress: string ): string | null => { @@ -116,8 +127,15 @@ export const getAssetFullyQualifiedType = ( : 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 + */ export const getForeignAssetAddress = ( - tokenBridgeAddress: string, // 32 bytes + tokenBridgeAddress: string, originChain: ChainId, originAddress: string ): string | null => { @@ -142,9 +160,20 @@ export const getForeignAssetAddress = ( ); }; -export const isValidAptosType = (address: string): boolean => - /^(0x)?[0-9a-fA-F]+::\w+::\w+$/.test(address); +/** + * Test if given string is a valid fully qualified type of moduleAddress::moduleName::structName. + * @param str String to test + * @returns Whether or not given string is a valid type + */ +export const isValidAptosType = (str: string): boolean => + /^(0x)?[0-9a-fA-F]+::\w+::\w+$/.test(str); +/** + * Hashes the given type. Because fully qualified types are a concept unique to Aptos, this + * output acts as the address on other chains. + * @param fullyQualifiedType Fully qualified type on Aptos + * @returns External address corresponding to given type + */ export const getExternalAddressFromType = ( fullyQualifiedType: string ): string => { @@ -152,6 +181,13 @@ export const getExternalAddressFromType = ( return sha3_256(fullyQualifiedType); }; +/** + * Given a hash, returns the fully qualified type by querying the corresponding TypeInfo. + * @param client Client used to transfer data to/from Aptos node + * @param tokenBridgeAddress Address of token bridge + * @param fullyQualifiedTypeHash Hash of fully qualified type + * @returns The fully qualified type associated with the given hash + */ export async function getTypeFromExternalAddress( client: AptosClient, tokenBridgeAddress: string, @@ -193,3 +229,34 @@ export async function getTypeFromExternalAddress( return null; } } + +/** + * Simulates given raw transaction and either returns the resulting transaction that was submitted + * to the mempool, or throws if it fails. + * @param client Client used to transfer data to/from Aptos node + * @param sender Account that will submit transaction + * @param rawTx Raw transaction to sign & submit + * @returns Transaction data + */ +const signAndSubmitTransaction = async ( + client: AptosClient, + sender: AptosAccount, + rawTx: TxnBuilderTypes.RawTransaction +): Promise => { + // simulate transaction + await client.simulateTransaction(sender, rawTx).then((sims) => + sims.forEach((tx) => { + if (!tx.success) { + throw new Error( + `Transaction failed: ${tx.vm_status}\n${JSON.stringify(tx, null, 2)}` + ); + } + }) + ); + + // sign & submit transaction + return client + .signTransaction(sender, rawTx) + .then((signedTx) => client.submitTransaction(signedTx)) + .then((pendingTx) => client.waitForTransactionWithResult(pendingTx.hash)); +};