diff --git a/sdk/js/src/aptos/types.ts b/sdk/js/src/aptos/types.ts index 7fe1e1a35..c47fa5126 100644 --- a/sdk/js/src/aptos/types.ts +++ b/sdk/js/src/aptos/types.ts @@ -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; +}; diff --git a/sdk/js/src/nft_bridge/__tests__/aptos-integration.ts b/sdk/js/src/nft_bridge/__tests__/aptos-integration.ts index 511da5b2b..dab3163b3 100644 --- a/sdk/js/src/nft_bridge/__tests__/aptos-integration.ts +++ b/sdk/js/src/nft_bridge/__tests__/aptos-integration.ts @@ -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) { - throw new Error("Function not implemented."); -} + +// https://github.com/microsoft/TypeScript/issues/34523 +const assertIsNotNull: (x: T | null) => asserts x is T = (x) => { + expect(x).not.toBeNull(); +}; diff --git a/sdk/js/src/nft_bridge/__tests__/utils/deployTestNft.ts b/sdk/js/src/nft_bridge/__tests__/utils/deployTestNft.ts index 301509c7e..461839362 100644 --- a/sdk/js/src/nft_bridge/__tests__/utils/deployTestNft.ts +++ b/sdk/js/src/nft_bridge/__tests__/utils/deployTestNft.ts @@ -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; }; export async function deployTestNftOnEthereum( diff --git a/sdk/js/src/nft_bridge/getForeignAsset.ts b/sdk/js/src/nft_bridge/getForeignAsset.ts index d8e1a7b1d..a71e20011 100644 --- a/sdk/js/src/nft_bridge/getForeignAsset.ts +++ b/sdk/js/src/nft_bridge/getForeignAsset.ts @@ -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 { 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( diff --git a/sdk/js/src/nft_bridge/getIsWrappedAsset.ts b/sdk/js/src/nft_bridge/getIsWrappedAsset.ts index 097368276..428203b39 100644 --- a/sdk/js/src/nft_bridge/getIsWrappedAsset.ts +++ b/sdk/js/src/nft_bridge/getIsWrappedAsset.ts @@ -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; } } diff --git a/sdk/js/src/nft_bridge/getOriginalAsset.ts b/sdk/js/src/nft_bridge/getOriginalAsset.ts index a32472893..a61686da3 100644 --- a/sdk/js/src/nft_bridge/getOriginalAsset.ts +++ b/sdk/js/src/nft_bridge/getOriginalAsset.ts @@ -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; } } diff --git a/sdk/js/src/nft_bridge/transfer.ts b/sdk/js/src/nft_bridge/transfer.ts index a53445bbe..bcff06c7c 100644 --- a/sdk/js/src/nft_bridge/transfer.ts +++ b/sdk/js/src/nft_bridge/transfer.ts @@ -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: [], diff --git a/sdk/js/src/utils/aptos.ts b/sdk/js/src/utils/aptos.ts index 9f0c61580..57f137f43 100644 --- a/sdk/js/src/utils/aptos.ts +++ b/sdk/js/src/utils/aptos.ts @@ -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 => { 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 => { + 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.