浏览代码

sdk/js: aptos (#1736)

* sdk/js: aptos

* sdk/aptos: change api interface to be more flexible

sdk/aptos: add attestToken

sdk/aptos: added createdWrapped

sdk/aptos: add getForeignAsset

sdk/aptos: stricter sanity check for fully qualified type

sdk/aptos: ensure addresses are left padded

sdk/aptos: check if asset exists in getForeignAsset

sdk/aptos: stricter sanity check - hex prefix can't be capital

sdk/aptos: add updatewrapped

sdk/aptos: update readme with token attestation example

sdk/aptos: added transfer

sdk/aptos: add getIsTransferCompleted

sdk/aptos: add isWrappedAsset and getOriginalAsset

sdk/aptos: add redeem

sdk/aptos: make init tokenbridge entry func

* sdk/aptos: separated signing/submitting txs from creating raw txs

* clients/js: hash aptos fully qualified type to get address

* sdk/aptos: return payload from api instead of rawtx

* sdk/aptos: derive token info from vaa

* sdk/aptos: fix getAssetFullyQualifiedType for native asset

* sdk/aptos: add min gas price

* sdk/aptos: bump aptos version

* sdk/aptos: dont require 0x in front of addresses

* sdk/aptos: progress on e2e tests

* sdk/aptos: upgrade resource address derivation

This was changed recently
https://github.com/aptos-labs/aptos-core/blob/25696fd266498d81d346fe86e01c330705a71465/aptos-move/framework/aptos-framework/sources/account.move#L90-L95

* sdk/js: fix getForeignAssetAptos

* sdk/js: update testnet aptos address

* sdk/js: update aptos entry functions

* sdk/aptos: fix parsesequencefromlog

* sdk/aptos: throw errors instead of string literal

* sdk/aptos: update testnet/mainnet addresses

* sdk/aptos: fix  completeTransfer and getOriginalAsset

* sdk/aptos: update transferTokens to take in type and remove wormholeFee param

* sdk/aptos: add typeToExternalAddress utility

* sdk/js: update parseSequenceFromLogAptos

* sdk/js: test version bump again

* sdk/aptos: make transfer param type consistent

* sdk/aptos: test transfer to another chain test done

* sdk/aptos: use completeTransferAndRegister

* sdk/aptos: allow tryNativeToHexString to take in account addresses

* sdk/aptos: finish e2e tests

* sdk/aptos: test all apis

* sdk/aptos: add registerCoin utility

* sdk/js: utility to submit script bytecode to chain

* sdk/aptos: update test to be idempotent

* sdk/aptos: stricter check on aptos type

* clients/js: remove unused imports from rebase

* sdk/aptos: change node and faucet urls in ci

Co-authored-by: aki <akshath@live.com>
Co-authored-by: Evan Gray <battledingo@gmail.com>
Csongor Kiss 3 年之前
父节点
当前提交
eaa5107b33

+ 25 - 13
clients/js/README.md

@@ -1,6 +1,7 @@
 # Wormhole CLI
 
 This tool is a command line interface to Wormhole.
+
 ## Installation
 
     make install
@@ -12,7 +13,7 @@ private keys, based on `.env.sample` in this folder.
 
 ## Usage
 
-``` sh
+```sh
 worm [command]
 
 Commands:
@@ -31,28 +32,41 @@ Options:
   --version  Show version number                                       [boolean]
 ```
 
- Consult the `--help` flag for using subcommands.
+Consult the `--help` flag for using subcommands.
 
- ### VAA generation
+### VAA generation
 
- Use `generate` to create VAAs for testing. For example, to create an NFT bridge registration VAA:
+Use `generate` to create VAAs for testing. For example, to create an NFT bridge registration VAA:
 
-``` sh
+```sh
 $ worm generate registration --module NFTBridge \
     --chain bsc \
     --contract-address 0x706abc4E45D419950511e474C7B9Ed348A4a716c \
     --guardian-secret cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0
 ```
 
+Example creating a token attestation VAA:
+
+```sh
+$ worm generate attestation --emitter-chain ethereum \
+    --emitter-address 11111111111111111111111111111115 \
+    --chain ethereum \
+    --token-address 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
+    --decimals 6 \
+    --symbol USDC \
+    --name USDC \
+    --guardian-secret cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0
+```
+
 ### VAA parsing
 
 Use `parse` to parse a VAA into JSON. For example,
 
     worm parse $(worm-fetch-governance 13940208096455381020)
-    
+
 will fetch governance VAA `13940208096455381020` and print it as JSON.
-    
-``` sh
+
+```sh
 # ...signatures elided
 timestamp: 1651416474,
 nonce: 1570649151,
@@ -97,12 +111,11 @@ what's the destination chain and module. For example, a contract upgrade contain
 
     worm submit $(cat my-nft-registration.txt) --network mainnet
 
-
 For VAAs that don't have a specific target chain (like registrations or guardian
 set upgrades), the script will ask you to specify the target chain.
 For example, to submit a guardian set upgrade on all chains, simply run:
 
-``` sh
+```sh
 $ worm-fetch-governance 13940208096455381020 > guardian-upgrade.txt
 $ worm submit $(cat guardian-upgrade.txt) --network mainnet --chain oasis
 $ worm submit $(cat guardian-upgrade.txt) --network mainnet --chain aurora
@@ -121,12 +134,11 @@ $ worm submit $(cat guardian-upgrade.txt) --network mainnet --chain celo
 
 The VAA payload type (guardian set upgrade) specifies that this VAA should go to the core bridge, and the tool directs it there.
 
-
 ### info
 
 To get info about a contract (only EVM supported at this time)
 
-``` sh
+```sh
 $ worm evm info -c bsc -n mainnet -m TokenBridge
 
 {
@@ -169,6 +181,7 @@ $ worm evm info -c bsc -n mainnet -m TokenBridge
 }
 
 ```
+
 ### Misc
 
 To get the contract address for a module:
@@ -178,4 +191,3 @@ To get the contract address for a module:
 To get the RPC address for a chain
 
     $ worm rpc mainnet bsc
-

+ 6 - 2
clients/js/main.ts

@@ -27,6 +27,7 @@ import { impossible, Payload, serialiseVAA, VAA } from "./vaa";
 import { ethers } from "ethers";
 import { NETWORKS } from "./networks";
 import base58 from "bs58";
+import { sha3_256 } from "js-sha3";
 import { isOutdated } from "./cmds/update";
 import { setDefaultWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm";
 import { assertChain, assertEVMChain, ChainName, CHAINS, CONTRACTS as SDK_CONTRACTS, isCosmWasmChain, isEVMChain, isTerraChain, toChainId, toChainName } from "@certusone/wormhole-sdk/lib/cjs/utils/consts";
@@ -923,8 +924,11 @@ function parseAddress(chain: ChainName, address: string): string {
   } else if (chain === "sui") {
     throw Error("SUI is not supported yet");
   } else if (chain === "aptos") {
-    // TODO: is there a better native format for aptos?
-    return "0x" + evm_address(address);
+    if (/^(0x)?[0-9a-fA-F]+$/.test(address)) {
+      return "0x" + evm_address(address);
+    }
+    
+    return sha3_256(Buffer.from(address)); // address is hash of fully qualified type
   } else if (chain === "wormholechain" || (chain + "") == "wormchain") {
     // TODO: update this condition after ChainName is updated to remove "wormholechain"
     const sdk = require("@certusone/wormhole-sdk/lib/cjs/utils/array")

+ 117 - 8
sdk/js/package-lock.json

@@ -17,6 +17,7 @@
         "@terra-money/terra.js": "^3.1.3",
         "@xpla/xpla.js": "^0.2.1",
         "algosdk": "^1.15.0",
+        "aptos": "^1.3.16",
         "axios": "^0.24.0",
         "bech32": "^2.0.0",
         "js-base64": "^3.6.1",
@@ -2496,10 +2497,9 @@
       }
     },
     "node_modules/@noble/hashes": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz",
-      "integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==",
-      "dev": true,
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.3.tgz",
+      "integrity": "sha512-CE0FCR57H2acVI5UOzIGSSIYxZ6v/HOhDR0Ro9VLyhnzLwx0o8W1mmgaqlEUx4049qJDlIBRztv5k+MM8vbO3A==",
       "funding": [
         {
           "type": "individual",
@@ -2568,6 +2568,32 @@
       "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
       "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
     },
+    "node_modules/@scure/base": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
+      "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://paulmillr.com/funding/"
+        }
+      ]
+    },
+    "node_modules/@scure/bip39": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.0.tgz",
+      "integrity": "sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://paulmillr.com/funding/"
+        }
+      ],
+      "dependencies": {
+        "@noble/hashes": "~1.1.1",
+        "@scure/base": "~1.1.0"
+      }
+    },
     "node_modules/@sindresorhus/is": {
       "version": "0.14.0",
       "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
@@ -3353,6 +3379,43 @@
         "node": ">= 8"
       }
     },
+    "node_modules/aptos": {
+      "version": "1.3.16",
+      "resolved": "https://registry.npmjs.org/aptos/-/aptos-1.3.16.tgz",
+      "integrity": "sha512-LxI4XctQ5VeL+HokjwuGPwsb1fcydLIn4agFXyhn7hSYosTLNRxQ3UIixyP4Fmv6qPBjQVu8hELVSlThQk/EjA==",
+      "dependencies": {
+        "@noble/hashes": "1.1.3",
+        "@scure/bip39": "1.1.0",
+        "axios": "0.27.2",
+        "form-data": "4.0.0",
+        "tweetnacl": "1.0.3"
+      },
+      "engines": {
+        "node": ">=11.0.0"
+      }
+    },
+    "node_modules/aptos/node_modules/axios": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
+      "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
+      "dependencies": {
+        "follow-redirects": "^1.14.9",
+        "form-data": "^4.0.0"
+      }
+    },
+    "node_modules/aptos/node_modules/form-data": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+      "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/argparse": {
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
@@ -15368,10 +15431,9 @@
       }
     },
     "@noble/hashes": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz",
-      "integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==",
-      "dev": true
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.3.tgz",
+      "integrity": "sha512-CE0FCR57H2acVI5UOzIGSSIYxZ6v/HOhDR0Ro9VLyhnzLwx0o8W1mmgaqlEUx4049qJDlIBRztv5k+MM8vbO3A=="
     },
     "@openzeppelin/contracts": {
       "version": "4.2.0",
@@ -15433,6 +15495,20 @@
       "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
       "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
     },
+    "@scure/base": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
+      "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="
+    },
+    "@scure/bip39": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.0.tgz",
+      "integrity": "sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w==",
+      "requires": {
+        "@noble/hashes": "~1.1.1",
+        "@scure/base": "~1.1.0"
+      }
+    },
     "@sindresorhus/is": {
       "version": "0.14.0",
       "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
@@ -16092,6 +16168,39 @@
         "picomatch": "^2.0.4"
       }
     },
+    "aptos": {
+      "version": "1.3.16",
+      "resolved": "https://registry.npmjs.org/aptos/-/aptos-1.3.16.tgz",
+      "integrity": "sha512-LxI4XctQ5VeL+HokjwuGPwsb1fcydLIn4agFXyhn7hSYosTLNRxQ3UIixyP4Fmv6qPBjQVu8hELVSlThQk/EjA==",
+      "requires": {
+        "@noble/hashes": "1.1.3",
+        "@scure/bip39": "1.1.0",
+        "axios": "0.27.2",
+        "form-data": "4.0.0",
+        "tweetnacl": "1.0.3"
+      },
+      "dependencies": {
+        "axios": {
+          "version": "0.27.2",
+          "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
+          "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
+          "requires": {
+            "follow-redirects": "^1.14.9",
+            "form-data": "^4.0.0"
+          }
+        },
+        "form-data": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+          "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+          "requires": {
+            "asynckit": "^0.4.0",
+            "combined-stream": "^1.0.8",
+            "mime-types": "^2.1.12"
+          }
+        }
+      }
+    },
     "argparse": {
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",

+ 2 - 1
sdk/js/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@certusone/wormhole-sdk",
-  "version": "0.7.2",
+  "version": "0.7.6",
   "description": "SDK for interacting with Wormhole",
   "homepage": "https://wormhole.com",
   "main": "./lib/cjs/index.js",
@@ -66,6 +66,7 @@
     "@terra-money/terra.js": "^3.1.3",
     "@xpla/xpla.js": "^0.2.1",
     "algosdk": "^1.15.0",
+    "aptos": "^1.3.16",
     "axios": "^0.24.0",
     "bech32": "^2.0.0",
     "js-base64": "^3.6.1",

+ 39 - 0
sdk/js/src/aptos/api/common.ts

@@ -0,0 +1,39 @@
+import { Types } from "aptos";
+
+// Contract upgrade
+
+export const authorizeUpgrade = (
+  address: string,
+  vaa: Uint8Array
+): Types.EntryFunctionPayload => {
+  if (!address) throw new Error("Need bridge address.");
+  return {
+    function: `${address}::contract_upgrade::submit_vaa_entry`,
+    type_arguments: [],
+    arguments: [vaa],
+  };
+};
+
+export const upgradeContract = (
+  address: string,
+  metadataSerialized: Uint8Array,
+  code: Array<Uint8Array>
+): Types.EntryFunctionPayload => {
+  if (!address) throw new Error("Need bridge address.");
+  return {
+    function: `${address}::contract_upgrade::upgrade`,
+    type_arguments: [],
+    arguments: [metadataSerialized, code],
+  };
+};
+
+export const migrateContract = (
+  address: string
+): Types.EntryFunctionPayload => {
+  if (!address) throw new Error("Need bridge address.");
+  return {
+    function: `${address}::contract_upgrade::migrate`,
+    type_arguments: [],
+    arguments: [],
+  };
+};

+ 33 - 0
sdk/js/src/aptos/api/coreBridge.ts

@@ -0,0 +1,33 @@
+import { Types } from "aptos";
+import { ChainId } from "../../utils";
+
+// Guardian set upgrade
+
+export const upgradeGuardianSet = (
+  coreBridgeAddress: string,
+  vaa: Uint8Array,
+): Types.EntryFunctionPayload => {
+  if (!coreBridgeAddress) throw new Error("Need core bridge address.");
+  return {
+    function: `${coreBridgeAddress}::guardian_set_upgrade::submit_vaa_entry`,
+    type_arguments: [],
+    arguments: [vaa],
+  };
+};
+
+// Init WH
+
+export const initWormhole = (
+  coreBridgeAddress: string,
+  chainId: ChainId,
+  governanceChainId: number,
+  governanceContract: Uint8Array,
+  initialGuardian: Uint8Array,
+): Types.EntryFunctionPayload => {
+  if (!coreBridgeAddress) throw new Error("Need core bridge address.");
+  return {
+    function: `${coreBridgeAddress}::wormhole::init`,
+    type_arguments: [],
+    arguments: [chainId, governanceChainId, governanceContract, initialGuardian],
+  };
+};

+ 241 - 0
sdk/js/src/aptos/api/tokenBridge.ts

@@ -0,0 +1,241 @@
+import { AptosClient, TxnBuilderTypes, Types } from "aptos";
+import { _parseVAAAlgorand } from "../../algorand";
+import {
+  assertChain,
+  ChainId,
+  ChainName,
+  CHAIN_ID_APTOS,
+  coalesceChainId,
+  getAssetFullyQualifiedType,
+  getTypeFromExternalAddress,
+  hexToUint8Array,
+  isValidAptosType,
+} from "../../utils";
+
+// Attest token
+
+export const attestToken = (
+  tokenBridgeAddress: string,
+  tokenChain: ChainId | ChainName,
+  tokenAddress: string,
+): Types.EntryFunctionPayload => {
+  if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
+  const assetType = getAssetFullyQualifiedType(
+    tokenBridgeAddress,
+    coalesceChainId(tokenChain),
+    tokenAddress,
+  );
+  if (!assetType) throw new Error("Invalid asset address.");
+  
+  return {
+    function: `${tokenBridgeAddress}::attest_token::attest_token_entry`,
+    type_arguments: [assetType],
+    arguments: [],
+  };
+};
+
+// Complete transfer
+
+export const completeTransfer = async (
+  client: AptosClient,
+  tokenBridgeAddress: string,
+  transferVAA: Uint8Array,
+  feeRecipient: string,
+): Promise<Types.EntryFunctionPayload> => {
+  if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
+
+  const parsedVAA = _parseVAAAlgorand(transferVAA);
+  if (!parsedVAA.FromChain || !parsedVAA.Contract || !parsedVAA.ToChain) {
+    throw new Error("VAA does not contain required information");
+  }
+
+  if (parsedVAA.ToChain !== CHAIN_ID_APTOS) {
+    throw new Error("Transfer is not destined for Aptos");
+  }
+  
+  assertChain(parsedVAA.FromChain);
+  const assetType =
+    parsedVAA.FromChain === CHAIN_ID_APTOS
+      ? await getTypeFromExternalAddress(
+          client,
+          tokenBridgeAddress,
+          parsedVAA.Contract
+        )
+      : getAssetFullyQualifiedType(
+          tokenBridgeAddress,
+          coalesceChainId(parsedVAA.FromChain),
+          parsedVAA.Contract
+        );
+  if (!assetType) throw new Error("Invalid asset address.");
+
+  return {
+    function: `${tokenBridgeAddress}::complete_transfer::submit_vaa_entry`,
+    type_arguments: [assetType],
+    arguments: [transferVAA, feeRecipient],
+  };
+};
+
+export const completeTransferAndRegister = async (
+  client: AptosClient,
+  tokenBridgeAddress: string,
+  transferVAA: Uint8Array,
+): Promise<Types.EntryFunctionPayload> => {
+  if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
+
+  const parsedVAA = _parseVAAAlgorand(transferVAA);
+  if (!parsedVAA.FromChain || !parsedVAA.Contract || !parsedVAA.ToChain) {
+    throw new Error("VAA does not contain required information");
+  }
+
+  if (parsedVAA.ToChain !== CHAIN_ID_APTOS) {
+    throw new Error("Transfer is not destined for Aptos");
+  }
+  
+  assertChain(parsedVAA.FromChain);
+  const assetType =
+    parsedVAA.FromChain === CHAIN_ID_APTOS
+      ? await getTypeFromExternalAddress(
+          client,
+          tokenBridgeAddress,
+          parsedVAA.Contract
+        )
+      : getAssetFullyQualifiedType(
+          tokenBridgeAddress,
+          coalesceChainId(parsedVAA.FromChain),
+          parsedVAA.Contract
+        );
+  if (!assetType) throw new Error("Invalid asset address.");
+
+  return {
+    function: `${tokenBridgeAddress}::complete_transfer::submit_vaa_and_register_entry`,
+    type_arguments: [assetType],
+    arguments: [transferVAA],
+  };
+};
+
+export const completeTransferWithPayload = (
+  _tokenBridgeAddress: string,
+  _tokenChain: ChainId | ChainName,
+  _tokenAddress: string,
+  _vaa: Uint8Array,
+): Types.EntryFunctionPayload => {
+  throw new Error("Completing transfers with payload is not yet supported in the sdk");
+};
+
+export const registerCoin = (
+  tokenBridgeAddress: string,
+  originChain: ChainId | ChainName,
+  originAddress: string
+): TxnBuilderTypes.TransactionPayloadScript => {
+  const bytecode = hexToUint8Array(
+    "a11ceb0b050000000601000403041104150405190b072436085a200000000101020002000003020401000004000101000103020301060c000105010900010104636f696e067369676e65720a616464726573735f6f661569735f6163636f756e745f726567697374657265640872656769737465720000000000000000000000000000000000000000000000000000000000000001010000010c0a001100380020030605090b003801050b0b000102"
+  );
+  const assetType = getAssetFullyQualifiedType(
+    tokenBridgeAddress,
+    coalesceChainId(originChain),
+    originAddress
+  );
+  if (!assetType) throw new Error("Asset type is null");
+  const typeTag = new TxnBuilderTypes.TypeTagStruct(
+    TxnBuilderTypes.StructTag.fromString(assetType)
+  );
+
+  return new TxnBuilderTypes.TransactionPayloadScript(
+    new TxnBuilderTypes.Script(bytecode, [typeTag], [])
+  );
+};
+
+// Deploy coin
+
+// don't need `signer` and `&signer` in argument list because the Move VM will inject them
+export const deployCoin = (tokenBridgeAddress: string): Types.EntryFunctionPayload => {
+  if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
+  return {
+    function: `${tokenBridgeAddress}::deploy_coin::deploy_coin`,
+    type_arguments: [],
+    arguments: [],
+  };
+};
+
+// Register chain
+
+export const registerChain = (
+  tokenBridgeAddress: string,
+  vaa: Uint8Array,
+): Types.EntryFunctionPayload => {
+  if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
+  return {
+    function: `${tokenBridgeAddress}::register_chain::submit_vaa_entry`,
+    type_arguments: [],
+    arguments: [vaa],
+  };
+};
+
+// Transfer tokens
+
+export const transferTokens = (
+  tokenBridgeAddress: string,
+  fullyQualifiedType: string,
+  amount: string,
+  recipientChain: ChainId | ChainName,
+  recipient: Uint8Array,
+  relayerFee: string,
+  nonce: number,
+  payload: string = "",
+): Types.EntryFunctionPayload => {
+  if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
+  if (!isValidAptosType(fullyQualifiedType)) {
+    throw new Error("Need fully qualified address");
+  }
+
+  const recipientChainId = coalesceChainId(recipientChain);
+  if (payload) {
+    throw new Error("Transfer with payload are not yet supported in the sdk");
+  } else {
+    return {
+      function: `${tokenBridgeAddress}::transfer_tokens::transfer_tokens_entry`,
+      type_arguments: [fullyQualifiedType],
+      arguments: [amount, recipientChainId, recipient, relayerFee, nonce],
+    };
+  }
+};
+
+// Created wrapped coin
+
+export const createWrappedCoinType = (
+  tokenBridgeAddress: string,
+  vaa: Uint8Array,
+): Types.EntryFunctionPayload => {
+  if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
+  return {
+    function: `${tokenBridgeAddress}::wrapped::create_wrapped_coin_type`,
+    type_arguments: [],
+    arguments: [vaa],
+  };
+};
+
+export const createWrappedCoin = (
+  tokenBridgeAddress: string,
+  attestVAA: Uint8Array
+): Types.EntryFunctionPayload => {
+  if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
+
+  const parsedVAA = _parseVAAAlgorand(attestVAA);
+  if (!parsedVAA.FromChain || !parsedVAA.Contract) {
+    throw new Error("VAA does not contain required information");
+  }
+
+  assertChain(parsedVAA.FromChain);
+  const assetType = getAssetFullyQualifiedType(
+    tokenBridgeAddress,
+    coalesceChainId(parsedVAA.FromChain),
+    parsedVAA.Contract
+  );
+  if (!assetType) throw new Error("Invalid asset address.");
+
+  return {
+    function: `${tokenBridgeAddress}::wrapped::create_wrapped_coin`,
+    type_arguments: [assetType],
+    arguments: [attestVAA],
+  };
+};

+ 3 - 0
sdk/js/src/aptos/index.ts

@@ -0,0 +1,3 @@
+export * from "./api/common";
+export * from "./api/coreBridge";
+export * from "./api/tokenBridge";

+ 38 - 0
sdk/js/src/aptos/types.ts

@@ -0,0 +1,38 @@
+export type State = {
+  consumed_vaas: {
+    elems: {
+      handle: string;
+    };
+  };
+  emitter_cap: {
+    emitter: string;
+    sequence: string;
+  };
+  governance_chain_id: {
+    number: string;
+  };
+  governance_contract: {
+    external_address: string;
+  };
+  native_infos: {
+    handle: string;
+  };
+  registered_emitters: {
+    handle: string;
+  };
+  signer_cap: {
+    account: string;
+  };
+  wrapped_infos: {
+    handle: string;
+  };
+};
+
+export type OriginInfo = {
+  token_address: {
+    external_address: string;
+  };
+  token_chain: {
+    number: string; // lol
+  };
+};

+ 21 - 0
sdk/js/src/bridge/parseSequenceFromLog.ts

@@ -1,6 +1,7 @@
 import { TransactionResponse } from "@solana/web3.js";
 import { TxInfo } from "@terra-money/terra.js";
 import { TxInfo as XplaTxInfo } from "@xpla/xpla.js";
+import { AptosClient, Types } from "aptos";
 import { BigNumber, ContractReceipt } from "ethers";
 import { FinalExecutionOutcome } from "near-api-js/lib/providers";
 import { Implementation__factory } from "../ethers-contracts";
@@ -159,3 +160,23 @@ export function parseSequenceFromLogNear(
   }
   return null;
 }
+
+/**
+ * Given a transaction result, return the first WormholeMessage event sequence
+ * @param coreBridgeAddress Wormhole Core bridge address
+ * @param result the result of client.waitForTransactionWithResult(txHash)
+ * @returns sequence
+ */
+export function parseSequenceFromLogAptos(
+  coreBridgeAddress: string,
+  result: Types.UserTransaction
+): string | null {
+  if (result.success) {
+    const event = result.events.find(
+      (e) => e.type === `${coreBridgeAddress}::state::WormholeMessage`
+    );
+    return event?.data.sequence || null;
+  }
+
+  return null;
+}

+ 410 - 0
sdk/js/src/token_bridge/__tests__/aptos-integration.ts

@@ -0,0 +1,410 @@
+import { describe, expect, jest, test } from "@jest/globals";
+import {
+  AptosAccount,
+  AptosClient,
+  FaucetClient,
+  HexString,
+  Types,
+} from "aptos";
+import {
+  approveEth,
+  APTOS_TOKEN_BRIDGE_EMITTER_ADDRESS,
+  attestFromAptos,
+  attestFromEth,
+  CHAIN_ID_APTOS,
+  CHAIN_ID_ETH,
+  CONTRACTS,
+  createWrappedOnAptos,
+  createWrappedOnEth,
+  createWrappedTypeOnAptos,
+  getAssetFullyQualifiedType,
+  getEmitterAddressEth,
+  getExternalAddressFromType,
+  getForeignAssetAptos,
+  getForeignAssetEth,
+  getIsTransferCompletedAptos,
+  getIsTransferCompletedEth,
+  getIsWrappedAssetAptos,
+  getOriginalAssetAptos,
+  getSignedVAAWithRetry,
+  hexToUint8Array,
+  redeemOnAptos,
+  redeemOnEth,
+  signAndSubmitEntryFunction,
+  signAndSubmitScript,
+  TokenImplementation__factory,
+  transferFromAptos,
+  transferFromEth,
+  tryNativeToHexString,
+  tryNativeToUint8Array,
+  uint8ArrayToHex,
+} from "../..";
+import { setDefaultWasm } from "../../solana/wasm";
+import {
+  APTOS_FAUCET_URL,
+  APTOS_NODE_URL,
+  APTOS_PRIVATE_KEY,
+  ETH_NODE_URL,
+  ETH_PRIVATE_KEY3,
+  TEST_ERC20,
+  WORMHOLE_RPC_HOSTS,
+} from "./consts";
+import {
+  parseSequenceFromLogAptos,
+  parseSequenceFromLogEth,
+} from "../../bridge/parseSequenceFromLog";
+import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
+import { ethers } from "ethers";
+import { parseUnits } from "ethers/lib/utils";
+import { registerCoin } from "../../aptos";
+
+setDefaultWasm("node");
+
+const JEST_TEST_TIMEOUT = 60000;
+jest.setTimeout(JEST_TEST_TIMEOUT);
+
+describe("Aptos SDK tests", () => {
+  test("Transfer native token from Aptos to Ethereum", async () => {
+    // setup aptos
+    const client = new AptosClient(APTOS_NODE_URL);
+    const faucet = new FaucetClient(APTOS_NODE_URL, APTOS_FAUCET_URL);
+    const sender = new AptosAccount(hexToUint8Array(APTOS_PRIVATE_KEY));
+    const aptosTokenBridge = CONTRACTS.DEVNET.aptos.token_bridge;
+    const aptosCoreBridge = CONTRACTS.DEVNET.aptos.core;
+
+    // sanity check funds in the account
+    const COIN_TYPE = "0x1::aptos_coin::AptosCoin";
+    const before = await getBalanceAptos(client, COIN_TYPE, sender.address());
+    await faucet.fundAccount(sender.address(), 100_000_000);
+    const after = await getBalanceAptos(client, COIN_TYPE, sender.address());
+    expect(Number(after) - Number(before)).toEqual(100_000_000);
+
+    // attest native aptos token
+    const attestPayload = attestFromAptos(
+      aptosTokenBridge,
+      CHAIN_ID_APTOS,
+      COIN_TYPE
+    );
+    let tx = (await signAndSubmitEntryFunction(
+      client,
+      sender,
+      attestPayload
+    )) as Types.UserTransaction;
+
+    // get signed attest vaa
+    let sequence = parseSequenceFromLogAptos(aptosCoreBridge, tx);
+    expect(sequence).toBeTruthy();
+
+    const { vaaBytes: attestVAA } = await getSignedVAAWithRetry(
+      WORMHOLE_RPC_HOSTS,
+      CHAIN_ID_APTOS,
+      APTOS_TOKEN_BRIDGE_EMITTER_ADDRESS,
+      sequence!,
+      {
+        transport: NodeHttpTransport(),
+      },
+      1000,
+      5
+    );
+    expect(attestVAA).toBeTruthy();
+
+    // setup ethereum
+    const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
+    const recipient = new ethers.Wallet(ETH_PRIVATE_KEY3, provider);
+    const recipientAddress = await recipient.getAddress();
+    const ethTokenBridge = CONTRACTS.DEVNET.ethereum.token_bridge;
+    try {
+      await createWrappedOnEth(ethTokenBridge, recipient, attestVAA);
+    } catch (e) {
+      // this could fail because the token is already attested (in an unclean env)
+    }
+
+    // check attestation on ethereum
+    const externalAddress = hexToUint8Array(
+      await getExternalAddressFromType(COIN_TYPE)
+    );
+    const address = getForeignAssetEth(
+      ethTokenBridge,
+      provider,
+      CHAIN_ID_APTOS,
+      externalAddress
+    );
+    expect(address).toBeTruthy();
+    expect(address).not.toBe(ethers.constants.AddressZero);
+
+    // transfer from aptos
+    const balanceBeforeTransferAptos = ethers.BigNumber.from(
+      await getBalanceAptos(client, COIN_TYPE, sender.address())
+    );
+    const transferPayload = transferFromAptos(
+      aptosTokenBridge,
+      COIN_TYPE,
+      (10_000_000).toString(),
+      CHAIN_ID_ETH,
+      tryNativeToUint8Array(recipientAddress, CHAIN_ID_ETH)
+    );
+    tx = (await signAndSubmitEntryFunction(
+      client,
+      sender,
+      transferPayload
+    )) as Types.UserTransaction;
+    const balanceAfterTransferAptos = ethers.BigNumber.from(
+      await getBalanceAptos(client, COIN_TYPE, sender.address())
+    );
+    expect(
+      balanceBeforeTransferAptos
+        .sub(balanceAfterTransferAptos)
+        .gt((10_000_000).toString())
+    ).toBe(true);
+
+    // get signed transfer vaa
+    sequence = parseSequenceFromLogAptos(aptosCoreBridge, tx);
+    expect(sequence).toBeTruthy();
+
+    const { vaaBytes: transferVAA } = await getSignedVAAWithRetry(
+      WORMHOLE_RPC_HOSTS,
+      CHAIN_ID_APTOS,
+      APTOS_TOKEN_BRIDGE_EMITTER_ADDRESS,
+      sequence!,
+      {
+        transport: NodeHttpTransport(),
+      },
+      1000,
+      5
+    );
+    expect(transferVAA).toBeTruthy();
+
+    // get balance on eth
+    const originAssetHex = tryNativeToUint8Array(COIN_TYPE, CHAIN_ID_APTOS);
+    if (!originAssetHex) {
+      throw new Error("originAssetHex is null");
+    }
+
+    const foreignAsset = await getForeignAssetEth(
+      ethTokenBridge,
+      provider,
+      CHAIN_ID_APTOS,
+      originAssetHex
+    );
+    if (!foreignAsset) {
+      throw new Error("foreignAsset is null");
+    }
+
+    const balanceBeforeTransferEth = await getBalanceEth(
+      foreignAsset,
+      recipient
+    );
+
+    // redeem on eth
+    await redeemOnEth(ethTokenBridge, recipient, transferVAA);
+    expect(
+      await getIsTransferCompletedEth(ethTokenBridge, provider, transferVAA)
+    ).toBe(true);
+    const balanceAfterTransferEth = await getBalanceEth(
+      foreignAsset,
+      recipient
+    );
+    expect(
+      balanceAfterTransferEth.sub(balanceBeforeTransferEth).toNumber()
+    ).toEqual(10_000_000);
+
+    // clean up
+    provider.destroy();
+  });
+  test("Transfer native ERC-20 from Ethereum to Aptos", async () => {
+    // setup ethereum
+    const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
+    const sender = new ethers.Wallet(ETH_PRIVATE_KEY3, provider);
+    const ethTokenBridge = CONTRACTS.DEVNET.ethereum.token_bridge;
+    const ethCoreBridge = CONTRACTS.DEVNET.ethereum.core;
+
+    // attest from eth
+    const attestReceipt = await attestFromEth(
+      ethTokenBridge,
+      sender,
+      TEST_ERC20
+    );
+
+    // get signed attest vaa
+    let sequence = parseSequenceFromLogEth(attestReceipt, ethCoreBridge);
+    expect(sequence).toBeTruthy();
+
+    const { vaaBytes: attestVAA } = await getSignedVAAWithRetry(
+      WORMHOLE_RPC_HOSTS,
+      CHAIN_ID_ETH,
+      getEmitterAddressEth(ethTokenBridge),
+      sequence,
+      {
+        transport: NodeHttpTransport(),
+      },
+      1000,
+      5
+    );
+    expect(attestVAA).toBeTruthy();
+
+    // setup aptos
+    const client = new AptosClient(APTOS_NODE_URL);
+    const recipient = new AptosAccount(hexToUint8Array(APTOS_PRIVATE_KEY));
+    const aptosTokenBridge = CONTRACTS.DEVNET.aptos.token_bridge;
+    const createWrappedCoinTypePayload = createWrappedTypeOnAptos(
+      aptosTokenBridge,
+      attestVAA
+    );
+    try {
+      await signAndSubmitEntryFunction(
+        client,
+        recipient,
+        createWrappedCoinTypePayload
+      );
+    } catch (e) {
+      // only throw if token has not been attested but this call fails
+      if (
+        !(
+          new Error(e).message.includes("ECOIN_INFO_ALREADY_PUBLISHED") ||
+          new Error(e).message.includes("ERESOURCE_ACCCOUNT_EXISTS")
+        )
+      ) {
+        throw e;
+      }
+    }
+
+    const createWrappedCoinPayload = createWrappedOnAptos(
+      aptosTokenBridge,
+      attestVAA
+    );
+    try {
+      await signAndSubmitEntryFunction(
+        client,
+        recipient,
+        createWrappedCoinPayload
+      );
+    } catch (e) {
+      // only throw if token has not been attested but this call fails
+      if (
+        !(
+          new Error(e).message.includes("ECOIN_INFO_ALREADY_PUBLISHED") ||
+          new Error(e).message.includes("ERESOURCE_ACCCOUNT_EXISTS")
+        )
+      ) {
+        throw e;
+      }
+    }
+
+    // check attestation on aptos
+    const aptosWrappedAddress = await getForeignAssetAptos(
+      client,
+      aptosTokenBridge,
+      CHAIN_ID_ETH,
+      TEST_ERC20
+    );
+    if (!aptosWrappedAddress) {
+      throw new Error("Failed to create wrapped coin on Aptos");
+    }
+
+    const wrappedType = getAssetFullyQualifiedType(
+      aptosTokenBridge,
+      CHAIN_ID_ETH,
+      TEST_ERC20
+    );
+    if (!wrappedType) {
+      throw new Error("wrappedType is null");
+    }
+
+    const info = await getOriginalAssetAptos(
+      client,
+      aptosTokenBridge,
+      wrappedType
+    );
+    expect(uint8ArrayToHex(info.assetAddress)).toEqual(
+      tryNativeToHexString(TEST_ERC20, CHAIN_ID_ETH)
+    );
+    expect(info.chainId).toEqual(CHAIN_ID_ETH);
+    expect(info.isWrapped).toEqual(
+      await getIsWrappedAssetAptos(
+        client,
+        aptosTokenBridge,
+        aptosWrappedAddress
+      )
+    );
+
+    // transfer from eth
+    const balanceBeforeTransferEth = await getBalanceEth(TEST_ERC20, sender);
+    const amount = parseUnits("1", 18);
+    await approveEth(ethTokenBridge, TEST_ERC20, sender, amount);
+    const transferReceipt = await transferFromEth(
+      ethTokenBridge,
+      sender,
+      TEST_ERC20,
+      amount,
+      CHAIN_ID_APTOS,
+      tryNativeToUint8Array(recipient.address().hex(), CHAIN_ID_APTOS)
+    );
+
+    // get signed transfer vaa
+    sequence = parseSequenceFromLogEth(transferReceipt, ethCoreBridge);
+    expect(sequence).toBeTruthy();
+
+    const { vaaBytes: transferVAA } = await getSignedVAAWithRetry(
+      WORMHOLE_RPC_HOSTS,
+      CHAIN_ID_ETH,
+      getEmitterAddressEth(ethTokenBridge),
+      sequence,
+      {
+        transport: NodeHttpTransport(),
+      },
+      1000,
+      5
+    );
+    expect(transferVAA).toBeTruthy();
+    
+    // register token on aptos
+    const script = registerCoin(aptosTokenBridge, CHAIN_ID_ETH, TEST_ERC20);
+    await signAndSubmitScript(client, recipient, script);
+
+    // redeem on aptos
+    const balanceBeforeTransferAptos = ethers.BigNumber.from(
+      await getBalanceAptos(client, wrappedType, recipient.address())
+    );
+    const redeemPayload = await redeemOnAptos(
+      client,
+      aptosTokenBridge,
+      transferVAA
+    );
+    await signAndSubmitEntryFunction(client, recipient, redeemPayload);
+    expect(
+      await getIsTransferCompletedAptos(client, aptosTokenBridge, transferVAA)
+    ).toBe(true);
+
+    // check balances
+    const balanceAfterTransferAptos = ethers.BigNumber.from(
+      await getBalanceAptos(client, wrappedType, recipient.address())
+    );
+    expect(
+      balanceAfterTransferAptos.sub(balanceBeforeTransferAptos).toString()
+    ).toEqual(parseUnits("1", 8).toString()); // max decimals is 8
+    const balanceAfterTransferEth = await getBalanceEth(TEST_ERC20, sender);
+    expect(
+      balanceBeforeTransferEth.sub(balanceAfterTransferEth).toString()
+    ).toEqual(amount.toString());
+
+    // clean up
+    provider.destroy();
+  });
+});
+
+const getBalanceAptos = async (
+  client: AptosClient,
+  type: string,
+  address: HexString
+): Promise<string> => {
+  const res = await client.getAccountResource(
+    address,
+    `0x1::coin::CoinStore<${type}>`
+  );
+  return (res.data as any).coin.value;
+};
+
+const getBalanceEth = (tokenAddress: string, wallet: ethers.Wallet) => {
+  let token = TokenImplementation__factory.connect(tokenAddress, wallet);
+  return token.balanceOf(wallet.address);
+};

+ 4 - 0
sdk/js/src/token_bridge/__tests__/consts.ts

@@ -85,6 +85,10 @@ export const TERRA_HOST =
 
 export const NEAR_NODE_URL = ci ? "http://near:3030" : "http://localhost:3030";
 
+export const APTOS_NODE_URL = ci ? "http://aptos:8080/v1" : "http://0.0.0.0:8080/v1"
+export const APTOS_FAUCET_URL = ci ? "http://aptos:8081" : "http://0.0.0.0:8081"
+export const APTOS_PRIVATE_KEY = "537c1f91e56891445b491068f519b705f8c0f1a1e66111816dd5d4aa85b8113d";
+
 describe("consts should exist", () => {
   it("has Solana test token", () => {
     expect.assertions(1);

+ 13 - 0
sdk/js/src/token_bridge/attest.ts

@@ -21,6 +21,7 @@ import { importTokenWasm } from "../solana/wasm";
 import {
   callFunctionNear,
   hashAccount,
+  ChainId,
   textToHexString,
   textToUint8Array,
   uint8ArrayToHex,
@@ -32,6 +33,8 @@ import { isNativeDenomInjective, isNativeDenomXpla } from "../cosmwasm";
 import { Provider } from "near-api-js/lib/providers";
 import { FunctionCallOptions } from "near-api-js/lib/account";
 import { MsgExecuteContract as XplaMsgExecuteContract } from "@xpla/xpla.js";
+import { Types } from "aptos";
+import { attestToken as attestTokenAptos } from "../aptos";
 
 export async function attestFromEth(
   tokenBridgeAddress: string,
@@ -319,3 +322,13 @@ export async function attestNearFromNear(
     gas: new BN("100000000000000"),
   };
 }
+
+// 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?
+export function attestFromAptos(
+  tokenBridgeAddress: string,
+  tokenChain: ChainId,
+  tokenAddress: string
+): Types.EntryFunctionPayload {
+  return attestTokenAptos(tokenBridgeAddress, tokenChain, tokenAddress);
+}

+ 23 - 15
sdk/js/src/token_bridge/createWrapped.ts

@@ -1,18 +1,23 @@
 import { Connection, PublicKey, Transaction } from "@solana/web3.js";
 import { MsgExecuteContract } from "@terra-money/terra.js";
 import { Algodv2 } from "algosdk";
+import { Types } from "aptos";
+import BN from "bn.js";
 import { ethers, Overrides } from "ethers";
 import { fromUint8Array } from "js-base64";
-import { TransactionSignerPair, _submitVAAAlgorand } from "../algorand";
+import { TransactionSignerPair, _parseVAAAlgorand, _submitVAAAlgorand } from "../algorand";
 import { Bridge__factory } from "../ethers-contracts";
 import { ixFromRust } from "../solana";
 import { importTokenWasm } from "../solana/wasm";
 import { submitVAAOnInjective } from "./redeem";
-import BN from "bn.js";
 import { FunctionCallOptions } from "near-api-js/lib/account";
 import { Provider } from "near-api-js/lib/providers";
 import { callFunctionNear } from "../utils";
 import { MsgExecuteContract as XplaMsgExecuteContract } from "@xpla/xpla.js";
+import {
+  createWrappedCoin as createWrappedCoinAptos,
+  createWrappedCoinType as createWrappedCoinTypeAptos,
+} from "../aptos";
 
 export async function createWrappedOnEth(
   tokenBridgeAddress: string,
@@ -61,12 +66,7 @@ export async function createWrappedOnSolana(
 ): Promise<Transaction> {
   const { create_wrapped_ix } = await importTokenWasm();
   const ix = ixFromRust(
-    create_wrapped_ix(
-      tokenBridgeAddress,
-      bridgeAddress,
-      payerAddress,
-      signedVAA
-    )
+    create_wrapped_ix(tokenBridgeAddress, bridgeAddress, payerAddress, signedVAA)
   );
   const transaction = new Transaction().add(ix);
   const { blockhash } = await connection.getRecentBlockhash();
@@ -82,13 +82,7 @@ export async function createWrappedOnAlgorand(
   senderAddr: string,
   attestVAA: Uint8Array
 ): Promise<TransactionSignerPair[]> {
-  return await _submitVAAAlgorand(
-    client,
-    tokenBridgeId,
-    bridgeId,
-    attestVAA,
-    senderAddr
-  );
+  return await _submitVAAAlgorand(client, tokenBridgeId, bridgeId, attestVAA, senderAddr);
 }
 
 export async function createWrappedOnNear(
@@ -114,3 +108,17 @@ export async function createWrappedOnNear(
   msgs.push({ ...msgs[0] });
   return msgs;
 }
+
+export function createWrappedTypeOnAptos(
+  tokenBridgeAddress: string,
+  signedVAA: Uint8Array
+): Types.EntryFunctionPayload {
+  return createWrappedCoinTypeAptos(tokenBridgeAddress, signedVAA);
+}
+
+export function createWrappedOnAptos(
+  tokenBridgeAddress: string,
+  attestVAA: Uint8Array
+): Types.EntryFunctionPayload {
+  return createWrappedCoinAptos(tokenBridgeAddress, attestVAA);
+}

+ 34 - 26
sdk/js/src/token_bridge/getForeignAsset.ts

@@ -2,13 +2,10 @@ import { Connection, PublicKey } from "@solana/web3.js";
 import { LCDClient } from "@terra-money/terra.js";
 import { ChainGrpcWasmApi } from "@injectivelabs/sdk-ts";
 import { Algodv2 } from "algosdk";
+import { AptosClient } from "aptos";
 import { ethers } from "ethers";
 import { fromUint8Array } from "js-base64";
-import {
-  calcLogicSigAccount,
-  decodeLocalState,
-  hexToNativeAssetBigIntAlgorand,
-} from "../algorand";
+import { calcLogicSigAccount, decodeLocalState, hexToNativeAssetBigIntAlgorand } from "../algorand";
 import { Bridge__factory } from "../ethers-contracts";
 import { importTokenWasm } from "../solana/wasm";
 import {
@@ -17,6 +14,8 @@ import {
   ChainName,
   CHAIN_ID_ALGORAND,
   coalesceChainId,
+  ensureHexPrefix,
+  getForeignAssetAddress,
 } from "../utils";
 import { Provider } from "near-api-js/lib/providers";
 import { LCDClient as XplaLCDClient } from "@xpla/xpla.js";
@@ -37,10 +36,7 @@ export async function getForeignAssetEth(
 ): Promise<string | null> {
   const tokenBridge = Bridge__factory.connect(tokenBridgeAddress, provider);
   try {
-    return await tokenBridge.wrappedAsset(
-      coalesceChainId(originChain),
-      originAsset
-    );
+    return await tokenBridge.wrappedAsset(coalesceChainId(originChain), originAsset);
   } catch (e) {
     return null;
   }
@@ -53,15 +49,12 @@ export async function getForeignAssetTerra(
   originAsset: Uint8Array
 ): Promise<string | null> {
   try {
-    const result: { address: string } = await client.wasm.contractQuery(
-      tokenBridgeAddress,
-      {
-        wrapped_registry: {
-          chain: coalesceChainId(originChain),
-          address: fromUint8Array(originAsset),
-        },
-      }
-    );
+    const result: { address: string } = await client.wasm.contractQuery(tokenBridgeAddress, {
+      wrapped_registry: {
+        chain: coalesceChainId(originChain),
+        address: fromUint8Array(originAsset),
+      },
+    });
     return result.address;
   } catch (e) {
     return null;
@@ -149,9 +142,7 @@ export async function getForeignAssetSolana(
     coalesceChainId(originChain)
   );
   const wrappedAddressPK = new PublicKey(wrappedAddress);
-  const wrappedAssetAccountInfo = await connection.getAccountInfo(
-    wrappedAddressPK
-  );
+  const wrappedAssetAccountInfo = await connection.getAccountInfo(wrappedAddressPK);
   return wrappedAssetAccountInfo ? wrappedAddressPK.toString() : null;
 }
 
@@ -174,11 +165,7 @@ export async function getForeignAssetAlgorand(
     if (!doesExist) {
       return null;
     }
-    let asset: Uint8Array = await decodeLocalState(
-      client,
-      tokenBridgeId,
-      lsa.address()
-    );
+    let asset: Uint8Array = await decodeLocalState(client, tokenBridgeId, lsa.address());
     if (asset.length > 8) {
       const tmp = Buffer.from(asset.slice(0, 8));
       return tmp.readBigUInt64BE(0);
@@ -203,3 +190,24 @@ export async function getForeignAssetNear(
   );
   return ret !== "" ? ret : null;
 }
+
+export async function getForeignAssetAptos(
+  client: AptosClient,
+  tokenBridgeAddress: string,
+  originChain: ChainId | ChainName,
+  originAddress: string
+): Promise<string | null> {
+  const originChainId = coalesceChainId(originChain);
+  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>`);
+    return assetAddress;
+  } catch (e) {
+    return null;
+  }
+}

+ 46 - 25
sdk/js/src/token_bridge/getIsTransferCompleted.ts

@@ -2,11 +2,12 @@ import { ChainGrpcWasmApi } from "@injectivelabs/sdk-ts";
 import { Connection, PublicKey } from "@solana/web3.js";
 import { LCDClient } from "@terra-money/terra.js";
 import { Algodv2, bigIntToBytes } from "algosdk";
+import { AptosClient } from "aptos";
 import axios from "axios";
 import { ethers } from "ethers";
 import { fromUint8Array } from "js-base64";
 import { redeemOnTerra } from ".";
-import { TERRA_REDEEMED_CHECK_WALLET_ADDRESS } from "..";
+import { ensureHexPrefix, TERRA_REDEEMED_CHECK_WALLET_ADDRESS } from "..";
 import {
   BITS_PER_KEY,
   calcLogicSigAccount,
@@ -20,11 +21,12 @@ import { importCoreWasm } from "../solana/wasm";
 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";
 
 export async function getIsTransferCompletedEth(
   tokenBridgeAddress: string,
   provider: ethers.Signer | ethers.providers.Provider,
-  signedVAA: Uint8Array
+  signedVAA: Uint8Array,
 ): Promise<boolean> {
   const tokenBridge = Bridge__factory.connect(tokenBridgeAddress, provider);
   const signedVAAHash = await getSignedVAAHash(signedVAA);
@@ -38,18 +40,16 @@ export async function getIsTransferCompletedTerra(
   tokenBridgeAddress: string,
   signedVAA: Uint8Array,
   client: LCDClient,
-  gasPriceUrl: string
+  gasPriceUrl: string,
 ): Promise<boolean> {
   const msg = await redeemOnTerra(
     tokenBridgeAddress,
     TERRA_REDEEMED_CHECK_WALLET_ADDRESS,
-    signedVAA
+    signedVAA,
   );
   // TODO: remove gasPriceUrl and just use the client's gas prices
   const gasPrices = await axios.get(gasPriceUrl).then((result) => result.data);
-  const account = await client.auth.accountInfo(
-    TERRA_REDEEMED_CHECK_WALLET_ADDRESS
-  );
+  const account = await client.auth.accountInfo(TERRA_REDEEMED_CHECK_WALLET_ADDRESS);
   try {
     await client.tx.estimateFee(
       [
@@ -63,7 +63,7 @@ export async function getIsTransferCompletedTerra(
         memo: "already redeemed calculation",
         feeDenoms: ["uluna"],
         gasPrices,
-      }
+      },
     );
   } catch (e: any) {
     // redeemed if the VAA was already executed
@@ -136,28 +136,22 @@ export async function getIsTransferCompletedXpla(
   signedVAA: Uint8Array,
   client: XplaLCDClient
 ): Promise<boolean> {
-  const result: { is_redeemed: boolean } = await client.wasm.contractQuery(
-    tokenBridgeAddress,
-    {
-      is_vaa_redeemed: {
-        vaa: fromUint8Array(signedVAA),
-      },
-    }
-  );
+  const result: { is_redeemed: boolean } = await client.wasm.contractQuery(tokenBridgeAddress, {
+    is_vaa_redeemed: {
+      vaa: fromUint8Array(signedVAA),
+    },
+  });
   return result.is_redeemed;
 }
 
 export async function getIsTransferCompletedSolana(
   tokenBridgeAddress: string,
   signedVAA: Uint8Array,
-  connection: Connection
+  connection: Connection,
 ): Promise<boolean> {
   const { claim_address } = await importCoreWasm();
   const claimAddress = await claim_address(tokenBridgeAddress, signedVAA);
-  const claimInfo = await connection.getAccountInfo(
-    new PublicKey(claimAddress),
-    "confirmed"
-  );
+  const claimInfo = await connection.getAccountInfo(new PublicKey(claimAddress), "confirmed");
   return !!claimInfo;
 }
 
@@ -175,7 +169,7 @@ async function checkBitsSet(
   client: Algodv2,
   appId: bigint,
   addr: string,
-  seq: bigint
+  seq: bigint,
 ): Promise<boolean> {
   let retval: boolean = false;
   let appState: any[] = [];
@@ -222,7 +216,7 @@ async function checkBitsSet(
 export async function getIsTransferCompletedAlgorand(
   client: Algodv2,
   appId: bigint,
-  signedVAA: Uint8Array
+  signedVAA: Uint8Array,
 ): Promise<boolean> {
   const parsedVAA = _parseVAAAlgorand(signedVAA);
   const seq: bigint = parsedVAA.sequence;
@@ -232,7 +226,7 @@ export async function getIsTransferCompletedAlgorand(
     client,
     appId,
     seq / BigInt(MAX_BITS),
-    chainRaw + em
+    chainRaw + em,
   );
   if (!doesExist) {
     return false;
@@ -245,7 +239,7 @@ export async function getIsTransferCompletedAlgorand(
 export async function getIsTransferCompletedNear(
   provider: Provider,
   tokenBridge: string,
-  signedVAA: Uint8Array
+  signedVAA: Uint8Array,
 ): Promise<boolean> {
   const vaa = Buffer.from(signedVAA).toString("hex");
   return (
@@ -254,3 +248,30 @@ export async function getIsTransferCompletedNear(
     })
   )[1];
 }
+
+export async function getIsTransferCompletedAptos(
+  client: AptosClient,
+  tokenBridgeAddress: string,
+  signedVAA: Uint8Array,
+): Promise<boolean> {
+  // get handle
+  tokenBridgeAddress = ensureHexPrefix(tokenBridgeAddress);
+  const state = (
+    await client.getAccountResource(tokenBridgeAddress, `${tokenBridgeAddress}::state::State`)
+  ).data as State;
+  const handle = state.consumed_vaas.elems.handle;
+
+  // check if vaa hash is in consumed_vaas
+  const signedVAAHash = await getSignedVAAHash(signedVAA);
+  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,
+    });
+    return true;
+  } catch {
+    return false;
+  }
+}

+ 18 - 1
sdk/js/src/token_bridge/getIsWrappedAsset.ts

@@ -2,10 +2,11 @@ import { ChainGrpcWasmApi } from "@injectivelabs/sdk-ts";
 import { Connection, PublicKey } from "@solana/web3.js";
 import { LCDClient } from "@terra-money/terra.js";
 import { Algodv2, getApplicationAddress } from "algosdk";
+import { AptosClient } from "aptos";
 import { ethers } from "ethers";
 import { Bridge__factory } from "../ethers-contracts";
 import { importTokenWasm } from "../solana/wasm";
-import { CHAIN_ID_INJECTIVE, tryNativeToHexString } from "../utils";
+import { CHAIN_ID_INJECTIVE, ensureHexPrefix, tryNativeToHexString } from "../utils";
 import { safeBigIntToNumber } from "../utils/bigint";
 import { getForeignAssetInjective } from "./getForeignAsset";
 
@@ -114,3 +115,19 @@ export function getIsWrappedAssetNear(
 ): boolean {
   return asset.endsWith("." + tokenBridge);
 }
+
+// TODO: do we need to check if token is registered in bridge?
+export async function getIsWrappedAssetAptos(
+  client: AptosClient,
+  tokenBridgeAddress: string,
+  assetAddress: string,
+): Promise<boolean> {
+  assetAddress = ensureHexPrefix(assetAddress);
+  try {
+    // get origin info from asset address
+    await client.getAccountResource(assetAddress, `${tokenBridgeAddress}::state::OriginInfo`);
+    return true;
+  } catch {
+    return false;
+  }
+}

+ 54 - 1
sdk/js/src/token_bridge/getOriginalAsset.ts

@@ -11,9 +11,11 @@ import { importTokenWasm } from "../solana/wasm";
 import { buildNativeId } from "../terra";
 import { canonicalAddress } from "../cosmos";
 import {
+  assertChain,
   ChainId,
   ChainName,
   CHAIN_ID_ALGORAND,
+  CHAIN_ID_APTOS,
   CHAIN_ID_NEAR,
   CHAIN_ID_INJECTIVE,
   CHAIN_ID_SOLANA,
@@ -25,6 +27,7 @@ import {
   coalesceCosmWasmChainId,
   tryHexToNativeAssetString,
   callFunctionNear,
+  isValidAptosType,
 } from "../utils";
 import { safeBigIntToNumber } from "../utils/bigint";
 import {
@@ -34,6 +37,9 @@ import {
 } from "./getIsWrappedAsset";
 import { Provider } from "near-api-js/lib/providers";
 import { LCDClient as XplaLCDClient } from "@xpla/xpla.js";
+import { AptosClient } from "aptos";
+import { OriginInfo } from "../aptos/types"
+import { sha3_256 } from "js-sha3";;
 
 // TODO: remove `as ChainId` and return number in next minor version as we can't ensure it will match our type definition
 export interface WormholeWrappedInfo {
@@ -284,7 +290,7 @@ export async function getOriginalAssetNear(
     chainId: CHAIN_ID_NEAR,
     assetAddress: new Uint8Array(),
   };
-  retVal.isWrapped = await getIsWrappedAssetNear(tokenAccount, assetAccount);
+  retVal.isWrapped = getIsWrappedAssetNear(tokenAccount, assetAccount);
   if (!retVal.isWrapped) {
     retVal.assetAddress = assetAccount
       ? arrayify(sha256(Buffer.from(assetAccount)))
@@ -306,3 +312,50 @@ export async function getOriginalAssetNear(
 
   return retVal;
 }
+
+export async function getOriginalAssetAptos(
+  client: AptosClient,
+  tokenBridgeAddress: string,
+  fullyQualifiedType: string
+): Promise<WormholeWrappedInfo> {
+  if (!isValidAptosType(fullyQualifiedType)) {
+    throw new Error("Need fully qualified address");
+  }
+
+  let originInfo: OriginInfo | undefined;
+  try {
+    originInfo = (
+      await client.getAccountResource(
+        fullyQualifiedType.split("::")[0],
+        `${tokenBridgeAddress}::state::OriginInfo`
+      )
+    ).data as OriginInfo;
+  } catch {
+    return {
+      isWrapped: false,
+      chainId: CHAIN_ID_APTOS,
+      assetAddress: hexToUint8Array(sha3_256(fullyQualifiedType)),
+    };
+  }
+
+  if (!!originInfo) {
+    // wrapped asset
+    const chainId = parseInt(originInfo.token_chain.number);
+    assertChain(chainId);
+    const assetAddress = hexToUint8Array(
+      originInfo.token_address.external_address.substring(2)
+    );
+    return {
+      isWrapped: true,
+      chainId,
+      assetAddress,
+    };
+  } else {
+    // native asset
+    return {
+      isWrapped: false,
+      chainId: CHAIN_ID_APTOS,
+      assetAddress: hexToUint8Array(sha3_256(fullyQualifiedType)),
+    };
+  }
+}

+ 15 - 1
sdk/js/src/token_bridge/redeem.ts

@@ -23,7 +23,7 @@ import {
   WSOL_DECIMALS,
   uint8ArrayToHex,
   callFunctionNear,
-  hashLookup,
+  hashLookup
 } from "../utils";
 
 import { getForeignAssetNear } from ".";
@@ -37,6 +37,8 @@ import { MsgExecuteContract as MsgExecuteContractInjective } from "@injectivelab
 import { FunctionCallOptions } from "near-api-js/lib/account";
 import { Provider } from "near-api-js/lib/providers";
 import { MsgExecuteContract as XplaMsgExecuteContract } from "@xpla/xpla.js";
+import { AptosClient, Types } from "aptos";
+import { completeTransfer as completeTransferAptos, completeTransferAndRegister } from "../aptos";
 
 export async function redeemOnEth(
   tokenBridgeAddress: string,
@@ -382,3 +384,15 @@ export async function redeemOnNear(
 
   return options;
 }
+
+export function redeemOnAptos(
+  client: AptosClient,
+  tokenBridgeAddress: string,
+  transferVAA: Uint8Array
+): Promise<Types.EntryFunctionPayload> {
+  return completeTransferAndRegister(
+    client,
+    tokenBridgeAddress,
+    transferVAA
+  );
+}

+ 23 - 0
sdk/js/src/token_bridge/transfer.ts

@@ -48,10 +48,12 @@ import {
 } from "../utils";
 import { safeBigIntToNumber } from "../utils/bigint";
 import { isNativeDenomInjective, isNativeDenomXpla } from "../cosmwasm";
+import { Types } from "aptos";
 const BN = require("bn.js");
 import { FunctionCallOptions } from "near-api-js/lib/account";
 import { Provider } from "near-api-js/lib/providers";
 import { MsgExecuteContract as XplaMsgExecuteContract } from "@xpla/xpla.js";
+import { transferTokens as transferTokensAptos } from "../aptos";
 
 export async function getAllowanceEth(
   tokenBridgeAddress: string,
@@ -958,3 +960,24 @@ export async function transferNearFromNear(
     gas: new BN("100000000000000"),
   };
 }
+
+export function transferFromAptos(
+  tokenBridgeAddress: string,
+  fullyQualifiedType: string,
+  amount: string,
+  recipientChain: ChainId | ChainName,
+  recipient: Uint8Array,
+  relayerFee: string = "0",
+  payload: string = ""
+): Types.EntryFunctionPayload {
+  return transferTokensAptos(
+    tokenBridgeAddress,
+    fullyQualifiedType,
+    amount,
+    recipientChain,
+    recipient,
+    relayerFee,
+    createNonce().readUInt32LE(0),
+    payload
+  );
+}

+ 3 - 0
sdk/js/src/token_bridge/updateWrapped.ts

@@ -6,6 +6,7 @@ import {
   createWrappedOnNear,
   submitVAAOnInjective,
   createWrappedOnXpla,
+  createWrappedOnAptos,
 } from ".";
 import { Bridge__factory } from "../ethers-contracts";
 
@@ -32,3 +33,5 @@ export const updateWrappedOnSolana = createWrappedOnSolana;
 export const updateWrappedOnAlgorand = createWrappedOnAlgorand;
 
 export const updateWrappedOnNear = createWrappedOnNear;
+
+export const updateWrappedOnAptos = createWrappedOnAptos;

+ 195 - 0
sdk/js/src/utils/aptos.ts

@@ -0,0 +1,195 @@
+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";
+
+export const signAndSubmitEntryFunction = (
+  client: AptosClient,
+  sender: AptosAccount,
+  payload: Types.EntryFunctionPayload,
+  opts?: Partial<Types.SubmitTransactionRequest>
+): Promise<Types.UserTransaction> => {
+  // overwriting `max_gas_amount` and `gas_unit_price` defaults
+  // rest of defaults are defined here: https://aptos-labs.github.io/ts-sdk-doc/classes/AptosClient.html#generateTransaction
+  const customOpts = Object.assign(
+    {
+      gas_unit_price: "100",
+      max_gas_amount: "30000",
+    },
+    opts
+  );
+
+  return client
+    .generateTransaction(sender.address(), payload, customOpts)
+    .then(
+      (rawTx) =>
+        signAndSubmitTransaction(
+          client,
+          sender,
+          rawTx
+        ) as Promise<Types.UserTransaction>
+    );
+};
+
+export const signAndSubmitScript = async (
+  client: AptosClient,
+  sender: AptosAccount,
+  payload: TxnBuilderTypes.TransactionPayloadScript,
+  opts?: Partial<Types.SubmitTransactionRequest>
+) => {
+  // overwriting `max_gas_amount` and `gas_unit_price` defaults
+  // rest of defaults are defined here: https://aptos-labs.github.io/ts-sdk-doc/classes/AptosClient.html#generateTransaction
+  const customOpts = Object.assign(
+    {
+      gas_unit_price: "100",
+      max_gas_amount: "30000",
+    },
+    opts
+  );
+
+  // create raw transaction
+  const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([
+    client.getAccount(sender.address()),
+    client.getChainId(),
+  ]);
+  const rawTx = new TxnBuilderTypes.RawTransaction(
+    TxnBuilderTypes.AccountAddress.fromHex(sender.address()),
+    BigInt(sequenceNumber),
+    payload,
+    BigInt(customOpts.max_gas_amount),
+    BigInt(customOpts.gas_unit_price),
+    BigInt(Math.floor(Date.now() / 1000) + 10),
+    new TxnBuilderTypes.ChainId(chainId)
+  );
+  // sign & submit transaction
+  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));
+};
+
+export const getAssetFullyQualifiedType = (
+  tokenBridgeAddress: string, // 32 bytes
+  originChain: ChainId,
+  originAddress: string
+): string | null => {
+  // native asset
+  if (originChain === CHAIN_ID_APTOS) {
+    // originAddress should be of form address::module::type
+    if (!isValidAptosType(originAddress)) {
+      console.error("Need fully qualified address for native asset");
+      return null;
+    }
+
+    return ensureHexPrefix(originAddress);
+  }
+
+  // non-native asset, derive unique address
+  const wrappedAssetAddress = getForeignAssetAddress(
+    tokenBridgeAddress,
+    originChain,
+    originAddress
+  );
+  return wrappedAssetAddress
+    ? `${ensureHexPrefix(wrappedAssetAddress)}::coin::T`
+    : null;
+};
+
+export const getForeignAssetAddress = (
+  tokenBridgeAddress: string, // 32 bytes
+  originChain: ChainId,
+  originAddress: string
+): string | null => {
+  if (originChain === CHAIN_ID_APTOS) {
+    return null;
+  }
+
+  // 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);
+  DERIVE_RESOURCE_ACCOUNT_SCHEME.writeUInt8(255);
+
+  let chain: Buffer = Buffer.alloc(2);
+  chain.writeUInt16BE(originChain);
+  return sha3_256(
+    Buffer.concat([
+      hex(hexZeroPad(ensureHexPrefix(tokenBridgeAddress), 32)),
+      chain,
+      Buffer.from("::", "ascii"),
+      hex(hexZeroPad(ensureHexPrefix(originAddress), 32)),
+      DERIVE_RESOURCE_ACCOUNT_SCHEME,
+    ])
+  );
+};
+
+export const isValidAptosType = (address: string): boolean =>
+  /^(0x)?[0-9a-fA-F]+::\w+::\w+$/.test(address);
+
+export const getExternalAddressFromType = (
+  fullyQualifiedType: string
+): string => {
+  // hash the type so it fits into 32 bytes
+  return sha3_256(fullyQualifiedType);
+};
+
+export async function getTypeFromExternalAddress(
+  client: AptosClient,
+  tokenBridgeAddress: string,
+  fullyQualifiedTypeHash: string
+): Promise<string | null> {
+  // get handle
+  tokenBridgeAddress = ensureHexPrefix(tokenBridgeAddress);
+  const state = (
+    await client.getAccountResource(
+      tokenBridgeAddress,
+      `${tokenBridgeAddress}::state::State`
+    )
+  ).data as State;
+  const handle = state.native_infos.handle;
+
+  try {
+    // get type info
+    const typeInfo = await client.getTableItem(handle, {
+      key_type: `${tokenBridgeAddress}::token_hash::TokenHash`,
+      value_type: "0x1::type_info::TypeInfo",
+      key: { hash: fullyQualifiedTypeHash },
+    });
+
+    if (!typeInfo) {
+      return null;
+    }
+
+    // construct type
+    const moduleName = Buffer.from(
+      typeInfo.module_name.substring(2),
+      "hex"
+    ).toString("ascii");
+    const structName = Buffer.from(
+      typeInfo.struct_name.substring(2),
+      "hex"
+    ).toString("ascii");
+    return `${typeInfo.account_address}::${moduleName}::${structName}`;
+  } catch {
+    return null;
+  }
+}

+ 22 - 5
sdk/js/src/utils/array.ts

@@ -2,6 +2,7 @@ import { arrayify, zeroPad } from "@ethersproject/bytes";
 import { PublicKey } from "@solana/web3.js";
 import { hexValue, hexZeroPad, sha256, stripZeros } from "ethers/lib/utils";
 import { Provider as NearProvider } from "near-api-js/lib/providers";
+import { ethers } from "ethers";
 import {
   hexToNativeAssetStringAlgorand,
   nativeStringToHexAlgorand,
@@ -14,12 +15,13 @@ import {
   ChainId,
   ChainName,
   CHAIN_ID_ALGORAND,
-  CHAIN_ID_NEAR,
+  CHAIN_ID_APTOS,
   CHAIN_ID_INJECTIVE,
+  CHAIN_ID_NEAR,
   CHAIN_ID_OSMOSIS,
-  CHAIN_ID_SUI,
-  CHAIN_ID_APTOS,
+  CHAIN_ID_PYTHNET,
   CHAIN_ID_SOLANA,
+  CHAIN_ID_SUI,
   CHAIN_ID_TERRA,
   CHAIN_ID_TERRA2,
   CHAIN_ID_WORMCHAIN,
@@ -27,10 +29,10 @@ import {
   coalesceChainId,
   isEVMChain,
   isTerraChain,
-  CHAIN_ID_PYTHNET,
   CHAIN_ID_XPLA,
 } from "./consts";
 import { hashLookup } from "./near";
+import { getExternalAddressFromType, isValidAptosType } from "./aptos";
 
 /**
  *
@@ -241,7 +243,11 @@ export const tryNativeToHexString = (
   } else if (chainId === CHAIN_ID_SUI) {
     throw Error("hexToNativeString: Sui not supported yet.");
   } else if (chainId === CHAIN_ID_APTOS) {
-    throw Error("hexToNativeString: Aptos not supported yet.");
+    if (isValidAptosType(address)) {
+      return getExternalAddressFromType(address);
+    }
+
+    return uint8ArrayToHex(zeroPad(arrayify(address, { allowMissingPrefix:true }), 32));
   } else if (chainId === CHAIN_ID_UNSET) {
     throw Error("hexToNativeString: Chain id unset");
   } else {
@@ -309,3 +315,14 @@ export function textToHexString(name: string): string {
 export function textToUint8Array(name: string): Uint8Array {
   return new Uint8Array(Buffer.from(name, "binary"));
 }
+
+export function hex(x: string): Buffer {
+  return Buffer.from(
+    ethers.utils.hexlify(x, { allowMissingPrefix: true }).substring(2),
+    "hex"
+  );
+}
+
+export function ensureHexPrefix(x: string): string {
+  return x.substring(0, 2) !== "0x" ? `0x${x}` : x;
+}

+ 16 - 7
sdk/js/src/utils/consts.ts

@@ -56,6 +56,7 @@ export type EVMChainName =
   | "optimism"
   | "gnosis"
   | "ropsten";
+
 /**
  *
  * All the Solana-based chain names that Wormhole supports
@@ -75,6 +76,8 @@ export type ChainContracts = {
   [chain in ChainName]: Contracts;
 };
 
+export type Network = "MAINNET" | "TESTNET" | "DEVNET";
+
 const MAINNET = {
   unset: {
     core: undefined,
@@ -167,8 +170,9 @@ const MAINNET = {
     nft_bridge: undefined,
   },
   aptos: {
-    core: undefined,
-    token_bridge: undefined,
+    core: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625",
+    token_bridge:
+      "0x576410486a2da45eee6c949c995670112ddf2fbeedab20350d506328eefc9d4f",
     nft_bridge: undefined,
   },
   sui: {
@@ -214,7 +218,8 @@ const MAINNET = {
   },
   xpla: {
     core: "xpla1jn8qmdda5m6f6fqu9qv46rt7ajhklg40ukpqchkejcvy8x7w26cqxamv3w",
-    token_bridge: "xpla137w0wfch2dfmz7jl2ap8pcmswasj8kg06ay4dtjzw7tzkn77ufxqfw7acv",
+    token_bridge:
+      "xpla137w0wfch2dfmz7jl2ap8pcmswasj8kg06ay4dtjzw7tzkn77ufxqfw7acv",
     nft_bridge: undefined,
   },
   ropsten: {
@@ -321,8 +326,9 @@ const TESTNET = {
     nft_bridge: undefined,
   },
   aptos: {
-    core: undefined,
-    token_bridge: undefined,
+    core: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625",
+    token_bridge:
+      "0x576410486a2da45eee6c949c995670112ddf2fbeedab20350d506328eefc9d4f",
     nft_bridge: undefined,
   },
   sui: {
@@ -476,8 +482,9 @@ const DEVNET = {
     nft_bridge: undefined,
   },
   aptos: {
-    core: undefined,
-    token_bridge: undefined,
+    core: "0xde0036a9600559e295d5f6802ef6f3f802f510366e0c23912b0655d972166017",
+    token_bridge:
+      "0x84a5f374d29fc77e370014dce4fd6a55b58ad608de8074b0be5571701724da31",
     nft_bridge: undefined,
   },
   sui: {
@@ -781,6 +788,8 @@ export function assertEVMChain(
 export const WSOL_ADDRESS = "So11111111111111111111111111111111111111112";
 export const WSOL_DECIMALS = 9;
 export const MAX_VAA_DECIMALS = 8;
+export const APTOS_TOKEN_BRIDGE_EMITTER_ADDRESS =
+  "0000000000000000000000000000000000000000000000000000000000000001";
 
 export const TERRA_REDEEMED_CHECK_WALLET_ADDRESS =
   "terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v";

+ 4 - 3
sdk/js/src/utils/index.ts

@@ -1,6 +1,7 @@
-export * from "./consts";
-export * from "./createNonce";
-export * from "./parseVaa";
+export * from "./aptos";
 export * from "./array";
 export * from "./bigint";
+export * from "./consts";
+export * from "./createNonce";
 export * from "./near";
+export * from "./parseVaa";