sdk/aptos: documentation

This commit is contained in:
aki 2022-10-25 05:15:21 +00:00 committed by Evan Gray
parent 41fb6ba756
commit 9ae43b3dd9
11 changed files with 217 additions and 47 deletions

View File

@ -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<CoinType>(user: &signer) {
* if (!coin::is_account_registered<CoinType>(signer::address_of(user))) {
* coin::register<CoinType>(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,

View File

@ -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);

View File

@ -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,

View File

@ -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<CoinType>`. 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<CoinType>`. 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

View File

@ -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<string | null> {
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;

View File

@ -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<boolean> {
// 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<T>, key is type T and value is 0
await client.getTableItem(handle, {
key_type: "vector<u8>",
value_type: "u8",
key: signedVAAHash,
key: transferVAAHash,
});
return true;
} catch {

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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<Types.Transaction> => {
// 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<Types.Transaction> => {
// 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));
};