sdk/js: finish aptos tests

This commit is contained in:
heyitaki 2023-01-22 04:32:06 +00:00 committed by Evan Gray
parent 54cf6781d1
commit aa56dc5498
6 changed files with 240 additions and 142 deletions

View File

@ -1,3 +1,5 @@
import { TokenTypes } from "aptos";
export type TokenBridgeState = {
consumed_vaas: {
elems: {
@ -74,11 +76,7 @@ export type CreateTokenDataEvent = {
type: "0x3::token::CreateTokenDataEvent";
data: {
description: string;
id: {
collection: string;
creator: string;
name: string;
};
id: TokenTypes.TokenDataId;
maximum: string;
mutability_config: {
description: boolean;
@ -108,22 +106,6 @@ export type DepositEvent = {
type: "0x3::token::DepositEvent";
data: {
amount: string;
id: RawTokenId;
id: TokenTypes.TokenId;
};
};
export type RawTokenId = {
property_version: string;
token_data_id: {
collection: string;
creator: string;
name: string;
};
};
export type TokenId = {
creatorAddress: string;
collectionName: string;
tokenName: string;
propertyVersion: number;
};

View File

@ -6,24 +6,26 @@ import {
jest,
test,
} from "@jest/globals";
import { BN } from "@project-serum/anchor";
import { getAssociatedTokenAddress } from "@solana/spl-token";
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
import { AptosAccount, AptosClient, FaucetClient, Types } from "aptos";
import { ethers } from "ethers";
import Web3 from "web3";
import { DepositEvent, TokenId } from "../../aptos/types";
import { DepositEvent } from "../../aptos/types";
import {
CHAIN_ID_APTOS,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
CONTRACTS,
deriveCollectionHashFromTokenId,
deriveTokenHashFromTokenId,
generateSignAndSubmitEntryFunction,
tryNativeToHexString,
tryNativeToUint8Array,
} from "../../utils";
import { parseNftTransferVaa } from "../../vaa";
import { getForeignAssetAptos } from "../getForeignAsset";
import { getForeignAssetAptos, getForeignAssetEth } from "../getForeignAsset";
import { getIsTransferCompletedAptos } from "../getIsTransferCompleted";
import { getIsWrappedAssetAptos } from "../getIsWrappedAsset";
import { getOriginalAssetAptos } from "../getOriginalAsset";
@ -130,35 +132,31 @@ describe("Aptos NFT SDK tests", () => {
)) as Types.UserTransaction;
expect(aptosRedeemTxResult.success).toBe(true);
expect(
await getIsTransferCompletedAptos(
getIsTransferCompletedAptos(
aptosClient,
APTOS_NFT_BRIDGE_ADDRESS,
ethTransferVaa
)
).toBe(true);
).resolves.toBe(true);
// get token data
const tokenData = await getForeignAssetAptos(
const tokenId = await getForeignAssetAptos(
aptosClient,
APTOS_NFT_BRIDGE_ADDRESS,
CHAIN_ID_ETH,
tryNativeToUint8Array(ethNft.address, CHAIN_ID_ETH)
);
assertIsNotNull(tokenData);
assertIsNotNull(tokenId);
expect(
await getIsWrappedAssetAptos(
getIsWrappedAssetAptos(
aptosClient,
APTOS_NFT_BRIDGE_ADDRESS,
tokenData.creatorAddress
tokenId.token_data_id.creator
)
).toBe(true);
).resolves.toBe(true);
expect(
await getOriginalAssetAptos(
aptosClient,
APTOS_NFT_BRIDGE_ADDRESS,
tokenData.creatorAddress
)
).toMatchObject({
getOriginalAssetAptos(aptosClient, APTOS_NFT_BRIDGE_ADDRESS, tokenId)
).resolves.toStrictEqual({
isWrapped: true,
chainId: CHAIN_ID_ETH,
assetAddress: Uint8Array.from(ethTransferVaaParsed.tokenAddress),
@ -167,9 +165,9 @@ describe("Aptos NFT SDK tests", () => {
// transfer NFT from Aptos back to Ethereum
const aptosTransferPayload = await transferFromAptos(
APTOS_NFT_BRIDGE_ADDRESS,
tokenData.creatorAddress,
tokenData.collectionName,
tokenData.tokenName.padStart(64, "0"),
tokenId.token_data_id.creator,
tokenId.token_data_id.collection,
tokenId.token_data_id.name.padStart(64, "0"),
0,
CHAIN_ID_ETH,
tryNativeToUint8Array(ethSigner.address, CHAIN_ID_ETH)
@ -215,6 +213,13 @@ describe("Aptos NFT SDK tests", () => {
APTOS_COLLECTION_NAME,
"APE🦧"
);
expect(
await getIsWrappedAssetAptos(
aptosClient,
APTOS_NFT_BRIDGE_ADDRESS,
aptosAccount.address().toString()
)
).toBe(false);
// get token data from user wallet
const event = (
@ -222,24 +227,18 @@ describe("Aptos NFT SDK tests", () => {
aptosAccount.address(),
"0x3::token::TokenStore",
"deposit_events",
{ limit: 1 } // most users will more than one deposit event
{ limit: 1 }
)
)[0] as DepositEvent;
const tokenId: TokenId = {
creatorAddress: event.data.id.token_data_id.creator,
collectionName: event.data.id.token_data_id.collection,
tokenName: event.data.id.token_data_id.name,
propertyVersion: Number(event.data.id.property_version),
};
console.log(tokenId);
const depositTokenId = event.data.id;
// transfer NFT from Aptos to Solana
// transfer NFT from Aptos to Ethereum
const aptosTransferPayload = await transferFromAptos(
APTOS_NFT_BRIDGE_ADDRESS,
tokenId.creatorAddress,
tokenId.collectionName,
tokenId.tokenName,
tokenId.propertyVersion,
depositTokenId.token_data_id.creator,
depositTokenId.token_data_id.collection,
depositTokenId.token_data_id.name,
Number(depositTokenId.property_version),
CHAIN_ID_ETH,
tryNativeToUint8Array(ethSigner.address, CHAIN_ID_ETH)
);
@ -268,44 +267,85 @@ describe("Aptos NFT SDK tests", () => {
ethSigner,
aptosTransferVaa
);
console.log(JSON.stringify(ethRedeemTx, null, 2));
expect(ethRedeemTx.status).toBe(1);
// sanity check token hash and id
const tokenHash = await deriveTokenHashFromTokenId(depositTokenId);
expect(Buffer.from(tokenHash).toString("hex")).toBe(
new BN(aptosTransferVaaParsed.tokenId.toString())
.toString("hex")
.padStart(64, "0") // conversion to BN strips leading zeros
);
const foreignAssetTokenId = await getForeignAssetAptos(
aptosClient,
APTOS_NFT_BRIDGE_ADDRESS,
CHAIN_ID_APTOS,
tokenHash
);
assertIsNotNull(foreignAssetTokenId);
expect(foreignAssetTokenId).toStrictEqual(depositTokenId);
// get token address on Ethereum
const tokenHash = await deriveTokenHashFromTokenId(tokenId);
const tokenAddressAptos = await deriveCollectionHashFromTokenId(
foreignAssetTokenId
);
const tokenAddressEth = await getForeignAssetEth(
ETH_NFT_BRIDGE_ADDRESS,
ethSigner,
CHAIN_ID_APTOS,
tokenAddressAptos
);
assertIsNotNull(tokenAddressEth);
// transfer NFT from Ethereum back to Aptos
const ethTransferTx = await transferFromEth(
ETH_NFT_BRIDGE_ADDRESS,
ethSigner,
tokenAddressEth,
tokenHash,
CHAIN_ID_APTOS,
tryNativeToUint8Array(aptosAccount.address().toString(), CHAIN_ID_APTOS)
);
expect(ethTransferTx.status).toBe(1);
// observe tx and get vaa
const ethTransferVaa = await getSignedVaaEthereum(ethTransferTx);
const ethTransferVaaParsed = parseNftTransferVaa(ethTransferVaa);
expect(ethTransferVaaParsed.name).toBe(APTOS_COLLECTION_NAME);
// redeem NFT on Aptos
const aptosRedeemTxPayload = await redeemOnAptos(
APTOS_NFT_BRIDGE_ADDRESS,
ethTransferVaa
);
const aptosRedeemTx = await generateSignAndSubmitEntryFunction(
aptosClient,
aptosAccount,
aptosRedeemTxPayload
);
const aptosRedeemTxResult = (await aptosClient.waitForTransactionWithResult(
aptosRedeemTx.hash
)) as Types.UserTransaction;
expect(aptosRedeemTxResult.success).toBe(true);
expect(
await getForeignAssetAptos(
getIsTransferCompletedAptos(
aptosClient,
APTOS_NFT_BRIDGE_ADDRESS,
CHAIN_ID_APTOS,
tokenHash
ethTransferVaa
)
).toMatchObject(tokenId);
// const tokenAddress = await getForeignAssetEth(
// ETH_NFT_BRIDGE_ADDRESS,
// ethSigner,
// CHAIN_ID_APTOS,
// tokenHash
// );
// console.log(tokenAddress);
// assertIsNotNull(tokenAddress);
// // transfer NFT from Ethereum back to Aptos
// const ethTransferTx = await transferFromEth(
// ETH_NFT_BRIDGE_ADDRESS,
// ethSigner,
// tokenAddress,
// 0,
// CHAIN_ID_APTOS,
// tryNativeToUint8Array(aptosAccount.address().toString(), CHAIN_ID_APTOS)
// );
// expect(ethTransferTx.status).toBe(1);
// // observe tx and get vaa
// // redeem NFT on Aptos
// check NFT is the same
).resolves.toBe(true);
expect(
getOriginalAssetAptos(
aptosClient,
APTOS_NFT_BRIDGE_ADDRESS,
foreignAssetTokenId
)
).resolves.toStrictEqual({
isWrapped: false,
chainId: CHAIN_ID_APTOS,
assetAddress: tokenAddressAptos,
});
});
test("Transfer Solana SPL to Aptos", async () => {
@ -359,7 +399,9 @@ describe("Aptos NFT SDK tests", () => {
new Uint8Array(solanaTransferVaaParsed.tokenAddress)
);
assertIsNotNull(tokenData);
expect(tokenData.collectionName).toBe("Wormhole Bridged Solana-NFT"); // this will change if SPL cache is deprecated in favor of separate collections
expect(tokenData.token_data_id.collection).toBe(
"Wormhole Bridged Solana-NFT" // this will change if SPL cache is deprecated in favor of separate collections
);
// check if token is in user's account
const events = (await aptosClient.getEventsByEventHandle(

View File

@ -1,17 +1,12 @@
import { BN } from "@project-serum/anchor";
import { PublicKeyInitData } from "@solana/web3.js";
import { LCDClient } from "@terra-money/terra.js";
import { AptosClient, HexString } from "aptos";
import { AptosClient, HexString, TokenTypes } from "aptos";
import { ethers } from "ethers";
import { isBytes } from "ethers/lib/utils";
import { fromUint8Array } from "js-base64";
import { CHAIN_ID_SOLANA } from "..";
import {
CreateTokenDataEvent,
NftBridgeState,
RawTokenId,
TokenId,
} from "../aptos/types";
import { CreateTokenDataEvent, NftBridgeState } from "../aptos/types";
import { NFTBridge__factory } from "../ethers-contracts";
import { deriveWrappedMintKey } from "../solana/nftBridge";
import {
@ -116,14 +111,15 @@ export const getForeignAssetSol = getForeignAssetSolana;
/**
* Get the token id of a foreign asset on Aptos. Tokens on Aptos are identified
* by the tuple (creatorAddress, collectionName, tokenName, propertyVersion),
* which this method returns.
* which this method returns as an object.
*
* This method also supports native assets, in which case it expects the token
* hash (which can be obtained from `deriveTokenHashFromTokenId`).
* @param client
* @param nftBridgeAddress
* @param originChain
* @param originAddress External address of token on origin chain, or token hash if origin chain is Aptos
* @param originAddress External address of token on origin chain, or token hash
* if origin chain is Aptos
* @returns Unique token identifier on Aptos
*/
export async function getForeignAssetAptos(
@ -131,7 +127,7 @@ export async function getForeignAssetAptos(
nftBridgeAddress: string,
originChain: ChainId | ChainName,
originAddress: Uint8Array
): Promise<TokenId | null> {
): Promise<TokenTypes.TokenId | null> {
const originChainId = coalesceChainId(originChain);
if (originChainId === CHAIN_ID_APTOS) {
const state = (
@ -141,19 +137,17 @@ export async function getForeignAssetAptos(
)
).data as NftBridgeState;
const handle = state.native_infos.handle;
const value = (await client.getTableItem(handle, {
key_type: `${nftBridgeAddress}::token_hash::TokenHash`,
value_type: `0x3::token::TokenId`,
key: {
hash: HexString.fromUint8Array(originAddress).hex(),
},
})) as RawTokenId;
return {
creatorAddress: value.token_data_id.creator,
collectionName: value.token_data_id.collection,
tokenName: value.token_data_id.name,
propertyVersion: Number(value.property_version),
};
const { token_data_id, property_version } = (await client.getTableItem(
handle,
{
key_type: `${nftBridgeAddress}::token_hash::TokenHash`,
value_type: `0x3::token::TokenId`,
key: {
hash: HexString.fromUint8Array(originAddress).hex(),
},
}
)) as TokenTypes.TokenId & { __headers: unknown };
return { token_data_id, property_version };
}
const creatorAddress = await deriveResourceAccountAddress(
@ -165,19 +159,18 @@ export async function getForeignAssetAptos(
throw new Error("Could not derive creator account address");
}
// Each creator account should contain a single collection and a single token
// creation event. The latter contains the token id that we're looking for.
const event = (
await client.getEventsByEventHandle(
creatorAddress,
"0x3::token::Collections",
"create_token_data_events",
{ limit: 1 } // there should only ever be one event per resource account
{ limit: 1 }
)
)[0] as CreateTokenDataEvent;
const tokenData = event.data.id;
return {
creatorAddress: tokenData.creator,
collectionName: tokenData.collection,
tokenName: tokenData.name,
propertyVersion: 0,
token_data_id: event.data.id,
property_version: "0",
};
}

View File

@ -56,8 +56,11 @@ export async function getIsWrappedAssetAptos(
`${nftBridgeAddress}::state::OriginInfo`
);
return true;
} catch (e) {
if (e instanceof Types.ApiError && e.status === 404) {
} catch (e: any) {
if (
(e instanceof Types.ApiError || e.errorCode === "resource_not_found") &&
e.status === 404
) {
return false;
}

View File

@ -5,7 +5,7 @@ import {
PublicKeyInitData,
} from "@solana/web3.js";
import { LCDClient } from "@terra-money/terra.js";
import { AptosClient, Types } from "aptos";
import { AptosClient, TokenTypes, Types } from "aptos";
import { BigNumber, ethers } from "ethers";
import { arrayify, zeroPad } from "ethers/lib/utils";
import { WormholeWrappedInfo } from "..";
@ -20,6 +20,7 @@ import {
CHAIN_ID_APTOS,
CHAIN_ID_SOLANA,
coalesceChainId,
deriveCollectionHashFromTokenId,
hex,
} from "../utils";
import { getIsWrappedAssetEth } from "./getIsWrappedAsset";
@ -182,16 +183,26 @@ export async function getOriginalAssetTerra(
};
}
// TODO(aki): should this also return tokenId?
/**
* Given a token ID, returns the original asset chain and address. If this is a
* native asset, the asset address will be the collection hash.
* @param client
* @param nftBridgeAddress
* @param tokenId An object containing creator address, collection name, token
* name, and property version, which together uniquely identify a token on
* Aptos. For wrapped assets, property version will be 0.
* @returns Object containing origin chain and Wormhole compatible 32-byte asset
* address.
*/
export async function getOriginalAssetAptos(
client: AptosClient,
nftBridgeAddress: string,
creatorAddress: string
tokenId: TokenTypes.TokenId
): Promise<WormholeWrappedInfo> {
try {
const originInfo = (
await client.getAccountResource(
creatorAddress,
tokenId.token_data_id.creator,
`${nftBridgeAddress}::state::OriginInfo`
)
).data as OriginInfo;
@ -204,15 +215,20 @@ export async function getOriginalAssetAptos(
hex(originInfo.token_address.external_address)
),
};
} catch (e) {
if (e instanceof Types.ApiError && e.status === 404) {
return {
isWrapped: false,
chainId: CHAIN_ID_APTOS,
assetAddress: new Uint8Array(hex(creatorAddress)),
};
} catch (e: any) {
if (
!(
(e instanceof Types.ApiError || e.errorCode === "resource_not_found") &&
e.status === 404
)
) {
throw e;
}
throw e;
}
return {
isWrapped: false,
chainId: CHAIN_ID_APTOS,
assetAddress: await deriveCollectionHashFromTokenId(tokenId),
};
}

View File

@ -1,7 +1,15 @@
import { AptosAccount, AptosClient, BCS, TxnBuilderTypes, Types } from "aptos";
import {
AptosAccount,
AptosClient,
BCS,
HexString,
TokenTypes,
TxnBuilderTypes,
Types,
} from "aptos";
import { hexZeroPad } from "ethers/lib/utils";
import { sha3_256 } from "js-sha3";
import { TokenBridgeState, TokenId } from "../aptos/types";
import { NftBridgeState, TokenBridgeState } from "../aptos/types";
import {
ChainId,
ChainName,
@ -248,7 +256,7 @@ export const coalesceModuleAddress = (str: string): string => {
/**
* The NFT bridge creates resource accounts, which in turn create a collection
* and mint a singl token for each transferred NFT. This method derives the
* and mint a single token for each transferred NFT. This method derives the
* address of that resource account from the given origin chain and address.
* @param nftBridgeAddress
* @param originChain
@ -276,28 +284,82 @@ export const deriveResourceAccountAddress = async (
};
/**
* Native tokens in Aptos are represented by a single token hash that is
* derived from creator address, collection name, token name, and property
* version, ensuring that it is unique. This method derives that token hash.
* Get a hash that uniquely identifies a collection on Aptos.
* @param tokenId
* @returns Collection hash
*/
export const deriveCollectionHashFromTokenId = async (
tokenId: TokenTypes.TokenId
): Promise<Uint8Array> => {
const inputs = Buffer.concat([
BCS.bcsToBytes(
TxnBuilderTypes.AccountAddress.fromHex(tokenId.token_data_id.creator)
),
Buffer.from(sha3_256(tokenId.token_data_id.collection), "hex"),
]);
return new Uint8Array(Buffer.from(sha3_256(inputs), "hex"));
};
/**
* Get a hash that uniquely identifies a token on Aptos.
*
* Native tokens in Aptos are uniquely identified by a hash of creator address,
* collection name, token name, and property version. This hash is converted to
* a bigint in the `tokenId` field in NFT transfer VAAs.
* @param tokenId
* @returns Token hash identifying the token
*/
export const deriveTokenHashFromTokenId = async (
tokenId: TokenId
tokenId: TokenTypes.TokenId
): Promise<Uint8Array> => {
const propertyVersion = Buffer.alloc(8);
propertyVersion.writeBigUInt64BE(BigInt(tokenId.propertyVersion));
propertyVersion.writeBigUInt64BE(BigInt(tokenId.property_version));
const inputs = Buffer.concat([
BCS.bcsToBytes(
TxnBuilderTypes.AccountAddress.fromHex(tokenId.creatorAddress)
),
Buffer.from(sha3_256(tokenId.collectionName), "hex"),
Buffer.from(sha3_256(tokenId.tokenName), "hex"),
Buffer.from(tokenId.token_data_id.creator, "hex"),
Buffer.from(sha3_256(tokenId.token_data_id.collection), "hex"),
Buffer.from(sha3_256(tokenId.token_data_id.name), "hex"),
propertyVersion,
]);
return new Uint8Array(Buffer.from(sha3_256(inputs), "hex"));
};
/**
* Get creator address, collection name, token name, and property version from
* a token hash. Note that this method is meant to be used for native tokens
* that have already been registered in the NFT bridge.
*
* The token hash is stored in the `tokenId` field of NFT transfer VAAs and
* is calculated by the operations in `deriveTokenHashFromTokenId`.
* @param client
* @param nftBridgeAddress
* @param tokenHash Token hash
* @returns Token ID
*/
export const getTokenIdFromTokenHash = async (
client: AptosClient,
nftBridgeAddress: string,
tokenHash: Uint8Array
): Promise<TokenTypes.TokenId> => {
const state = (
await client.getAccountResource(
nftBridgeAddress,
`${nftBridgeAddress}::state::State`
)
).data as NftBridgeState;
const handle = state.native_infos.handle;
const { token_data_id, property_version } = (await client.getTableItem(
handle,
{
key_type: `${nftBridgeAddress}::token_hash::TokenHash`,
value_type: `0x3::token::TokenId`,
key: {
hash: HexString.fromUint8Array(tokenHash).hex(),
},
}
)) as TokenTypes.TokenId & { __headers: unknown };
return { token_data_id, property_version };
};
/**
* Simulates given raw transaction and either returns the resulting transaction that was submitted
* to the mempool, or throws if it fails.