sdk/js: add additional tests & docs

This commit is contained in:
heyitaki 2023-01-20 23:25:05 +00:00 committed by Evan Gray
parent 3bb0562f45
commit 54cf6781d1
8 changed files with 394 additions and 85 deletions

View File

@ -63,3 +63,67 @@ export type NftBridgeState = {
handle: string;
};
};
export type CreateTokenDataEvent = {
version: string;
guid: {
creation_number: string;
account_address: string;
};
sequence_number: string;
type: "0x3::token::CreateTokenDataEvent";
data: {
description: string;
id: {
collection: string;
creator: string;
name: string;
};
maximum: string;
mutability_config: {
description: boolean;
maximum: boolean;
properties: boolean;
royalty: boolean;
uri: boolean;
};
name: string;
property_keys: [string];
property_types: [string];
property_values: [string];
royalty_payee_address: string;
royalty_points_denominator: string;
royalty_points_numerator: string;
uri: string;
};
};
export type DepositEvent = {
version: string;
guid: {
creation_number: string;
account_address: string;
};
sequence_number: string;
type: "0x3::token::DepositEvent";
data: {
amount: string;
id: RawTokenId;
};
};
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

@ -1,52 +1,89 @@
import { beforeAll, describe, expect, jest, test } from "@jest/globals";
import {
afterAll,
beforeEach,
describe,
expect,
jest,
test,
} from "@jest/globals";
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 {
CHAIN_ID_APTOS,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
CONTRACTS,
deriveResourceAccountAddress,
deriveTokenHashFromTokenId,
generateSignAndSubmitEntryFunction,
tryNativeToHexString,
tryNativeToUint8Array,
} from "../../utils";
import { parseNftTransferVaa } from "../../vaa";
import { getForeignAssetAptos } from "../getForeignAsset";
import { getIsTransferCompletedAptos } from "../getIsTransferCompleted";
import { getIsWrappedAssetAptos } from "../getIsWrappedAsset";
import { getOriginalAssetAptos } from "../getOriginalAsset";
import { redeemOnAptos } from "../redeem";
import { transferFromAptos, transferFromEth } from "../transfer";
import { redeemOnAptos, redeemOnEth } from "../redeem";
import {
transferFromAptos,
transferFromEth,
transferFromSolana,
} from "../transfer";
import {
APTOS_FAUCET_URL,
APTOS_NODE_URL,
ETH_NODE_URL,
ETH_PRIVATE_KEY,
SOLANA_HOST,
SOLANA_PRIVATE_KEY,
TEST_SOLANA_TOKEN,
} from "./consts";
import { deployTestNftOnEthereum } from "./utils/deployTestNft";
import { getSignedVaaAptos, getSignedVaaEthereum } from "./utils/getSignedVaa";
import {
deployTestNftOnAptos,
deployTestNftOnEthereum,
} from "./utils/deployTestNft";
import {
getSignedVaaAptos,
getSignedVaaEthereum,
getSignedVaaSolana,
} from "./utils/getSignedVaa";
jest.setTimeout(60000);
const APTOS_NFT_BRIDGE_ADDRESS = CONTRACTS.DEVNET.aptos.nft_bridge;
const ETH_NFT_BRIDGE_ADDRESS = CONTRACTS.DEVNET.ethereum.nft_bridge;
const SOLANA_NFT_BRIDGE_ADDRESS = CONTRACTS.DEVNET.solana.nft_bridge;
const SOLANA_CORE_BRIDGE_ADDRESS = CONTRACTS.DEVNET.solana.core;
// aptos setup
let aptosClient: AptosClient;
let aptosAccount: AptosAccount;
let web3: Web3;
let ethProvider: ethers.providers.WebSocketProvider;
let ethSigner: ethers.Wallet;
let faucet: FaucetClient;
beforeAll(async () => {
// aptos setup
// ethereum setup
const web3 = new Web3(ETH_NODE_URL);
const ethProvider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
const ethSigner = new ethers.Wallet(ETH_PRIVATE_KEY, ethProvider); // corresponds to accounts[1]
// solana setup
const solanaConnection = new Connection(SOLANA_HOST, "confirmed");
const solanaKeypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
const solanaPayerAddress = solanaKeypair.publicKey.toString();
beforeEach(async () => {
aptosClient = new AptosClient(APTOS_NODE_URL);
aptosAccount = new AptosAccount();
const faucet = new FaucetClient(APTOS_NODE_URL, APTOS_FAUCET_URL);
faucet = new FaucetClient(APTOS_NODE_URL, APTOS_FAUCET_URL);
await faucet.fundAccount(aptosAccount.address(), 100_000_000);
});
// ethereum setup
web3 = new Web3(ETH_NODE_URL);
ethProvider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
ethSigner = new ethers.Wallet(ETH_PRIVATE_KEY, ethProvider); // corresponds to accounts[1]
afterAll(async () => {
(web3.currentProvider as any).disconnect();
await ethProvider.destroy();
});
describe("Aptos NFT SDK tests", () => {
@ -100,25 +137,26 @@ describe("Aptos NFT SDK tests", () => {
)
).toBe(true);
// get creator address
const creatorAddress = await deriveResourceAccountAddress(
// get token data
const tokenData = await getForeignAssetAptos(
aptosClient,
APTOS_NFT_BRIDGE_ADDRESS,
CHAIN_ID_ETH,
ethNft.address
tryNativeToUint8Array(ethNft.address, CHAIN_ID_ETH)
);
expect(creatorAddress).toBeTruthy();
assertIsNotNull(tokenData);
expect(
await getIsWrappedAssetAptos(
aptosClient,
APTOS_NFT_BRIDGE_ADDRESS,
creatorAddress!
tokenData.creatorAddress
)
).toBe(true);
expect(
await getOriginalAssetAptos(
aptosClient,
APTOS_NFT_BRIDGE_ADDRESS,
creatorAddress!
tokenData.creatorAddress
)
).toMatchObject({
isWrapped: true,
@ -129,9 +167,9 @@ describe("Aptos NFT SDK tests", () => {
// transfer NFT from Aptos back to Ethereum
const aptosTransferPayload = await transferFromAptos(
APTOS_NFT_BRIDGE_ADDRESS,
creatorAddress!,
ethTransferVaaParsed.name, // TODO(aki): derive this properly
ethTransferVaaParsed.tokenId.toString(16).padStart(64, "0"), // TODO(aki): derive this properly
tokenData.creatorAddress,
tokenData.collectionName,
tokenData.tokenName.padStart(64, "0"),
0,
CHAIN_ID_ETH,
tryNativeToUint8Array(ethSigner.address, CHAIN_ID_ETH)
@ -147,32 +185,197 @@ describe("Aptos NFT SDK tests", () => {
)) as Types.UserTransaction;
expect(aptosTransferTxResult.success).toBe(true);
// observe tx and get vaa
let aptosTransferVaa = await getSignedVaaAptos(
aptosClient,
aptosTransferTxResult
);
const aptosTransferVaaParsed = parseNftTransferVaa(aptosTransferVaa);
expect(aptosTransferVaaParsed.name).toBe(ETH_COLLECTION_NAME);
expect(aptosTransferVaaParsed.tokenAddress.toString("hex")).toBe(
ethTransferVaaParsed.tokenAddress.toString("hex")
);
// redeem NFT on Ethereum
const ethRedeemTxResult = await redeemOnEth(
ETH_NFT_BRIDGE_ADDRESS,
ethSigner,
aptosTransferVaa
);
expect(ethRedeemTxResult.status).toBe(1);
});
test("Transfer Aptos native token to Ethereum and back", async () => {
const APTOS_COLLECTION_NAME = "Not an APE 🦧";
// mint NFT on Aptos
const aptosNftMintTxResult = await deployTestNftOnAptos(
aptosClient,
aptosAccount,
APTOS_COLLECTION_NAME,
"APE🦧"
);
// get token data from user wallet
const event = (
await aptosClient.getEventsByEventHandle(
aptosAccount.address(),
"0x3::token::TokenStore",
"deposit_events",
{ limit: 1 } // most users will more than one deposit event
)
)[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);
// transfer NFT from Aptos to Solana
const aptosTransferPayload = await transferFromAptos(
APTOS_NFT_BRIDGE_ADDRESS,
tokenId.creatorAddress,
tokenId.collectionName,
tokenId.tokenName,
tokenId.propertyVersion,
CHAIN_ID_ETH,
tryNativeToUint8Array(ethSigner.address, CHAIN_ID_ETH)
);
const aptosTransferTx = await generateSignAndSubmitEntryFunction(
aptosClient,
aptosAccount,
aptosTransferPayload
);
const aptosTransferTxResult =
(await aptosClient.waitForTransactionWithResult(
aptosTransferTx.hash
)) as Types.UserTransaction;
expect(aptosTransferTxResult.success).toBe(true);
// observe tx and get vaa
const aptosTransferVaa = await getSignedVaaAptos(
aptosClient,
aptosTransferTxResult
);
const aptosTransferVaaParsed = parseNftTransferVaa(aptosTransferVaa);
expect(aptosTransferVaaParsed.name).toBe(ETH_COLLECTION_NAME);
expect(aptosTransferVaaParsed.tokenAddress.toString("hex")).toBe(
ethTransferVaaParsed.tokenAddress.toString("hex")
);
expect(aptosTransferVaaParsed.name).toBe(APTOS_COLLECTION_NAME);
// TODO(aki): make this work
// // redeem NFT on Ethereum & check NFT is the same
// const ethRedeemTxResult = await redeemOnEth(
// redeem NFT on Ethereum
const ethRedeemTx = await redeemOnEth(
ETH_NFT_BRIDGE_ADDRESS,
ethSigner,
aptosTransferVaa
);
console.log(JSON.stringify(ethRedeemTx, null, 2));
expect(ethRedeemTx.status).toBe(1);
// get token address on Ethereum
const tokenHash = await deriveTokenHashFromTokenId(tokenId);
expect(
await getForeignAssetAptos(
aptosClient,
APTOS_NFT_BRIDGE_ADDRESS,
CHAIN_ID_APTOS,
tokenHash
)
).toMatchObject(tokenId);
// const tokenAddress = await getForeignAssetEth(
// ETH_NFT_BRIDGE_ADDRESS,
// ethSigner,
// aptosTransferVaa,
// { gasLimit: 3e7 }
// CHAIN_ID_APTOS,
// tokenHash
// );
// console.log(
// "ethRedeemTxResult",
// JSON.stringify(ethRedeemTxResult, null, 2)
// 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(ethRedeemTxResult.status).toBe(1);
// expect(ethTransferTx.status).toBe(1);
// // observe tx and get vaa
// // redeem NFT on Aptos
// check NFT is the same
});
test("Transfer Solana SPL to Aptos", async () => {
// transfer SPL token to Aptos
const fromAddress = await getAssociatedTokenAddress(
new PublicKey(TEST_SOLANA_TOKEN),
solanaKeypair.publicKey
);
const solanaTransferTx = await transferFromSolana(
solanaConnection,
SOLANA_CORE_BRIDGE_ADDRESS,
SOLANA_NFT_BRIDGE_ADDRESS,
solanaPayerAddress,
fromAddress.toString(),
TEST_SOLANA_TOKEN,
tryNativeToUint8Array(aptosAccount.address().toString(), CHAIN_ID_APTOS),
CHAIN_ID_APTOS
);
solanaTransferTx.partialSign(solanaKeypair);
const txid = await solanaConnection.sendRawTransaction(
solanaTransferTx.serialize()
);
await solanaConnection.confirmTransaction(txid);
const solanaTransferTxResult = await solanaConnection.getTransaction(txid);
assertIsNotNull(solanaTransferTxResult);
// observe tx and get vaa
const solanaTransferVaa = await getSignedVaaSolana(solanaTransferTxResult);
const solanaTransferVaaParsed = parseNftTransferVaa(solanaTransferVaa);
// redeem SPL on Aptos
const aptosRedeemTxPayload = await redeemOnAptos(
APTOS_NFT_BRIDGE_ADDRESS,
solanaTransferVaa
);
const aptosRedeemTx = await generateSignAndSubmitEntryFunction(
aptosClient,
aptosAccount,
aptosRedeemTxPayload
);
const aptosRedeemTxResult = (await aptosClient.waitForTransactionWithResult(
aptosRedeemTx.hash
)) as Types.UserTransaction;
expect(aptosRedeemTxResult.success).toBe(true);
// check if token is in SPL cache
const tokenData = await getForeignAssetAptos(
aptosClient,
APTOS_NFT_BRIDGE_ADDRESS,
CHAIN_ID_SOLANA,
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
// check if token is in user's account
const events = (await aptosClient.getEventsByEventHandle(
aptosAccount.address(),
"0x3::token::TokenStore",
"deposit_events",
{ limit: 1 }
)) as DepositEvent[];
expect(events.length).toBe(1);
expect(events[0].data.id.token_data_id.name).toBe(
tryNativeToHexString(TEST_SOLANA_TOKEN, CHAIN_ID_SOLANA)
);
});
});
function afterAll(arg0: () => Promise<void>) {
throw new Error("Function not implemented.");
}
// https://github.com/microsoft/TypeScript/issues/34523
const assertIsNotNull: <T>(x: T | null) => asserts x is T = (x) => {
expect(x).not.toBeNull();
};

View File

@ -1,4 +1,4 @@
import { AptosAccount, AptosClient, TokenClient } from "aptos";
import { AptosAccount, AptosClient, TokenClient, Types } from "aptos";
import { ethers } from "ethers";
import Web3 from "web3";
import {
@ -9,14 +9,15 @@ const ERC721 = require("@openzeppelin/contracts/build/contracts/ERC721PresetMint
export const deployTestNftOnAptos = async (
client: AptosClient,
account: AptosAccount
account: AptosAccount,
collectionName: string,
tokenName: string
) => {
const tokenClient = new TokenClient(client);
const collectionName = "testCollection";
const collectionHash = await tokenClient.createCollection(
account,
collectionName,
"test collection",
"collection description",
"https://www.wormhole.com"
);
await client.waitForTransaction(collectionHash);
@ -24,12 +25,14 @@ export const deployTestNftOnAptos = async (
const tokenHash = await tokenClient.createToken(
account,
collectionName,
"testToken",
"test token",
1,
"https://wormhole.com/static/a9281881f58cc7fbe4df796a9ba684ac/90d9d/s2.webp"
tokenName,
"token description",
10,
"https://www.wormhole.com"
);
await client.waitForTransaction(tokenHash);
return client.waitForTransactionWithResult(
tokenHash
) as Promise<Types.UserTransaction>;
};
export async function deployTestNftOnEthereum(

View File

@ -6,7 +6,12 @@ import { ethers } from "ethers";
import { isBytes } from "ethers/lib/utils";
import { fromUint8Array } from "js-base64";
import { CHAIN_ID_SOLANA } from "..";
import { CreateTokenDataEvent, NftBridgeState, TokenId } from "../aptos/types";
import {
CreateTokenDataEvent,
NftBridgeState,
RawTokenId,
TokenId,
} from "../aptos/types";
import { NFTBridge__factory } from "../ethers-contracts";
import { deriveWrappedMintKey } from "../solana/nftBridge";
import {
@ -15,7 +20,6 @@ import {
CHAIN_ID_APTOS,
coalesceChainId,
deriveResourceAccountAddress,
tryNativeToUint8Array,
} from "../utils";
/**
@ -119,14 +123,14 @@ export const getForeignAssetSol = getForeignAssetSolana;
* @param client
* @param nftBridgeAddress
* @param originChain
* @param originAddress 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(
client: AptosClient,
nftBridgeAddress: string,
originChain: ChainId | ChainName,
originAddress: string
originAddress: Uint8Array
): Promise<TokenId | null> {
const originChainId = coalesceChainId(originChain);
if (originChainId === CHAIN_ID_APTOS) {
@ -137,17 +141,19 @@ export async function getForeignAssetAptos(
)
).data as NftBridgeState;
const handle = state.native_infos.handle;
const value = await client.getTableItem(handle, {
const value = (await client.getTableItem(handle, {
key_type: `${nftBridgeAddress}::token_hash::TokenHash`,
value_type: `0x1::token::TokenId`,
value_type: `0x3::token::TokenId`,
key: {
hash: HexString.fromUint8Array(
tryNativeToUint8Array(originAddress, CHAIN_ID_APTOS)
).hex(),
hash: HexString.fromUint8Array(originAddress).hex(),
},
});
console.log("value", JSON.stringify(value, null, 2));
return null;
})) 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 creatorAddress = await deriveResourceAccountAddress(

View File

@ -1,5 +1,5 @@
import { Commitment, Connection, PublicKeyInitData } from "@solana/web3.js";
import { AptosClient } from "aptos";
import { AptosClient, Types } from "aptos";
import { ethers } from "ethers";
import { Bridge__factory } from "../ethers-contracts";
import { getWrappedMeta } from "../solana/nftBridge";
@ -45,7 +45,6 @@ export async function getIsWrappedAssetSolana(
export const getIsWrappedAssetSol = getIsWrappedAssetSolana;
// TODO(aki): only catch expected error
export async function getIsWrappedAssetAptos(
client: AptosClient,
nftBridgeAddress: string,
@ -57,7 +56,11 @@ export async function getIsWrappedAssetAptos(
`${nftBridgeAddress}::state::OriginInfo`
);
return true;
} catch {
return false;
} catch (e) {
if (e instanceof Types.ApiError && e.status === 404) {
return false;
}
throw e;
}
}

View File

@ -5,7 +5,7 @@ import {
PublicKeyInitData,
} from "@solana/web3.js";
import { LCDClient } from "@terra-money/terra.js";
import { AptosClient } from "aptos";
import { AptosClient, Types } from "aptos";
import { BigNumber, ethers } from "ethers";
import { arrayify, zeroPad } from "ethers/lib/utils";
import { WormholeWrappedInfo } from "..";
@ -182,8 +182,7 @@ export async function getOriginalAssetTerra(
};
}
// TODO(aki): should this also return tokenId? doesnt seem possible to implement right now
// TODO(aki): only catch expected error
// TODO(aki): should this also return tokenId?
export async function getOriginalAssetAptos(
client: AptosClient,
nftBridgeAddress: string,
@ -205,11 +204,15 @@ export async function getOriginalAssetAptos(
hex(originInfo.token_address.external_address)
),
};
} catch {
return {
isWrapped: false,
chainId: CHAIN_ID_APTOS,
assetAddress: new Uint8Array(hex(creatorAddress)),
};
} catch (e) {
if (e instanceof Types.ApiError && e.status === 404) {
return {
isWrapped: false,
chainId: CHAIN_ID_APTOS,
assetAddress: new Uint8Array(hex(creatorAddress)),
};
}
throw e;
}
}

View File

@ -8,7 +8,7 @@ import {
Transaction,
} from "@solana/web3.js";
import { MsgExecuteContract } from "@terra-money/terra.js";
import { HexString, Types } from "aptos";
import { Types } from "aptos";
import { ethers, Overrides } from "ethers";
import { isBytes } from "ethers/lib/utils";
import {
@ -180,7 +180,6 @@ export function transferFromAptos(
recipient: Uint8Array
): Types.EntryFunctionPayload {
const recipientChainId = coalesceChainId(recipientChain);
console.log(HexString.fromUint8Array(recipient).hex());
return {
function: `${nftBridgeAddress}::transfer_nft::transfer_nft_entry`,
type_arguments: [],

View File

@ -1,7 +1,7 @@
import { AptosAccount, AptosClient, TxnBuilderTypes, Types } from "aptos";
import { AptosAccount, AptosClient, BCS, TxnBuilderTypes, Types } from "aptos";
import { hexZeroPad } from "ethers/lib/utils";
import { sha3_256 } from "js-sha3";
import { TokenBridgeState } from "../aptos/types";
import { TokenBridgeState, TokenId } from "../aptos/types";
import {
ChainId,
ChainName,
@ -9,7 +9,6 @@ import {
coalesceChainId,
ensureHexPrefix,
hex,
tryNativeToUint8Array,
} from "../utils";
/**
@ -247,10 +246,19 @@ export const coalesceModuleAddress = (str: string): string => {
return str.split("::")[0];
};
/**
* 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
* address of that resource account from the given origin chain and address.
* @param nftBridgeAddress
* @param originChain
* @param originAddress External address of NFT on origin chain
* @returns Address of resource account
*/
export const deriveResourceAccountAddress = async (
nftBridgeAddress: string,
originChain: ChainId | ChainName,
originAddress: string
originAddress: Uint8Array
): Promise<string | null> => {
const originChainId = coalesceChainId(originChain);
if (originChainId === CHAIN_ID_APTOS) {
@ -259,10 +267,7 @@ export const deriveResourceAccountAddress = async (
const chainId = Buffer.alloc(2);
chainId.writeUInt16BE(originChainId);
const seed = Buffer.concat([
chainId,
tryNativeToUint8Array(originAddress, originChain),
]);
const seed = Buffer.concat([chainId, Buffer.from(originAddress)]);
const resourceAccountAddress = await AptosAccount.getResourceAccountAddress(
nftBridgeAddress,
seed
@ -270,6 +275,29 @@ export const deriveResourceAccountAddress = async (
return resourceAccountAddress.toString();
};
/**
* 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.
* @param tokenId
* @returns Token hash identifying the token
*/
export const deriveTokenHashFromTokenId = async (
tokenId: TokenId
): Promise<Uint8Array> => {
const propertyVersion = Buffer.alloc(8);
propertyVersion.writeBigUInt64BE(BigInt(tokenId.propertyVersion));
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"),
propertyVersion,
]);
return new Uint8Array(Buffer.from(sha3_256(inputs), "hex"));
};
/**
* Simulates given raw transaction and either returns the resulting transaction that was submitted
* to the mempool, or throws if it fails.