sdk/js: add aptos js sdk

Updated aptos npm package version to use `AptosAccount.getResourceAccountAddress` api in aptos.ts. `getForeignAsset` is also currently unimplemented.
This commit is contained in:
heyitaki 2023-01-19 07:19:18 +00:00 committed by Evan Gray
parent f584f5c739
commit 7c1721d199
12 changed files with 309 additions and 46 deletions

View File

@ -21,7 +21,7 @@
"@terra-money/terra.js": "^3.1.3",
"@xpla/xpla.js": "^0.2.1",
"algosdk": "^1.15.0",
"aptos": "^1.3.16",
"aptos": "1.5.0",
"axios": "^0.24.0",
"bech32": "^2.0.0",
"binary-parser": "^2.2.1",
@ -5233,9 +5233,9 @@
"dev": true
},
"node_modules/aptos": {
"version": "1.3.16",
"resolved": "https://registry.npmjs.org/aptos/-/aptos-1.3.16.tgz",
"integrity": "sha512-LxI4XctQ5VeL+HokjwuGPwsb1fcydLIn4agFXyhn7hSYosTLNRxQ3UIixyP4Fmv6qPBjQVu8hELVSlThQk/EjA==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/aptos/-/aptos-1.5.0.tgz",
"integrity": "sha512-N7OuRtU7IYHkDkNx+4QS3g/QQGCp+36KzYn3oXPmT7Kttfuv+UKliQVdjy3cLmwd/DCQSh9ObTovwdxnHjUn0g==",
"dependencies": {
"@noble/hashes": "1.1.3",
"@scure/bip39": "1.1.0",
@ -9269,6 +9269,7 @@
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.5.tgz",
"integrity": "sha512-HTm14iMQKK2FjFLRTM5lAVcyaUzOnqbPtesFIvREgXpJHdQm8bWS+GkQgIkfaBYRHuCnea7w8UVNfwiAQhlr9A==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
@ -9631,6 +9632,7 @@
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.7.tgz",
"integrity": "sha512-vLt1O5Pp+flcArHGIyKEQq883nBt8nN8tVBcoL0qUXj2XT1n7p70yGIq2VK98I5FdZ1YHc0wk/koOnHjnXWk1Q==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
@ -22480,9 +22482,9 @@
"dev": true
},
"aptos": {
"version": "1.3.16",
"resolved": "https://registry.npmjs.org/aptos/-/aptos-1.3.16.tgz",
"integrity": "sha512-LxI4XctQ5VeL+HokjwuGPwsb1fcydLIn4agFXyhn7hSYosTLNRxQ3UIixyP4Fmv6qPBjQVu8hELVSlThQk/EjA==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/aptos/-/aptos-1.5.0.tgz",
"integrity": "sha512-N7OuRtU7IYHkDkNx+4QS3g/QQGCp+36KzYn3oXPmT7Kttfuv+UKliQVdjy3cLmwd/DCQSh9ObTovwdxnHjUn0g==",
"requires": {
"@noble/hashes": "1.1.3",
"@scure/bip39": "1.1.0",

View File

@ -67,17 +67,17 @@
"dependencies": {
"@certusone/wormhole-sdk-proto-web": "0.0.6",
"@certusone/wormhole-sdk-wasm": "^0.0.1",
"@coral-xyz/borsh": "0.2.6",
"@injectivelabs/networks": "^1.0.73",
"@injectivelabs/sdk-ts": "^1.0.368",
"@injectivelabs/utils": "^1.0.63",
"@coral-xyz/borsh": "0.2.6",
"@project-serum/anchor": "^0.25.0",
"@solana/spl-token": "^0.3.5",
"@solana/web3.js": "^1.66.2",
"@terra-money/terra.js": "^3.1.3",
"@xpla/xpla.js": "^0.2.1",
"algosdk": "^1.15.0",
"aptos": "^1.3.16",
"aptos": "1.5.0",
"axios": "^0.24.0",
"bech32": "^2.0.0",
"binary-parser": "^2.2.1",

View File

@ -1,4 +1,4 @@
export type State = {
export type TokenBridgeState = {
consumed_vaas: {
elems: {
handle: string;
@ -36,3 +36,30 @@ export type OriginInfo = {
number: string; // lol
};
};
export type NftBridgeState = {
consumed_vaas: {
elems: {
handle: string;
};
};
emitter_cap: {
emitter: string;
sequence: string;
};
native_infos: {
handle: string;
};
registered_emitters: {
handle: string;
};
signer_cap: {
account: string;
};
spl_cache: {
handle: string;
};
wrapped_infos: {
handle: string;
};
};

View File

@ -3,9 +3,9 @@ import { decodeAddress, getApplicationAddress } from "algosdk";
import { bech32 } from "bech32";
import {
arrayify,
sha256,
BytesLike,
Hexable,
sha256,
zeroPad,
} from "ethers/lib/utils";
import { deriveWormholeEmitterKey } from "../solana/wormhole";
@ -17,9 +17,7 @@ export function getEmitterAddressEth(
return Buffer.from(zeroPad(arrayify(contractAddress), 32)).toString("hex");
}
export async function getEmitterAddressSolana(
programAddress: PublicKeyInitData
) {
export function getEmitterAddressSolana(programAddress: PublicKeyInitData) {
return deriveWormholeEmitterKey(programAddress).toBuffer().toString("hex");
}

View File

@ -1,13 +1,22 @@
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 { 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 { NFTBridge__factory } from "../ethers-contracts";
import { deriveWrappedMintKey } from "../solana/nftBridge";
import { ChainId, ChainName, coalesceChainId } from "../utils";
import {
ChainId,
ChainName,
CHAIN_ID_APTOS,
coalesceChainId,
deriveResourceAccountAddress,
tryNativeToUint8Array,
} from "../utils";
/**
* Returns a foreign asset address on Ethereum for a provided native chain and asset address, AddressZero if it does not exist
@ -99,3 +108,70 @@ export async function getForeignAssetSolana(
}
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.
*
* 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 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
): Promise<TokenId | null> {
const originChainId = coalesceChainId(originChain);
if (originChainId === CHAIN_ID_APTOS) {
const state = (
await client.getAccountResource(
nftBridgeAddress,
`${nftBridgeAddress}::state::State`
)
).data as NftBridgeState;
const handle = state.native_infos.handle;
const value = await client.getTableItem(handle, {
key_type: `${nftBridgeAddress}::token_hash::TokenHash`,
value_type: `0x1::token::TokenId`,
key: {
hash: HexString.fromUint8Array(
tryNativeToUint8Array(originAddress, CHAIN_ID_APTOS)
).hex(),
},
});
console.log("value", JSON.stringify(value, null, 2));
return null;
}
const creatorAddress = await deriveResourceAccountAddress(
nftBridgeAddress,
originChainId,
originAddress
);
if (!creatorAddress) {
throw new Error("Could not derive creator account address");
}
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
)
)[0] as CreateTokenDataEvent;
const tokenData = event.data.id;
return {
creatorAddress: tokenData.creator,
collectionName: tokenData.collection,
tokenName: tokenData.name,
propertyVersion: 0,
};
}

View File

@ -1,11 +1,13 @@
import { ethers } from "ethers";
import { NFTBridge__factory } from "../ethers-contracts";
import { getSignedVAAHash } from "../bridge";
import { Commitment, Connection, PublicKeyInitData } from "@solana/web3.js";
import { LCDClient } from "@terra-money/terra.js";
import { AptosClient } from "aptos";
import axios from "axios";
import { ethers } from "ethers";
import { redeemOnTerra } from ".";
import { TERRA_REDEEMED_CHECK_WALLET_ADDRESS } from "..";
import { ensureHexPrefix, TERRA_REDEEMED_CHECK_WALLET_ADDRESS } from "..";
import { NftBridgeState } from "../aptos/types";
import { getSignedVAAHash } from "../bridge";
import { NFTBridge__factory } from "../ethers-contracts";
import { getClaim } from "../solana/wormhole";
import { parseVaa, SignedVaa } from "../vaa/wormhole";
@ -73,3 +75,33 @@ export async function getIsTransferCompletedSolana(
commitment
).catch((e) => false);
}
export async function getIsTransferCompletedAptos(
client: AptosClient,
nftBridgeAddress: string,
transferVaa: Uint8Array
): Promise<boolean> {
// get handle
nftBridgeAddress = ensureHexPrefix(nftBridgeAddress);
const state = (
await client.getAccountResource(
nftBridgeAddress,
`${nftBridgeAddress}::state::State`
)
).data as NftBridgeState;
const handle = state.consumed_vaas.elems.handle;
// check if vaa hash is in consumed_vaas
const transferVaaHash = 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: transferVaaHash,
});
return true;
} catch {
return false;
}
}

View File

@ -1,4 +1,5 @@
import { Commitment, Connection, PublicKeyInitData } from "@solana/web3.js";
import { AptosClient } from "aptos";
import { ethers } from "ethers";
import { Bridge__factory } from "../ethers-contracts";
import { getWrappedMeta } from "../solana/nftBridge";
@ -43,3 +44,20 @@ export async function getIsWrappedAssetSolana(
}
export const getIsWrappedAssetSol = getIsWrappedAssetSolana;
// TODO(aki): only catch expected error
export async function getIsWrappedAssetAptos(
client: AptosClient,
nftBridgeAddress: string,
creatorAddress: string
) {
try {
await client.getAccountResource(
creatorAddress,
`${nftBridgeAddress}::state::OriginInfo`
);
return true;
} catch {
return false;
}
}

View File

@ -5,18 +5,22 @@ import {
PublicKeyInitData,
} from "@solana/web3.js";
import { LCDClient } from "@terra-money/terra.js";
import { AptosClient } from "aptos";
import { BigNumber, ethers } from "ethers";
import { arrayify, zeroPad } from "ethers/lib/utils";
import { WormholeWrappedInfo } from "..";
import { OriginInfo } from "../aptos/types";
import { canonicalAddress } from "../cosmos";
import { TokenImplementation__factory } from "../ethers-contracts";
import { getWrappedMeta } from "../solana/nftBridge";
import {
assertChain,
ChainId,
ChainName,
CHAIN_ID_APTOS,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
coalesceChainId,
hex,
} from "../utils";
import { getIsWrappedAssetEth } from "./getIsWrappedAsset";
@ -177,3 +181,35 @@ export async function getOriginalAssetTerra(
assetAddress: zeroPad(canonicalAddress(wrappedAddress), 32),
};
}
// TODO(aki): should this also return tokenId? doesnt seem possible to implement right now
// TODO(aki): only catch expected error
export async function getOriginalAssetAptos(
client: AptosClient,
nftBridgeAddress: string,
creatorAddress: string
): Promise<WormholeWrappedInfo> {
try {
const originInfo = (
await client.getAccountResource(
creatorAddress,
`${nftBridgeAddress}::state::OriginInfo`
)
).data as OriginInfo;
const chainId = Number(originInfo.token_chain.number);
assertChain(chainId);
return {
isWrapped: true,
chainId: chainId,
assetAddress: new Uint8Array(
hex(originInfo.token_address.external_address)
),
};
} catch {
return {
isWrapped: false,
chainId: CHAIN_ID_APTOS,
assetAddress: new Uint8Array(hex(creatorAddress)),
};
}
}

View File

@ -6,15 +6,16 @@ import {
Transaction,
} from "@solana/web3.js";
import { MsgExecuteContract } from "@terra-money/terra.js";
import { Types } from "aptos";
import { ethers, Overrides } from "ethers";
import { fromUint8Array } from "js-base64";
import { CHAIN_ID_SOLANA } from "..";
import { Bridge__factory } from "../ethers-contracts";
import {
createCompleteTransferNativeInstruction,
createCompleteTransferWrappedInstruction,
createCompleteWrappedMetaInstruction,
} from "../solana/nftBridge";
import { CHAIN_ID_APTOS, CHAIN_ID_SOLANA } from "../utils";
import { parseNftTransferVaa, parseVaa, SignedVaa } from "../vaa";
export async function redeemOnEth(
@ -101,3 +102,19 @@ export async function redeemOnTerra(
},
});
}
export async function redeemOnAptos(
nftBridgeAddress: string,
transferVAA: Uint8Array
): Promise<Types.EntryFunctionPayload> {
const parsedVAA = parseNftTransferVaa(transferVAA);
if (parsedVAA.toChain !== CHAIN_ID_APTOS) {
throw new Error("Transfer is not destined for Aptos.");
}
return {
function: `${nftBridgeAddress}::complete_transfer::submit_vaa_and_register_entry`,
type_arguments: [],
arguments: [transferVAA],
};
}

View File

@ -1,4 +1,4 @@
import { createApproveInstruction } from "@solana/spl-token";
import { BN } from "@project-serum/anchor";
import {
Commitment,
Connection,
@ -8,8 +8,9 @@ import {
Transaction,
} from "@solana/web3.js";
import { MsgExecuteContract } from "@terra-money/terra.js";
import { HexString, Types } from "aptos";
import { ethers, Overrides } from "ethers";
import { BN } from "@project-serum/anchor";
import { isBytes } from "ethers/lib/utils";
import {
NFTBridge__factory,
NFTImplementation__factory,
@ -27,7 +28,6 @@ import {
coalesceChainId,
createNonce,
} from "../utils";
import { isBytes } from "ethers/lib/utils";
export async function transferFromEth(
nftBridgeAddress: string,
@ -169,3 +169,29 @@ export async function transferFromTerra(
),
];
}
export function transferFromAptos(
nftBridgeAddress: string,
creatorAddress: string,
collectionName: string,
tokenName: string,
propertyVersion: number,
recipientChain: ChainId | ChainName,
recipient: Uint8Array
): Types.EntryFunctionPayload {
const recipientChainId = coalesceChainId(recipientChain);
console.log(HexString.fromUint8Array(recipient).hex());
return {
function: `${nftBridgeAddress}::transfer_nft::transfer_nft_entry`,
type_arguments: [],
arguments: [
creatorAddress,
collectionName,
tokenName,
propertyVersion,
recipientChainId,
recipient,
createNonce().readUInt32LE(0),
],
};
}

View File

@ -1,32 +1,32 @@
import { ChainGrpcWasmApi } from "@injectivelabs/sdk-ts";
import { Commitment, Connection, PublicKeyInitData } from "@solana/web3.js";
import { LCDClient } from "@terra-money/terra.js";
import { LCDClient as XplaLCDClient } from "@xpla/xpla.js";
import { Algodv2, bigIntToBytes } from "algosdk";
import { AptosClient } from "aptos";
import axios from "axios";
import { ethers } from "ethers";
import { fromUint8Array } from "js-base64";
import { Provider } from "near-api-js/lib/providers";
import { redeemOnTerra } from ".";
import {
ensureHexPrefix,
parseSmartContractStateResponse,
TERRA_REDEEMED_CHECK_WALLET_ADDRESS,
} from "..";
import { getClaim } from "../solana/wormhole";
import {
BITS_PER_KEY,
calcLogicSigAccount,
MAX_BITS,
_parseVAAAlgorand,
} from "../algorand";
import { callFunctionNear } from "../utils/near";
import { TokenBridgeState } from "../aptos/types";
import { getSignedVAAHash } from "../bridge";
import { Bridge__factory } from "../ethers-contracts";
import { parseVaa, SignedVaa } from "../vaa/wormhole";
import { getClaim } from "../solana/wormhole";
import { safeBigIntToNumber } from "../utils/bigint";
import { Provider } from "near-api-js/lib/providers";
import { LCDClient as XplaLCDClient } from "@xpla/xpla.js";
import { State } from "../aptos/types";
import { callFunctionNear } from "../utils/near";
import { parseVaa, SignedVaa } from "../vaa/wormhole";
export async function getIsTransferCompletedEth(
tokenBridgeAddress: string,
@ -279,7 +279,7 @@ export async function getIsTransferCompletedAptos(
tokenBridgeAddress,
`${tokenBridgeAddress}::state::State`
)
).data as State;
).data as TokenBridgeState;
const handle = state.consumed_vaas.elems.handle;
// check if vaa hash is in consumed_vaas

View File

@ -1,14 +1,22 @@
import { AptosAccount, AptosClient, TxnBuilderTypes, Types } from "aptos";
import { hexZeroPad } from "ethers/lib/utils";
import { sha3_256 } from "js-sha3";
import { ChainId, CHAIN_ID_APTOS, ensureHexPrefix, hex } from "../utils";
import { AptosAccount, AptosClient, TxnBuilderTypes, Types } from "aptos";
import { State } from "../aptos/types";
import { TokenBridgeState } from "../aptos/types";
import {
ChainId,
ChainName,
CHAIN_ID_APTOS,
coalesceChainId,
ensureHexPrefix,
hex,
tryNativeToUint8Array,
} from "../utils";
/**
* 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
* 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
@ -46,11 +54,11 @@ export const generateSignAndSubmitEntryFunction = (
};
/**
* 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
* 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
@ -97,7 +105,7 @@ export const generateSignAndSubmitScript = async (
* @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
* asset's fully qualified type
* @returns The fully qualified type on Aptos for the given asset
*/
export const getAssetFullyQualifiedType = (
@ -144,7 +152,7 @@ export const getForeignAssetAddress = (
}
// from https://github.com/aptos-labs/aptos-core/blob/25696fd266498d81d346fe86e01c330705a71465/aptos-move/framework/aptos-framework/sources/account.move#L90-L95
let DERIVE_RESOURCE_ACCOUNT_SCHEME = Buffer.alloc(1);
const DERIVE_RESOURCE_ACCOUNT_SCHEME = Buffer.alloc(1);
DERIVE_RESOURCE_ACCOUNT_SCHEME.writeUInt8(255);
let chain: Buffer = Buffer.alloc(2);
@ -200,7 +208,7 @@ export async function getTypeFromExternalAddress(
tokenBridgeAddress,
`${tokenBridgeAddress}::state::State`
)
).data as State;
).data as TokenBridgeState;
const handle = state.native_infos.handle;
try {
@ -239,6 +247,29 @@ export const coalesceModuleAddress = (str: string): string => {
return str.split("::")[0];
};
export const deriveResourceAccountAddress = async (
nftBridgeAddress: string,
originChain: ChainId | ChainName,
originAddress: string
): Promise<string | null> => {
const originChainId = coalesceChainId(originChain);
if (originChainId === CHAIN_ID_APTOS) {
return null;
}
const chainId = Buffer.alloc(2);
chainId.writeUInt16BE(originChainId);
const seed = Buffer.concat([
chainId,
tryNativeToUint8Array(originAddress, originChain),
]);
const resourceAccountAddress = await AptosAccount.getResourceAccountAddress(
nftBridgeAddress,
seed
);
return resourceAccountAddress.toString();
};
/**
* Simulates given raw transaction and either returns the resulting transaction that was submitted
* to the mempool, or throws if it fails.