Browse Source

sdk/js: parallelize tests

Evan Gray 3 years ago
parent
commit
577d9b35f8

+ 8 - 6
ethereum/scripts/deploy_test_token.js

@@ -36,7 +36,7 @@ const interateToStandardTransactionCount = async () => {
   return Promise.resolve();
 };
 
-module.exports = async function (callback) {
+module.exports = async function(callback) {
   try {
     const accounts = await web3.eth.getAccounts();
 
@@ -89,14 +89,16 @@ module.exports = async function (callback) {
 
     console.log("WETH token deployed at: " + wethAddress);
 
-    await token.methods.mint(accounts[2], "1000000000000000000000").send({
-      from: accounts[0],
-      gas: 1000000,
-    });
+    for (let idx = 2; idx < 10; idx++) {
+      await token.methods.mint(accounts[idx], "1000000000000000000000").send({
+        from: accounts[0],
+        gas: 1000000,
+      });
+    }
 
     // devnet WETH token address should be deterministic
     if (wethAddress !== "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E") {
-      throw new Error("unexpected WETH token address")
+      throw new Error("unexpected WETH token address");
     }
 
     callback();

+ 0 - 12
sdk/js/src/nft_bridge/__tests__/consts.ts

@@ -7,10 +7,6 @@ const ci = !!process.env.CI;
 export const ETH_NODE_URL = ci ? "ws://eth-devnet:8545" : "ws://localhost:8545";
 export const ETH_PRIVATE_KEY =
   "0x6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1"; // account 1
-export const ETH_CORE_BRIDGE_ADDRESS =
-  "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550";
-export const ETH_NFT_BRIDGE_ADDRESS =
-  "0x26b4afb60d6c903165150c6f0aa14f8016be4aec";
 export const SOLANA_HOST = ci
   ? "http://solana-devnet:8899"
   : "http://localhost:8899";
@@ -20,10 +16,6 @@ export const SOLANA_PRIVATE_KEY = new Uint8Array([
   8, 174, 214, 157, 175, 126, 98, 90, 54, 24, 100, 177, 247, 77, 19, 112, 47,
   44, 165, 109, 233, 102, 14, 86, 109, 29, 134, 145, 132, 141,
 ]);
-export const SOLANA_CORE_BRIDGE_ADDRESS =
-  "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o";
-export const SOLANA_NFT_BRIDGE_ADDRESS =
-  "NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA";
 export const TERRA_NODE_URL = ci
   ? "http://terra-terrad:1317"
   : "http://localhost:1317";
@@ -31,10 +23,6 @@ export const TERRA_CHAIN_ID = "localterra";
 export const TERRA_GAS_PRICES_URL = ci
   ? "http://terra-fcd:3060/v1/txs/gas_prices"
   : "http://localhost:3060/v1/txs/gas_prices";
-export const TERRA_CORE_BRIDGE_ADDRESS =
-  "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5";
-export const TERRA_NFT_BRIDGE_ADDRESS =
-  "terra1plju286nnfj3z54wgcggd4enwaa9fgf5kgrgzl";
 export const TERRA_PRIVATE_KEY =
   "quality vacuum heart guard buzz spike sight swarm shove special gym robust assume sudden deposit grid alcohol choice devote leader tilt noodle tide penalty";
 export const TEST_ERC721 = "0x5b9b42d6e4B2e4Bf8d42Eba32D46918e10899B66";

+ 124 - 437
sdk/js/src/nft_bridge/__tests__/integration.ts

@@ -1,30 +1,25 @@
-import {
-  Coins,
-  LCDClient,
-  MnemonicKey,
-  Msg,
-  MsgExecuteContract,
-  Fee,
-  TxInfo,
-  WaitTxBroadcastResult,
-  Wallet,
-  isTxError,
-} from "@terra-money/terra.js";
 import axios from "axios";
 import Web3 from "web3";
 import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
-import { describe, expect, jest, test } from "@jest/globals";
+import {
+  afterEach,
+  beforeEach,
+  describe,
+  expect,
+  jest,
+  test,
+} from "@jest/globals";
 import {
   ASSOCIATED_TOKEN_PROGRAM_ID,
   Token,
   TOKEN_PROGRAM_ID,
 } from "@solana/spl-token";
-import { MsgInstantiateContract } from "@terra-money/terra.js";
-import { BigNumberish, ethers } from "ethers";
+import { BigNumber, BigNumberish, ethers } from "ethers";
 import {
   ChainId,
   CHAIN_ID_ETH,
   CHAIN_ID_TERRA,
+  CONTRACTS,
   getEmitterAddressEth,
   getEmitterAddressTerra,
   parseSequenceFromLogEth,
@@ -38,12 +33,9 @@ import {
 import getSignedVAAWithRetry from "../../rpc/getSignedVAAWithRetry";
 import { importCoreWasm, setDefaultWasm } from "../../solana/wasm";
 import {
-  ETH_CORE_BRIDGE_ADDRESS,
   ETH_NODE_URL,
   ETH_PRIVATE_KEY,
-  ETH_NFT_BRIDGE_ADDRESS,
   TERRA_GAS_PRICES_URL,
-  TERRA_NFT_BRIDGE_ADDRESS,
   WORMHOLE_RPC_HOSTS,
   TERRA_CW721_CODE_ID,
   TERRA_NODE_URL,
@@ -52,8 +44,6 @@ import {
   SOLANA_PRIVATE_KEY,
   TEST_SOLANA_TOKEN,
   SOLANA_HOST,
-  SOLANA_CORE_BRIDGE_ADDRESS,
-  SOLANA_NFT_BRIDGE_ADDRESS,
 } from "./consts";
 import {
   NFTImplementation,
@@ -68,6 +58,7 @@ import {
 } from "@solana/web3.js";
 import { postVaaSolanaWithRetry } from "../../solana";
 import { tryNativeToUint8Array } from "../../utils";
+import { arrayify } from "ethers/lib/utils";
 const ERC721 = require("@openzeppelin/contracts/build/contracts/ERC721PresetMinterPauserAutoId.json");
 
 setDefaultWasm("node");
@@ -76,18 +67,6 @@ jest.setTimeout(60000);
 
 type Address = string;
 
-// terra setup
-const lcd = new LCDClient({
-  URL: TERRA_NODE_URL,
-  chainID: TERRA_CHAIN_ID,
-  isClassic: true,
-});
-const terraWallet: Wallet = lcd.wallet(
-  new MnemonicKey({
-    mnemonic: TERRA_PRIVATE_KEY,
-  })
-);
-
 // ethereum setup
 const web3 = new Web3(ETH_NODE_URL);
 
@@ -110,165 +89,73 @@ afterEach(() => {
 });
 
 describe("Integration Tests", () => {
-  test("Send Ethereum ERC-721 to Terra and back", (done) => {
-    (async () => {
-      try {
-        const erc721 = await deployNFTOnEth(
-          "Not an APE 🐒",
-          "APE🐒",
-          "https://cloudflare-ipfs.com/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/",
-          11 // mint ids 0..10 (inclusive)
-        );
-        const transaction = await _transferFromEth(
-          erc721.address,
-          10,
-          terraWallet.key.accAddress,
-          CHAIN_ID_TERRA
-        );
-        let signedVAA = await waitUntilEthTxObserved(transaction);
-        (await expectReceivedOnTerra(signedVAA)).toBe(false);
-        await _redeemOnTerra(signedVAA);
-        (await expectReceivedOnTerra(signedVAA)).toBe(true);
-
-        // Check we have the wrapped NFT contract
-        const terra_addr = await nft_bridge.getForeignAssetTerra(
-          TERRA_NFT_BRIDGE_ADDRESS,
-          lcd,
-          CHAIN_ID_ETH,
-          tryNativeToUint8Array(erc721.address, CHAIN_ID_ETH)
-        );
-        if (!terra_addr) {
-          throw new Error("Terra address is null");
-        }
-
-        // 10 => "10"
-        const info: any = await lcd.wasm.contractQuery(terra_addr, {
-          nft_info: { token_id: "10" },
-        });
-        expect(info.token_uri).toBe(
-          "https://cloudflare-ipfs.com/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/10"
-        );
-
-        const ownerInfo: any = await lcd.wasm.contractQuery(terra_addr, {
-          owner_of: { token_id: "10" },
-        });
-        expect(ownerInfo.owner).toBe(terraWallet.key.accAddress);
-
-        let ownerEth = await erc721.ownerOf(10);
-        expect(ownerEth).not.toBe(signer.address);
-
-        // Send back the NFT to ethereum
-        const transaction2 = await _transferFromTerra(
-          terra_addr,
-          "10",
-          signer.address,
-          CHAIN_ID_ETH
-        );
-        signedVAA = await waitUntilTerraTxObserved(transaction2);
-        (await expectReceivedOnEth(signedVAA)).toBe(false);
-        await _redeemOnEth(signedVAA);
-        (await expectReceivedOnEth(signedVAA)).toBe(true);
-
-        // ensure that the transaction roundtrips back to the original native asset
-        ownerEth = await erc721.ownerOf(10);
-        expect(ownerEth).toBe(signer.address);
-
-        // the wrapped token should no longer exist
-        let error;
-        try {
-          await lcd.wasm.contractQuery(terra_addr, {
-            owner_of: { token_id: "10" },
-          });
-        } catch (e) {
-          error = e;
-        }
-        expect(error).not.toBeNull();
-
-        done();
-      } catch (e) {
-        console.error(e);
-        done(
-          `An error occured while trying to transfer from Ethereum to Terra and back: ${e}`
-        );
-      }
-    })();
-  });
-  test("Send Terra CW721 to Ethereum and back", (done) => {
-    (async () => {
-      try {
-        const token_id = "first";
-        const cw721 = await deployNFTOnTerra(
-          "Integration Test NFT",
-          "INT",
-          "https://ixmfkhnh2o4keek2457f2v2iw47cugsx23eynlcfpstxihsay7nq.arweave.net/RdhVHafTuKIRWud-XVdItz4qGlfWyYasRXyndB5Ax9s/",
-          token_id
-        );
-        // transfer NFT
-        const transaction = await _transferFromTerra(
-          cw721,
-          token_id,
-          signer.address,
-          CHAIN_ID_ETH
-        );
-        let signedVAA = await waitUntilTerraTxObserved(transaction);
-        (await expectReceivedOnEth(signedVAA)).toBe(false);
-        await _redeemOnEth(signedVAA);
-        (await expectReceivedOnEth(signedVAA)).toBe(true);
-
-        // Check we have the wrapped NFT contract
-        const eth_addr = await nft_bridge.getForeignAssetEth(
-          ETH_NFT_BRIDGE_ADDRESS,
-          provider,
-          CHAIN_ID_TERRA,
-          tryNativeToUint8Array(cw721, CHAIN_ID_TERRA)
-        );
-        if (!eth_addr) {
-          throw new Error("Ethereum address is null");
-        }
-
-        const token = NFTImplementation__factory.connect(eth_addr, signer);
-        // the id on eth will be the keccak256 hash of the terra id
-        const eth_id = "0x" + sha3.keccak256(token_id);
-        const owner = await token.ownerOf(eth_id);
-        expect(owner).toBe(signer.address);
-
-        // send back the NFT to terra
-        const transaction2 = await _transferFromEth(
-          eth_addr,
-          eth_id,
-          terraWallet.key.accAddress,
-          CHAIN_ID_TERRA
-        );
-        signedVAA = await waitUntilEthTxObserved(transaction2);
-        (await expectReceivedOnTerra(signedVAA)).toBe(false);
-        await _redeemOnTerra(signedVAA);
-        (await expectReceivedOnTerra(signedVAA)).toBe(true);
-
-        const ownerInfo: any = await lcd.wasm.contractQuery(cw721, {
-          owner_of: { token_id: token_id },
-        });
-        expect(ownerInfo.owner).toBe(terraWallet.key.accAddress);
-
-        // the wrapped token should no longer exist
-        let error;
-        try {
-          await token.ownerOf(eth_id);
-        } catch (e) {
-          error = e;
-        }
-        expect(error).not.toBeNull();
-        expect(error.message).toContain("nonexistent token");
-
-        done();
-      } catch (e) {
-        console.error(e);
-        done(
-          `An error occured while trying to transfer from Terra to Ethereum: ${e}`
-        );
-      }
-    })();
-  });
-  test("Send Solana SPL to Terra to Ethereum to Solana", (done) => {
+  // TODO: figure out why this isn't working
+  // test("Send Ethereum ERC-721 to Solana and back", (done) => {
+  //   (async () => {
+  //     try {
+  //       const erc721 = await deployNFTOnEth(
+  //         "Not an APE 🐒",
+  //         "APE🐒",
+  //         "https://cloudflare-ipfs.com/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/",
+  //         11 // mint ids 0..10 (inclusive)
+  //       );
+  //       const sol_addr = await nft_bridge.getForeignAssetSol(
+  //         CONTRACTS.DEVNET.solana.nft_bridge,
+  //         CHAIN_ID_ETH,
+  //         tryNativeToUint8Array(erc721.address, CHAIN_ID_ETH),
+  //         arrayify(BigNumber.from("10"))
+  //       );
+  //       const fromAddress = await Token.getAssociatedTokenAddress(
+  //         ASSOCIATED_TOKEN_PROGRAM_ID,
+  //         TOKEN_PROGRAM_ID,
+  //         new PublicKey(sol_addr),
+  //         keypair.publicKey
+  //       );
+  //       const transaction = await _transferFromEth(
+  //         erc721.address,
+  //         10,
+  //         fromAddress.toString(),
+  //         CHAIN_ID_SOLANA
+  //       );
+  //       let signedVAA = await waitUntilEthTxObserved(transaction);
+  //       await _redeemOnSolana(signedVAA);
+
+  //       let ownerEth = await erc721.ownerOf(10);
+  //       expect(ownerEth).not.toBe(signer.address);
+
+  //       // TODO: check wrapped balance
+
+  //       // Send back the NFT to ethereum
+  //       const transaction2 = await _transferFromSolana(
+  //         fromAddress,
+  //         sol_addr,
+  //         signer.address,
+  //         CHAIN_ID_ETH,
+  //         tryNativeToUint8Array(erc721.address, CHAIN_ID_ETH),
+  //         CHAIN_ID_ETH,
+  //         arrayify(BigNumber.from("10"))
+  //       );
+  //       signedVAA = await waitUntilSolanaTxObserved(transaction2);
+  //       (await expectReceivedOnEth(signedVAA)).toBe(false);
+  //       await _redeemOnEth(signedVAA);
+  //       (await expectReceivedOnEth(signedVAA)).toBe(true);
+
+  //       // ensure that the transaction roundtrips back to the original native asset
+  //       ownerEth = await erc721.ownerOf(10);
+  //       expect(ownerEth).toBe(signer.address);
+
+  //       // TODO: the wrapped token should no longer exist
+
+  //       done();
+  //     } catch (e) {
+  //       console.error(e);
+  //       done(
+  //         `An error occured while trying to transfer from Ethereum to Solana and back: ${e}`
+  //       );
+  //     }
+  //   })();
+  // });
+  test("Send Solana SPL to Ethereum and back", (done) => {
     (async () => {
       try {
         const { parse_vaa } = await importCoreWasm();
@@ -280,11 +167,12 @@ describe("Integration Tests", () => {
           keypair.publicKey
         );
 
-        // send to terra
+        // send to eth
         const transaction1 = await _transferFromSolana(
           fromAddress,
-          terraWallet.key.accAddress,
-          CHAIN_ID_TERRA
+          TEST_SOLANA_TOKEN,
+          signer.address,
+          CHAIN_ID_ETH
         );
         let signedVAA = await waitUntilSolanaTxObserved(transaction1);
 
@@ -293,28 +181,9 @@ describe("Integration Tests", () => {
           Buffer.from(new Uint8Array(parse_vaa(signedVAA).payload))
         );
 
-        await _redeemOnTerra(signedVAA);
-        const terra_addr = await nft_bridge.getForeignAssetTerra(
-          TERRA_NFT_BRIDGE_ADDRESS,
-          lcd,
-          CHAIN_ID_SOLANA,
-          tryNativeToUint8Array(TEST_SOLANA_TOKEN, CHAIN_ID_SOLANA)
-        );
-        if (!terra_addr) {
-          throw new Error("Terra address is null");
-        }
-        // send to ethereum
-        const transaction2 = await _transferFromTerra(
-          terra_addr,
-          tokenId.toString(),
-          signer.address,
-          CHAIN_ID_ETH
-        );
-        signedVAA = await waitUntilTerraTxObserved(transaction2);
-
         await _redeemOnEth(signedVAA);
         const eth_addr = await nft_bridge.getForeignAssetEth(
-          ETH_NFT_BRIDGE_ADDRESS,
+          CONTRACTS.DEVNET.ethereum.nft_bridge,
           provider,
           CHAIN_ID_SOLANA,
           tryNativeToUint8Array(TEST_SOLANA_TOKEN, CHAIN_ID_SOLANA)
@@ -350,39 +219,6 @@ describe("Integration Tests", () => {
       }
     })();
   });
-  test("Handles invalid utf8", (done) => {
-    (async () => {
-      const erc721 = await deployNFTOnEth(
-        // 31 bytes of valid characters + a 3 byte character
-        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaࠀ",
-        "test",
-        "https://foo.com",
-        1
-      );
-      const transaction = await _transferFromEth(
-        erc721.address,
-        0,
-        terraWallet.key.accAddress,
-        CHAIN_ID_TERRA
-      );
-      let signedVAA = await waitUntilEthTxObserved(transaction);
-      await _redeemOnTerra(signedVAA);
-      const terra_addr = await nft_bridge.getForeignAssetTerra(
-        TERRA_NFT_BRIDGE_ADDRESS,
-        lcd,
-        CHAIN_ID_ETH,
-        tryNativeToUint8Array(erc721.address, CHAIN_ID_ETH)
-      );
-      if (!terra_addr) {
-        throw new Error("Terra address is null");
-      }
-      const info: any = await lcd.wasm.contractQuery(terra_addr, {
-        contract_info: {},
-      });
-      expect(info.name).toBe("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa�");
-      done();
-    })();
-  });
 });
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -420,91 +256,17 @@ async function deployNFTOnEth(
   return NFTImplementation__factory.connect(nft.options.address, signer);
 }
 
-async function deployNFTOnTerra(
-  name: string,
-  symbol: string,
-  uri: string,
-  token_id: string
-): Promise<Address> {
-  var address: any;
-  await terraWallet
-    .createAndSignTx({
-      msgs: [
-        new MsgInstantiateContract(
-          terraWallet.key.accAddress,
-          terraWallet.key.accAddress,
-          TERRA_CW721_CODE_ID,
-          {
-            name,
-            symbol,
-            minter: terraWallet.key.accAddress,
-          }
-        ),
-      ],
-      memo: "",
-    })
-    .then((tx) => lcd.tx.broadcast(tx))
-    .then((rs) => {
-      const match = /"contract_address","value":"([^"]+)/gm.exec(rs.raw_log);
-      if (match) {
-        address = match[1];
-      }
-    });
-  await mint_cw721(address, token_id, uri);
-  return address;
-}
-
-async function getGasPrices() {
-  return axios.get(TERRA_GAS_PRICES_URL).then((result) => result.data);
-}
-
-async function estimateTerraFee(
-  gasPrices: Coins.Input,
-  msgs: Msg[]
-): Promise<Fee> {
-  const feeEstimate = await lcd.tx.estimateFee(
-    [
-      {
-        sequenceNumber: await terraWallet.sequence(),
-        publicKey: terraWallet.key.publicKey,
-      },
-    ],
-    {
-      msgs,
-      memo: "localhost",
-      feeDenoms: ["uluna"],
-      gasPrices,
-    }
-  );
-  return feeEstimate;
-}
-
-async function waitUntilTerraTxObserved(
-  txresult: WaitTxBroadcastResult
-): Promise<Uint8Array> {
-  // get the sequence from the logs (needed to fetch the vaa)
-  const info = await waitForTerraExecution(txresult.txhash);
-  const sequence = parseSequenceFromLogTerra(info);
-  const emitterAddress = await getEmitterAddressTerra(TERRA_NFT_BRIDGE_ADDRESS);
-  // poll until the guardian(s) witness and sign the vaa
-  const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
-    WORMHOLE_RPC_HOSTS,
-    CHAIN_ID_TERRA,
-    emitterAddress,
-    sequence,
-    {
-      transport: NodeHttpTransport(),
-    }
-  );
-  return signedVAA;
-}
-
 async function waitUntilEthTxObserved(
   receipt: ethers.ContractReceipt
 ): Promise<Uint8Array> {
   // get the sequence from the logs (needed to fetch the vaa)
-  let sequence = parseSequenceFromLogEth(receipt, ETH_CORE_BRIDGE_ADDRESS);
-  let emitterAddress = getEmitterAddressEth(ETH_NFT_BRIDGE_ADDRESS);
+  let sequence = parseSequenceFromLogEth(
+    receipt,
+    CONTRACTS.DEVNET.ethereum.core
+  );
+  let emitterAddress = getEmitterAddressEth(
+    CONTRACTS.DEVNET.ethereum.nft_bridge
+  );
   // poll until the guardian(s) witness and sign the vaa
   const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
     WORMHOLE_RPC_HOSTS,
@@ -524,7 +286,7 @@ async function waitUntilSolanaTxObserved(
   // get the sequence from the logs (needed to fetch the vaa)
   const sequence = parseSequenceFromLogSolana(response);
   const emitterAddress = await getEmitterAddressSolana(
-    SOLANA_NFT_BRIDGE_ADDRESS
+    CONTRACTS.DEVNET.solana.nft_bridge
   );
   // poll until the guardian(s) witness and sign the vaa
   const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
@@ -539,67 +301,10 @@ async function waitUntilSolanaTxObserved(
   return signedVAA;
 }
 
-async function mint_cw721(
-  contract_address: string,
-  token_id: string,
-  token_uri: any
-): Promise<void> {
-  await terraWallet
-    .createAndSignTx({
-      msgs: [
-        new MsgExecuteContract(
-          terraWallet.key.accAddress,
-          contract_address,
-          {
-            mint: {
-              token_id,
-              owner: terraWallet.key.accAddress,
-              token_uri: token_uri,
-            },
-          },
-          { uluna: 1000 }
-        ),
-      ],
-      memo: "",
-      fee: new Fee(2000000, {
-        uluna: "100000",
-      }),
-    })
-    .then((tx) => lcd.tx.broadcast(tx));
-}
-
-async function waitForTerraExecution(txHash: string): Promise<TxInfo> {
-  let info: TxInfo | undefined = undefined;
-  while (!info) {
-    await new Promise((resolve) => setTimeout(resolve, 1000));
-    try {
-      info = await lcd.tx.txInfo(txHash);
-    } catch (e) {
-      console.error(e);
-    }
-  }
-  if (isTxError(info)) {
-    // error code
-    throw new Error(`Tx ${txHash}: error code ${info.code}: ${info.raw_log}`);
-  }
-  return info;
-}
-
-async function expectReceivedOnTerra(signedVAA: Uint8Array) {
-  return expect(
-    await nft_bridge.getIsTransferCompletedTerra(
-      TERRA_NFT_BRIDGE_ADDRESS,
-      signedVAA,
-      lcd,
-      TERRA_GAS_PRICES_URL
-    )
-  );
-}
-
 async function expectReceivedOnEth(signedVAA: Uint8Array) {
   return expect(
     await nft_bridge.getIsTransferCompletedEth(
-      ETH_NFT_BRIDGE_ADDRESS,
+      CONTRACTS.DEVNET.ethereum.nft_bridge,
       provider,
       signedVAA
     )
@@ -613,7 +318,7 @@ async function _transferFromEth(
   chain: ChainId
 ): Promise<ethers.ContractReceipt> {
   return nft_bridge.transferFromEth(
-    ETH_NFT_BRIDGE_ADDRESS,
+    CONTRACTS.DEVNET.ethereum.nft_bridge,
     signer,
     erc721,
     token_id,
@@ -622,45 +327,27 @@ async function _transferFromEth(
   );
 }
 
-async function _transferFromTerra(
-  terra_addr: string,
-  token_id: string,
-  address: string,
-  chain: ChainId
-): Promise<WaitTxBroadcastResult> {
-  const gasPrices = await getGasPrices();
-  const msgs = await nft_bridge.transferFromTerra(
-    terraWallet.key.accAddress,
-    TERRA_NFT_BRIDGE_ADDRESS,
-    terra_addr,
-    token_id,
-    chain,
-    tryNativeToUint8Array(address, chain)
-  );
-  const tx = await terraWallet.createAndSignTx({
-    msgs: msgs,
-    memo: "test",
-    feeDenoms: ["uluna"],
-    gasPrices,
-    fee: await estimateTerraFee(gasPrices, msgs),
-  });
-  return lcd.tx.broadcast(tx);
-}
-
 async function _transferFromSolana(
   fromAddress: PublicKey,
+  tokenAddress: string,
   targetAddress: string,
-  chain: ChainId
+  chain: ChainId,
+  originAddress?: Uint8Array,
+  originChain?: ChainId,
+  originTokenId?: Uint8Array
 ): Promise<TransactionResponse> {
   const transaction = await nft_bridge.transferFromSolana(
     connection,
-    SOLANA_CORE_BRIDGE_ADDRESS,
-    SOLANA_NFT_BRIDGE_ADDRESS,
+    CONTRACTS.DEVNET.solana.core,
+    CONTRACTS.DEVNET.solana.nft_bridge,
     payerAddress,
     fromAddress.toString(),
-    TEST_SOLANA_TOKEN,
+    tokenAddress,
     tryNativeToUint8Array(targetAddress, chain),
-    chain
+    chain,
+    originAddress,
+    originChain,
+    originTokenId
   );
   // sign, send, and confirm transaction
   transaction.partialSign(keypair);
@@ -676,26 +363,11 @@ async function _transferFromSolana(
 async function _redeemOnEth(
   signedVAA: Uint8Array
 ): Promise<ethers.ContractReceipt> {
-  return nft_bridge.redeemOnEth(ETH_NFT_BRIDGE_ADDRESS, signer, signedVAA);
-}
-
-async function _redeemOnTerra(
-  signedVAA: Uint8Array
-): Promise<WaitTxBroadcastResult> {
-  const msg = await nft_bridge.redeemOnTerra(
-    TERRA_NFT_BRIDGE_ADDRESS,
-    terraWallet.key.accAddress,
+  return nft_bridge.redeemOnEth(
+    CONTRACTS.DEVNET.ethereum.nft_bridge,
+    signer,
     signedVAA
   );
-  const gasPrices = await getGasPrices();
-  const tx = await terraWallet.createAndSignTx({
-    msgs: [msg],
-    memo: "localhost",
-    feeDenoms: ["uluna"],
-    gasPrices,
-    fee: await estimateTerraFee(gasPrices, [msg]),
-  });
-  return lcd.tx.broadcast(tx);
 }
 
 async function _redeemOnSolana(signedVAA: Uint8Array) {
@@ -706,9 +378,24 @@ async function _redeemOnSolana(signedVAA: Uint8Array) {
       transaction.partialSign(keypair);
       return transaction;
     },
-    SOLANA_CORE_BRIDGE_ADDRESS,
+    CONTRACTS.DEVNET.solana.core,
     payerAddress,
     Buffer.from(signedVAA),
     maxRetries
   );
+  const transaction = await nft_bridge.redeemOnSolana(
+    connection,
+    CONTRACTS.DEVNET.solana.core,
+    CONTRACTS.DEVNET.solana.nft_bridge,
+    payerAddress,
+    signedVAA
+  );
+  transaction.partialSign(keypair);
+  const txid = await connection.sendRawTransaction(transaction.serialize());
+  await connection.confirmTransaction(txid);
+  const info = await connection.getTransaction(txid);
+  if (!info) {
+    throw new Error("An error occurred while fetching the transaction info");
+  }
+  return info;
 }

+ 1051 - 0
sdk/js/src/token_bridge/__tests__/algorand-integration.ts

@@ -0,0 +1,1051 @@
+import { parseUnits } from "@ethersproject/units";
+import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
+import { describe, expect, jest, test } from "@jest/globals";
+import algosdk, {
+  Account,
+  decodeAddress,
+  getApplicationAddress,
+  makeApplicationCallTxnFromObject,
+  OnApplicationComplete,
+  waitForConfirmation,
+} from "algosdk";
+import { BigNumber, ethers, utils } from "ethers";
+import {
+  approveEth,
+  attestFromAlgorand,
+  attestFromEth,
+  CHAIN_ID_ALGORAND,
+  CHAIN_ID_ETH,
+  CONTRACTS,
+  createWrappedOnAlgorand,
+  createWrappedOnEth,
+  getEmitterAddressAlgorand,
+  getEmitterAddressEth,
+  getForeignAssetEth,
+  getIsTransferCompletedAlgorand,
+  getIsTransferCompletedEth,
+  getOriginalAssetAlgorand,
+  hexToUint8Array,
+  nativeToHexString,
+  parseSequenceFromLogAlgorand,
+  parseSequenceFromLogEth,
+  redeemOnAlgorand,
+  redeemOnEth,
+  textToUint8Array,
+  TokenImplementation__factory,
+  transferFromAlgorand,
+  transferFromEth,
+  uint8ArrayToHex,
+  updateWrappedOnEth,
+  WormholeWrappedInfo,
+} from "../..";
+import { _parseVAAAlgorand } from "../../algorand";
+import {
+  createAsset,
+  getAlgoClient,
+  getBalance,
+  getBalances,
+  getForeignAssetFromVaaAlgorand,
+  getTempAccounts,
+  signSendAndConfirmAlgorand,
+} from "../../algorand/__tests__/testHelpers";
+import getSignedVAAWithRetry from "../../rpc/getSignedVAAWithRetry";
+import { setDefaultWasm } from "../../solana/wasm";
+import { safeBigIntToNumber } from "../../utils/bigint";
+import {
+  ETH_NODE_URL,
+  ETH_PRIVATE_KEY7,
+  TEST_ERC20,
+  WORMHOLE_RPC_HOSTS,
+} from "./consts";
+
+const CORE_ID = BigInt(4);
+const TOKEN_BRIDGE_ID = BigInt(6);
+
+setDefaultWasm("node");
+
+jest.setTimeout(60000);
+
+describe("Algorand tests", () => {
+  test("Algorand transfer native ALGO to Eth and back again", (done) => {
+    (async () => {
+      try {
+        const client: algosdk.Algodv2 = getAlgoClient();
+        const tempAccts: Account[] = await getTempAccounts();
+        const numAccts: number = tempAccts.length;
+        expect(numAccts).toBeGreaterThan(0);
+        const wallet: Account = tempAccts[0];
+
+        // let accountInfo = await client.accountInformation(wallet.addr).do();
+        // Asset Index of native ALGO is 0
+        const AlgoIndex = BigInt(0);
+        // const b = await getBalances(client, wallet.addr);
+        const txs = await attestFromAlgorand(
+          client,
+          TOKEN_BRIDGE_ID,
+          CORE_ID,
+          wallet.addr,
+          AlgoIndex
+        );
+
+        const result = await signSendAndConfirmAlgorand(client, txs, wallet);
+
+        const sn = parseSequenceFromLogAlgorand(result);
+
+        // Now, try to send a NOP
+        const suggParams: algosdk.SuggestedParams = await client
+          .getTransactionParams()
+          .do();
+        const nopTxn = makeApplicationCallTxnFromObject({
+          from: wallet.addr,
+          appIndex: safeBigIntToNumber(TOKEN_BRIDGE_ID),
+          onComplete: OnApplicationComplete.NoOpOC,
+          appArgs: [textToUint8Array("nop")],
+          suggestedParams: suggParams,
+        });
+        const resp = await client
+          .sendRawTransaction(nopTxn.signTxn(wallet.sk))
+          .do();
+        await waitForConfirmation(client, resp.txId, 1);
+        // End of NOP
+
+        const emitterAddr = getEmitterAddressAlgorand(TOKEN_BRIDGE_ID);
+        const { vaaBytes } = await getSignedVAAWithRetry(
+          WORMHOLE_RPC_HOSTS,
+          CHAIN_ID_ALGORAND,
+          emitterAddr,
+          sn,
+          { transport: NodeHttpTransport() }
+        );
+        const pvaa = _parseVAAAlgorand(vaaBytes);
+        const provider = new ethers.providers.WebSocketProvider(
+          ETH_NODE_URL
+        ) as any;
+        const signer = new ethers.Wallet(ETH_PRIVATE_KEY7, provider);
+        let success: boolean = true;
+        try {
+          const cr = await createWrappedOnEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            signer,
+            vaaBytes
+          );
+        } catch (e) {
+          success = false;
+        }
+        if (!success) {
+          try {
+            const cr = await updateWrappedOnEth(
+              CONTRACTS.DEVNET.ethereum.token_bridge,
+              signer,
+              vaaBytes
+            );
+            success = true;
+          } catch (e) {
+            console.error("failed to updateWrappedOnEth", e);
+          }
+        }
+        // Check wallet
+        const a = parseInt(AlgoIndex.toString());
+        const originAssetHex = (
+          "0000000000000000000000000000000000000000000000000000000000000000" +
+          a.toString(16)
+        ).slice(-64);
+        const foreignAsset = await getForeignAssetEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          provider,
+          CHAIN_ID_ALGORAND,
+          hexToUint8Array(originAssetHex)
+        );
+        if (!foreignAsset) {
+          throw new Error("foreignAsset is null");
+        }
+        let token = TokenImplementation__factory.connect(foreignAsset, signer);
+
+        // Get initial balance on ethereum
+        const initialBalOnEth = await token.balanceOf(
+          await signer.getAddress()
+        );
+        const initialBalOnEthInt = parseInt(initialBalOnEth._hex);
+
+        // Get initial balance on Algorand
+        let algoWalletBals: Map<number, number> = await getBalances(
+          client,
+          wallet.addr
+        );
+        const startingAlgoBal = algoWalletBals.get(
+          safeBigIntToNumber(AlgoIndex)
+        );
+        if (!startingAlgoBal) {
+          throw new Error("startingAlgoBal is undefined");
+        }
+
+        // Start transfer from Algorand to Ethereum
+        const hexStr = nativeToHexString(
+          await signer.getAddress(),
+          CHAIN_ID_ETH
+        );
+        if (!hexStr) {
+          throw new Error("Failed to convert to hexStr");
+        }
+        const AmountToTransfer: number = 12300;
+        const Fee: number = 0;
+        const transferTxs = await transferFromAlgorand(
+          client,
+          TOKEN_BRIDGE_ID,
+          CORE_ID,
+          wallet.addr,
+          AlgoIndex,
+          BigInt(AmountToTransfer),
+          hexStr,
+          CHAIN_ID_ETH,
+          BigInt(Fee)
+        );
+        const transferResult = await signSendAndConfirmAlgorand(
+          client,
+          transferTxs,
+          wallet
+        );
+        const txSid = parseSequenceFromLogAlgorand(transferResult);
+        const signedVaa = await getSignedVAAWithRetry(
+          WORMHOLE_RPC_HOSTS,
+          CHAIN_ID_ALGORAND,
+          emitterAddr,
+          txSid,
+          { transport: NodeHttpTransport() }
+        );
+        const pv = _parseVAAAlgorand(signedVaa.vaaBytes);
+        const roe = await redeemOnEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          signer,
+          signedVaa.vaaBytes
+        );
+        expect(
+          await getIsTransferCompletedEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            provider,
+            signedVaa.vaaBytes
+          )
+        ).toBe(true);
+        // Test finished.  Check wallet balances
+        const balOnEthAfter = await token.balanceOf(await signer.getAddress());
+        const balOnEthAfterInt = parseInt(balOnEthAfter._hex);
+        expect(balOnEthAfterInt - initialBalOnEthInt).toEqual(AmountToTransfer);
+
+        // Get final balance on Algorand
+        algoWalletBals = await getBalances(client, wallet.addr);
+        const finalAlgoBal = algoWalletBals.get(safeBigIntToNumber(AlgoIndex));
+        if (!finalAlgoBal) {
+          throw new Error("finalAlgoBal is undefined");
+        }
+        // expect(startingAlgoBal - finalAlgoBal).toBe(AmountToTransfer);
+
+        // Attempt to transfer from Eth back to Algorand
+        const Amount: string = "100";
+
+        // approve the bridge to spend tokens
+        await approveEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          foreignAsset,
+          signer,
+          Amount
+        );
+        const receipt = await transferFromEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          signer,
+          foreignAsset,
+          Amount,
+          CHAIN_ID_ALGORAND,
+          decodeAddress(wallet.addr).publicKey
+        );
+        // get the sequence from the logs (needed to fetch the vaa)
+        const sequence = parseSequenceFromLogEth(
+          receipt,
+          CONTRACTS.DEVNET.ethereum.core
+        );
+        const emitterAddress = getEmitterAddressEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge
+        );
+
+        // poll until the guardian(s) witness and sign the vaa
+        const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
+          WORMHOLE_RPC_HOSTS,
+          CHAIN_ID_ETH,
+          emitterAddress,
+          sequence,
+          {
+            transport: NodeHttpTransport(),
+          }
+        );
+        algoWalletBals = await getBalances(client, wallet.addr);
+        const redeemTxs = await redeemOnAlgorand(
+          client,
+          TOKEN_BRIDGE_ID,
+          CORE_ID,
+          signedVAA,
+          wallet.addr
+        );
+        await signSendAndConfirmAlgorand(client, redeemTxs, wallet);
+        const completed = await getIsTransferCompletedAlgorand(
+          client,
+          TOKEN_BRIDGE_ID,
+          signedVAA
+        );
+        expect(completed).toBe(true);
+
+        // Get second final balance on Algorand
+        algoWalletBals = await getBalances(client, wallet.addr);
+        const secondFinalAlgoBal = algoWalletBals.get(
+          safeBigIntToNumber(AlgoIndex)
+        );
+        if (!secondFinalAlgoBal) {
+          throw new Error("secondFinalAlgoBal is undefined");
+        }
+
+        provider.destroy();
+      } catch (e) {
+        console.error("Algorand ALGO transfer error:", e);
+        done("Algorand ALGO transfer error");
+        return;
+      }
+      done();
+    })();
+  });
+  test("Algorand create chuckNorium, transfer to Eth and back again", (done) => {
+    (async () => {
+      try {
+        const client: algosdk.Algodv2 = getAlgoClient();
+        const tempAccts: Account[] = await getTempAccounts();
+        const numAccts: number = tempAccts.length;
+        expect(numAccts).toBeGreaterThan(0);
+        const wallet: Account = tempAccts[0];
+
+        // let accountInfo = await client.accountInformation(wallet.addr).do();
+
+        const assetIndex: number = await createAsset(wallet);
+        const attestTxs = await attestFromAlgorand(
+          client,
+          TOKEN_BRIDGE_ID,
+          CORE_ID,
+          wallet.addr,
+          BigInt(assetIndex)
+        );
+        const attestResult = await signSendAndConfirmAlgorand(
+          client,
+          attestTxs,
+          wallet
+        );
+        const attestSn = parseSequenceFromLogAlgorand(attestResult);
+
+        // Now, try to send a NOP
+        const suggParams: algosdk.SuggestedParams = await client
+          .getTransactionParams()
+          .do();
+        const nopTxn = makeApplicationCallTxnFromObject({
+          from: wallet.addr,
+          appIndex: safeBigIntToNumber(TOKEN_BRIDGE_ID),
+          onComplete: OnApplicationComplete.NoOpOC,
+          appArgs: [textToUint8Array("nop")],
+          suggestedParams: suggParams,
+        });
+        const resp = await client
+          .sendRawTransaction(nopTxn.signTxn(wallet.sk))
+          .do();
+        await waitForConfirmation(client, resp.txId, 1);
+        // End of NOP
+
+        const emitterAddr = getEmitterAddressAlgorand(TOKEN_BRIDGE_ID);
+        const { vaaBytes } = await getSignedVAAWithRetry(
+          WORMHOLE_RPC_HOSTS,
+          CHAIN_ID_ALGORAND,
+          emitterAddr,
+          attestSn,
+          { transport: NodeHttpTransport() }
+        );
+        const provider = new ethers.providers.WebSocketProvider(
+          ETH_NODE_URL
+        ) as any;
+        const signer = new ethers.Wallet(ETH_PRIVATE_KEY7, provider);
+        let success: boolean = true;
+        try {
+          const cr = await createWrappedOnEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            signer,
+            vaaBytes
+          );
+        } catch (e) {
+          success = false;
+        }
+        if (!success) {
+          try {
+            const cr = await updateWrappedOnEth(
+              CONTRACTS.DEVNET.ethereum.token_bridge,
+              signer,
+              vaaBytes
+            );
+            success = true;
+          } catch (e) {
+            console.error("failed to updateWrappedOnEth", e);
+            done("failed to update attestation on Eth");
+            return;
+          }
+        }
+        // Check wallet
+        const a = parseInt(assetIndex.toString());
+        const originAssetHex = (
+          "0000000000000000000000000000000000000000000000000000000000000000" +
+          a.toString(16)
+        ).slice(-64);
+        const foreignAsset = await getForeignAssetEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          provider,
+          CHAIN_ID_ALGORAND,
+          hexToUint8Array(originAssetHex)
+        );
+        if (!foreignAsset) {
+          throw new Error("foreignAsset is null");
+        }
+        let token = TokenImplementation__factory.connect(foreignAsset, signer);
+
+        // Get initial balance on ethereum
+
+        // Get initial balance on Algorand
+        let algoWalletBals: Map<number, number> = await getBalances(
+          client,
+          wallet.addr
+        );
+        const startingAlgoBal = algoWalletBals.get(assetIndex);
+        if (!startingAlgoBal) {
+          throw new Error("startingAlgoBal is undefined");
+        }
+
+        // Start transfer from Algorand to Ethereum
+        const hexStr = nativeToHexString(
+          await signer.getAddress(),
+          CHAIN_ID_ETH
+        );
+        if (!hexStr) {
+          throw new Error("Failed to convert to hexStr");
+        }
+        const AmountToTransfer: number = 12300;
+        const Fee: number = 0;
+        const transferTxs = await transferFromAlgorand(
+          client,
+          TOKEN_BRIDGE_ID,
+          CORE_ID,
+          wallet.addr,
+          BigInt(assetIndex),
+          BigInt(AmountToTransfer),
+          hexStr,
+          CHAIN_ID_ETH,
+          BigInt(Fee)
+        );
+        const transferResult = await signSendAndConfirmAlgorand(
+          client,
+          transferTxs,
+          wallet
+        );
+        const txSid = parseSequenceFromLogAlgorand(transferResult);
+        const signedVaa = await getSignedVAAWithRetry(
+          WORMHOLE_RPC_HOSTS,
+          CHAIN_ID_ALGORAND,
+          emitterAddr,
+          txSid,
+          { transport: NodeHttpTransport() }
+        );
+        await redeemOnEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          signer,
+          signedVaa.vaaBytes
+        );
+        expect(
+          await getIsTransferCompletedEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            provider,
+            signedVaa.vaaBytes
+          )
+        ).toBe(true);
+        // Test finished.  Check wallet balances
+        const balOnEthAfter = await token.balanceOf(await signer.getAddress());
+        const balOnEthAfterInt = parseInt(balOnEthAfter._hex);
+        const FinalAmt: number = AmountToTransfer / 100;
+        expect(balOnEthAfterInt).toEqual(FinalAmt);
+
+        // Get final balance on Algorand
+        algoWalletBals = await getBalances(client, wallet.addr);
+        const finalAlgoBal = algoWalletBals.get(assetIndex);
+        if (!finalAlgoBal) {
+          throw new Error("finalAlgoBal is undefined");
+        }
+        expect(startingAlgoBal - finalAlgoBal).toBe(AmountToTransfer);
+
+        // Attempt to transfer from Eth back to Algorand
+        const Amount: string = "100";
+
+        // approve the bridge to spend tokens
+        await approveEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          foreignAsset,
+          signer,
+          Amount
+        );
+        const receipt = await transferFromEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          signer,
+          foreignAsset,
+          Amount,
+          CHAIN_ID_ALGORAND,
+          decodeAddress(wallet.addr).publicKey
+        );
+        // get the sequence from the logs (needed to fetch the vaa)
+        const sequence = parseSequenceFromLogEth(
+          receipt,
+          CONTRACTS.DEVNET.ethereum.core
+        );
+        const emitterAddress = getEmitterAddressEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge
+        );
+
+        // poll until the guardian(s) witness and sign the vaa
+        const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
+          WORMHOLE_RPC_HOSTS,
+          CHAIN_ID_ETH,
+          emitterAddress,
+          sequence,
+          {
+            transport: NodeHttpTransport(),
+          }
+        );
+        const redeemTxs = await redeemOnAlgorand(
+          client,
+          TOKEN_BRIDGE_ID,
+          CORE_ID,
+          signedVAA,
+          wallet.addr
+        );
+        await signSendAndConfirmAlgorand(client, redeemTxs, wallet);
+        const completed = await getIsTransferCompletedAlgorand(
+          client,
+          TOKEN_BRIDGE_ID,
+          signedVAA
+        );
+        expect(completed).toBe(true);
+        const newBal = await token.balanceOf(await signer.getAddress());
+        const newBalInt = parseInt(newBal._hex);
+        expect(newBalInt).toBe(FinalAmt - parseInt(Amount));
+
+        // Get second final balance on Algorand
+        algoWalletBals = await getBalances(client, wallet.addr);
+        const secondFinalAlgoBal = algoWalletBals.get(assetIndex);
+        if (!secondFinalAlgoBal) {
+          throw new Error("secondFinalAlgoBal is undefined");
+        }
+        expect(secondFinalAlgoBal - finalAlgoBal).toBe(parseInt(Amount) * 100);
+        provider.destroy();
+      } catch (e) {
+        console.error("Algorand chuckNorium transfer error:", e);
+        done("Algorand chuckNorium transfer error");
+        return;
+      }
+      done();
+    })();
+  });
+  test("Transfer ERC-20 from Eth to Algorand and back again", (done) => {
+    (async () => {
+      try {
+        const tbAddr: string = getApplicationAddress(TOKEN_BRIDGE_ID);
+        const decTbAddr: Uint8Array = decodeAddress(tbAddr).publicKey;
+        const aa: string = uint8ArrayToHex(decTbAddr);
+        const client: algosdk.Algodv2 = getAlgoClient();
+        const tempAccts: Account[] = await getTempAccounts();
+        const numAccts: number = tempAccts.length;
+        expect(numAccts).toBeGreaterThan(0);
+        const algoWallet: Account = tempAccts[0];
+        const Amount = "10";
+        // create a signer for Eth
+        const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
+        const signer = new ethers.Wallet(ETH_PRIVATE_KEY7, provider);
+        // attest the test token
+        const attestReceipt = await attestFromEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          signer,
+          TEST_ERC20
+        );
+        // get the sequence from the logs (needed to fetch the vaa)
+        const attestSequence = parseSequenceFromLogEth(
+          attestReceipt,
+          CONTRACTS.DEVNET.ethereum.core
+        );
+        const emitterAddress = getEmitterAddressEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge
+        );
+        // poll until the guardian(s) witness and sign the vaa
+        const { vaaBytes: attestSignedVaa } = await getSignedVAAWithRetry(
+          WORMHOLE_RPC_HOSTS,
+          CHAIN_ID_ETH,
+          emitterAddress,
+          attestSequence,
+          {
+            transport: NodeHttpTransport(),
+          }
+        );
+        const createWrappedTxs = await createWrappedOnAlgorand(
+          client,
+          TOKEN_BRIDGE_ID,
+          CORE_ID,
+          algoWallet.addr,
+          attestSignedVaa
+        );
+        await signSendAndConfirmAlgorand(client, createWrappedTxs, algoWallet);
+
+        let assetIdCreated = await getForeignAssetFromVaaAlgorand(
+          client,
+          TOKEN_BRIDGE_ID,
+          attestSignedVaa
+        );
+        if (!assetIdCreated) {
+          throw new Error("Failed to create asset");
+        }
+
+        // Start of transfer from Eth to Algorand
+        let token = TokenImplementation__factory.connect(TEST_ERC20, signer);
+        // Get initial balance on ethereum
+        const initialBalOnEth = await token.balanceOf(
+          await signer.getAddress()
+        );
+        const initialBalOnEthInt = parseInt(initialBalOnEth._hex);
+
+        // Get initial balance of TEST_ERC20 on Algorand
+        const originAssetHex = nativeToHexString(TEST_ERC20, CHAIN_ID_ETH);
+        if (!originAssetHex) {
+          throw new Error("originAssetHex is null");
+        }
+        // TODO:  Get wallet balance on Algorand
+
+        // Get Balances
+        const tbBals: Map<number, number> = await getBalances(
+          client,
+          algoWallet.addr
+          // "TPFKQBOR7RJ475XW6XMOZMSMBCZH6WNGFQNT7CM7NL2UMBCMBIU5PVBGPM"
+        );
+        let assetIdCreatedBegBal: number = 0;
+        const tempBal = tbBals.get(safeBigIntToNumber(assetIdCreated));
+        if (tempBal) {
+          assetIdCreatedBegBal = tempBal;
+        }
+
+        // Start transfer from Eth to Algorand
+        const parsedAmount = parseUnits(Amount, 18);
+        const expectedAmount = parseUnits(Amount, 8);
+        await approveEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          TEST_ERC20,
+          signer,
+          parsedAmount
+        );
+        const receipt = await transferFromEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          signer,
+          TEST_ERC20,
+          parsedAmount,
+          CHAIN_ID_ALGORAND,
+          decodeAddress(algoWallet.addr).publicKey // This needs to be Algorand wallet
+        );
+        const transferSequence = parseSequenceFromLogEth(
+          receipt,
+          CONTRACTS.DEVNET.ethereum.core
+        );
+        // poll until the guardian(s) witness and sign the vaa
+        const { vaaBytes: transferSignedVaa } = await getSignedVAAWithRetry(
+          WORMHOLE_RPC_HOSTS,
+          CHAIN_ID_ETH,
+          emitterAddress,
+          transferSequence,
+          {
+            transport: NodeHttpTransport(),
+          }
+        );
+        const redeemTxs = await redeemOnAlgorand(
+          client,
+          TOKEN_BRIDGE_ID,
+          CORE_ID,
+          transferSignedVaa,
+          algoWallet.addr
+        );
+        await signSendAndConfirmAlgorand(client, redeemTxs, algoWallet);
+        expect(
+          await getIsTransferCompletedAlgorand(
+            client,
+            TOKEN_BRIDGE_ID,
+            transferSignedVaa
+          )
+        ).toBe(true);
+
+        // Test finished.  Check wallet balances
+        // Get Balances
+        const bals: Map<number, number> = await getBalances(
+          client,
+          algoWallet.addr
+        );
+        let assetIdCreatedEndBal: number = 0;
+        const tmpBal = bals.get(safeBigIntToNumber(assetIdCreated));
+        if (tmpBal) {
+          assetIdCreatedEndBal = tmpBal;
+        }
+        expect(assetIdCreatedEndBal - assetIdCreatedBegBal).toBe(
+          expectedAmount.toNumber()
+        );
+
+        // Get intermediate balance of test token on Eth
+        const midBalOnEth = await token.balanceOf(await signer.getAddress());
+        const midBalOnEthInt = parseInt(midBalOnEth._hex);
+
+        expect(
+          BigInt(initialBalOnEthInt) - parsedAmount.toBigInt() ===
+            BigInt(midBalOnEthInt)
+        ).toBe(true);
+
+        // Start of transfer back to Eth
+        const TransferBackAmount: number = parseUnits("1", 8).toNumber();
+
+        // transfer wrapped luna from Algorand to Eth
+        const hexStr = nativeToHexString(
+          await signer.getAddress(),
+          CHAIN_ID_ETH
+        );
+        if (!hexStr) {
+          throw new Error("Failed to convert to hexStr");
+        }
+        const Fee: number = 0;
+        const transferTxs = await transferFromAlgorand(
+          client,
+          TOKEN_BRIDGE_ID,
+          CORE_ID,
+          algoWallet.addr,
+          assetIdCreated,
+          BigInt(TransferBackAmount),
+          hexStr,
+          CHAIN_ID_ETH,
+          BigInt(Fee)
+        );
+        const transferResult = await signSendAndConfirmAlgorand(
+          client,
+          transferTxs,
+          algoWallet
+        );
+        const txSid = parseSequenceFromLogAlgorand(transferResult);
+        const signedVaa = await getSignedVAAWithRetry(
+          WORMHOLE_RPC_HOSTS,
+          CHAIN_ID_ALGORAND,
+          aa,
+          txSid,
+          { transport: NodeHttpTransport() }
+        );
+
+        const roe = await redeemOnEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          signer,
+          signedVaa.vaaBytes
+        );
+        expect(
+          await getIsTransferCompletedEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            provider,
+            signedVaa.vaaBytes
+          )
+        ).toBe(true);
+
+        // Check wallet balances after
+        const finalBalOnEth = await token.balanceOf(await signer.getAddress());
+        const finalBalOnEthInt = parseInt(finalBalOnEth._hex);
+        expect(BigInt(finalBalOnEthInt - midBalOnEthInt)).toBe(
+          parseUnits("1", 18).toBigInt()
+        );
+        const retBals: Map<number, number> = await getBalances(
+          client,
+          algoWallet.addr
+        );
+        let assetIdCreatedFinBal: number = 0;
+        const tBal = retBals.get(safeBigIntToNumber(assetIdCreated));
+        if (tBal) {
+          assetIdCreatedFinBal = tBal;
+        }
+        expect(assetIdCreatedEndBal - assetIdCreatedFinBal).toBe(
+          TransferBackAmount
+        );
+        const info: WormholeWrappedInfo = await getOriginalAssetAlgorand(
+          client,
+          TOKEN_BRIDGE_ID,
+          assetIdCreated
+        );
+        expect(info.chainId).toBe(CHAIN_ID_ETH);
+        expect(info.isWrapped).toBe(true);
+        provider.destroy();
+      } catch (e) {
+        console.error("Eth <=> Algorand error:", e);
+        done("Eth <=> Algorand error");
+        return;
+      }
+      done();
+    })();
+  });
+  test("Testing relay type redeem", (done) => {
+    (async () => {
+      try {
+        const client: algosdk.Algodv2 = getAlgoClient();
+        const tempAccts: Account[] = await getTempAccounts();
+        const numAccts: number = tempAccts.length;
+        expect(numAccts).toBeGreaterThan(0);
+        const algoWallet: Account = tempAccts[0];
+        const algoWalletBalance = await getBalance(
+          client,
+          algoWallet.addr,
+          BigInt(0)
+        );
+        expect(algoWalletBalance).toBeGreaterThan(0);
+        const relayerWallet: Account = tempAccts[1];
+        const relayerWalletBalance = await getBalance(
+          client,
+          relayerWallet.addr,
+          BigInt(0)
+        );
+        expect(relayerWalletBalance).toBeGreaterThan(0);
+        // ETH setup to transfer LUNA to Algorand
+
+        // create a signer for Eth
+        const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
+        const signer = new ethers.Wallet(ETH_PRIVATE_KEY7, provider);
+        // attest the test token
+        const receipt = await attestFromEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          signer,
+          TEST_ERC20
+        );
+        // get the sequence from the logs (needed to fetch the vaa)
+        const sequence = parseSequenceFromLogEth(
+          receipt,
+          CONTRACTS.DEVNET.ethereum.core
+        );
+        const emitterAddress = getEmitterAddressEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge
+        );
+        // poll until the guardian(s) witness and sign the vaa
+        const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
+          WORMHOLE_RPC_HOSTS,
+          CHAIN_ID_ETH,
+          emitterAddress,
+          sequence,
+          {
+            transport: NodeHttpTransport(),
+          }
+        );
+        const createWrappedTxs = await createWrappedOnAlgorand(
+          client,
+          TOKEN_BRIDGE_ID,
+          CORE_ID,
+          algoWallet.addr,
+          signedVAA
+        );
+        await signSendAndConfirmAlgorand(client, createWrappedTxs, algoWallet);
+
+        let assetIdCreated = await getForeignAssetFromVaaAlgorand(
+          client,
+          TOKEN_BRIDGE_ID,
+          signedVAA
+        );
+        if (!assetIdCreated) {
+          throw new Error("Failed to create asset");
+        }
+
+        // Start of transfer from ETH to Algorand
+        // approve the bridge to spend tokens
+        const amount = parseUnits("2", 18);
+        const halfAmount = parseUnits("1", 18);
+        await approveEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          TEST_ERC20,
+          signer,
+          amount
+        );
+        // transfer half the tokens directly
+        const firstHalfReceipt = await transferFromEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          signer,
+          TEST_ERC20,
+          halfAmount,
+          CHAIN_ID_ALGORAND,
+          decodeAddress(algoWallet.addr).publicKey // This needs to be Algorand wallet
+        );
+        // get the sequence from the logs (needed to fetch the vaa)
+        const firstHalfSn = parseSequenceFromLogEth(
+          firstHalfReceipt,
+          CONTRACTS.DEVNET.ethereum.core
+        );
+        const ethEmitterAddress = getEmitterAddressEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge
+        );
+        // poll until the guardian(s) witness and sign the vaa
+        const { vaaBytes: firstHalfVaa } = await getSignedVAAWithRetry(
+          WORMHOLE_RPC_HOSTS,
+          CHAIN_ID_ETH,
+          ethEmitterAddress,
+          firstHalfSn,
+          {
+            transport: NodeHttpTransport(),
+          }
+        );
+
+        // Redeem half the amount on Algorand
+        const firstHalfRedeemTxs = await redeemOnAlgorand(
+          client,
+          TOKEN_BRIDGE_ID,
+          CORE_ID,
+          firstHalfVaa,
+          algoWallet.addr
+        );
+        await signSendAndConfirmAlgorand(
+          client,
+          firstHalfRedeemTxs,
+          algoWallet
+        );
+        expect(
+          await getIsTransferCompletedAlgorand(
+            client,
+            TOKEN_BRIDGE_ID,
+            firstHalfVaa
+          )
+        ).toBe(true);
+        // transfer second half of tokens via relayer
+        const secondHalfReceipt = await transferFromEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          signer,
+          TEST_ERC20,
+          halfAmount,
+          CHAIN_ID_ALGORAND,
+          decodeAddress(algoWallet.addr).publicKey // This needs to be Algorand wallet
+        );
+        // get the sequence from the logs (needed to fetch the vaa)
+        const secondHalfSn = parseSequenceFromLogEth(
+          secondHalfReceipt,
+          CONTRACTS.DEVNET.ethereum.core
+        );
+        // poll until the guardian(s) witness and sign the vaa
+        const { vaaBytes: secondHalfVaa } = await getSignedVAAWithRetry(
+          WORMHOLE_RPC_HOSTS,
+          CHAIN_ID_ETH,
+          ethEmitterAddress,
+          secondHalfSn,
+          {
+            transport: NodeHttpTransport(),
+          }
+        );
+
+        // Redeem second half the amount on Algorand
+        const redeemTxs = await redeemOnAlgorand(
+          client,
+          TOKEN_BRIDGE_ID,
+          CORE_ID,
+          secondHalfVaa,
+          relayerWallet.addr
+        );
+        await signSendAndConfirmAlgorand(client, redeemTxs, relayerWallet);
+        expect(
+          await getIsTransferCompletedAlgorand(
+            client,
+            TOKEN_BRIDGE_ID,
+            secondHalfVaa
+          )
+        ).toBe(true);
+        provider.destroy();
+      } catch (e) {
+        console.error("new test error:", e);
+        done("new test error");
+        return;
+      }
+      done();
+    })();
+  });
+
+  test("testing algorand payload3", (done) => {
+    (async () => {
+      try {
+        const tbAddr: string = getApplicationAddress(TOKEN_BRIDGE_ID);
+        const decTbAddr: Uint8Array = decodeAddress(tbAddr).publicKey;
+        const aa: string = uint8ArrayToHex(decTbAddr);
+
+        const client: algosdk.Algodv2 = getAlgoClient();
+        const tempAccts: Account[] = await getTempAccounts();
+        const numAccts: number = tempAccts.length;
+        expect(numAccts).toBeGreaterThan(0);
+        const algoWallet: Account = tempAccts[0];
+
+        const Fee: number = 0;
+        var testapp: number = 8;
+        var dest = utils
+          .hexZeroPad(BigNumber.from(testapp).toHexString(), 32)
+          .substring(2);
+
+        const transferTxs = await transferFromAlgorand(
+          client,
+          TOKEN_BRIDGE_ID,
+          CORE_ID,
+          algoWallet.addr,
+          BigInt(0),
+          BigInt(100),
+          dest,
+          CHAIN_ID_ALGORAND,
+          BigInt(Fee),
+          hexToUint8Array("ff")
+        );
+
+        const transferResult = await signSendAndConfirmAlgorand(
+          client,
+          transferTxs,
+          algoWallet
+        );
+        const txSid = parseSequenceFromLogAlgorand(transferResult);
+        const signedVaa = await getSignedVAAWithRetry(
+          WORMHOLE_RPC_HOSTS,
+          CHAIN_ID_ALGORAND,
+          aa,
+          txSid,
+          { transport: NodeHttpTransport() }
+        );
+
+        const txns = await redeemOnAlgorand(
+          client,
+          TOKEN_BRIDGE_ID,
+          CORE_ID,
+          signedVaa.vaaBytes,
+          algoWallet.addr
+        );
+
+        const wbefore = await getBalance(
+          client,
+          getApplicationAddress(testapp),
+          BigInt(0)
+        );
+
+        await signSendAndConfirmAlgorand(client, txns, algoWallet);
+        expect(
+          await getIsTransferCompletedAlgorand(
+            client,
+            TOKEN_BRIDGE_ID,
+            signedVaa.vaaBytes
+          )
+        ).toBe(true);
+        const wafter = await getBalance(
+          client,
+          getApplicationAddress(testapp),
+          BigInt(0)
+        );
+
+        expect(BigInt(wafter - wbefore) === BigInt(100));
+      } catch (e) {
+        console.error("new test error:", e);
+        done("new test error");
+        return;
+      }
+      done();
+    })();
+  });
+});

+ 16 - 15
sdk/js/src/token_bridge/__tests__/consts.ts

@@ -6,13 +6,24 @@ const ci = !!process.env.CI;
 // see devnet.md
 export const ETH_NODE_URL = ci ? "ws://eth-devnet:8545" : "ws://localhost:8545";
 export const ETH_PRIVATE_KEY =
-  "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d";
+  "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"; // account 0
+// account 1 used by NFT tests
 export const ETH_PRIVATE_KEY2 =
   "0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c"; // account 2
-export const ETH_CORE_BRIDGE_ADDRESS =
-  "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550";
-export const ETH_TOKEN_BRIDGE_ADDRESS =
-  "0x0290FB167208Af455bB137780163b7B7a9a10C16";
+export const ETH_PRIVATE_KEY3 =
+  "0x646f1ce2fdad0e6deeeb5c7e8e5543bdde65e86029e2fd9fc169899c440a7913"; // account 3
+export const ETH_PRIVATE_KEY4 =
+  "0xadd53f9a7e588d003326d1cbf9e4a43c061aadd9bc938c843a79e7b4fd2ad743"; // account 4
+export const ETH_PRIVATE_KEY5 =
+  "0x395df67f0c2d2d9fe1ad08d1bc8b6627011959b79c53d7dd6a3536a33ab8a4fd"; // account 5
+export const ETH_PRIVATE_KEY6 =
+  "0xe485d098507f54e7733a205420dfddbe58db035fa577fc294ebd14db90767a52"; // account 6
+export const ETH_PRIVATE_KEY7 =
+  "0xa453611d9419d0e56f499079478fd72c37b251a94bfde4d19872c44cf65386e3"; // account 7
+export const ETH_PRIVATE_KEY8 =
+  "0x829e924fdf021ba3dbbc4225edfece9aca04b929d6e75613329ca6f1d31c0bb4"; // account 8
+export const ETH_PRIVATE_KEY9 =
+  "0xb0057716d5917badaf911b193b12b910811c1497b5bada8d7711f758981c3773"; // account 9
 export const SOLANA_HOST = ci
   ? "http://solana-devnet:8899"
   : "http://localhost:8899";
@@ -22,10 +33,6 @@ export const SOLANA_PRIVATE_KEY = new Uint8Array([
   8, 174, 214, 157, 175, 126, 98, 90, 54, 24, 100, 177, 247, 77, 19, 112, 47,
   44, 165, 109, 233, 102, 14, 86, 109, 29, 134, 145, 132, 141,
 ]);
-export const SOLANA_CORE_BRIDGE_ADDRESS =
-  "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o";
-export const SOLANA_TOKEN_BRIDGE_ADDRESS =
-  "B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE";
 export const TERRA_NODE_URL = ci
   ? "http://terra-terrad:1317"
   : "http://localhost:1317";
@@ -36,12 +43,6 @@ export const TERRA_GAS_PRICES_URL = ci
 export const TERRA2_GAS_PRICES_URL = ci
   ? "http://terra2-fcd:3060/v1/txs/gas_prices"
   : "http://localhost:3061/v1/txs/gas_prices";
-export const TERRA_CORE_BRIDGE_ADDRESS =
-  "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5";
-export const TERRA_TOKEN_BRIDGE_ADDRESS =
-  "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4";
-export const TERRA2_TOKEN_BRIDGE_ADDRESS =
-  "terra1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrquka9l6";
 export const TERRA_PRIVATE_KEY =
   "notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius";
 export const TERRA2_PRIVATE_KEY =

+ 524 - 0
sdk/js/src/token_bridge/__tests__/eth-integration.ts

@@ -0,0 +1,524 @@
+import { formatUnits, parseUnits } from "@ethersproject/units";
+import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
+import { describe, expect, jest, test } from "@jest/globals";
+import {
+  ASSOCIATED_TOKEN_PROGRAM_ID,
+  Token,
+  TOKEN_PROGRAM_ID,
+} from "@solana/spl-token";
+import {
+  Connection,
+  Keypair,
+  PublicKey,
+  TokenAccountsFilter,
+  Transaction,
+} from "@solana/web3.js";
+import { ethers } from "ethers";
+import {
+  approveEth,
+  attestFromEth,
+  CHAIN_ID_ETH,
+  CHAIN_ID_SOLANA,
+  CONTRACTS,
+  createWrappedOnSolana,
+  getEmitterAddressEth,
+  getForeignAssetSolana,
+  getIsTransferCompletedSolana,
+  hexToUint8Array,
+  nativeToHexString,
+  parseSequenceFromLogEth,
+  postVaaSolana,
+  redeemOnSolana,
+  TokenImplementation__factory,
+  transferFromEth,
+  tryNativeToUint8Array,
+} from "../..";
+import getSignedVAAWithRetry from "../../rpc/getSignedVAAWithRetry";
+import { postVaaWithRetry } from "../../solana/postVaa";
+import { setDefaultWasm } from "../../solana/wasm";
+import {
+  ETH_NODE_URL,
+  ETH_PRIVATE_KEY,
+  SOLANA_HOST,
+  SOLANA_PRIVATE_KEY,
+  TEST_ERC20,
+  WORMHOLE_RPC_HOSTS,
+} from "./consts";
+
+setDefaultWasm("node");
+
+jest.setTimeout(60000);
+
+async function transferFromEthToSolana(): Promise<string> {
+  // create a keypair for Solana
+  const connection = new Connection(SOLANA_HOST, "confirmed");
+  const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
+  // determine destination address - an associated token account
+  const solanaMintKey = new PublicKey(
+    (await getForeignAssetSolana(
+      connection,
+      CONTRACTS.DEVNET.solana.token_bridge,
+      CHAIN_ID_ETH,
+      hexToUint8Array(nativeToHexString(TEST_ERC20, CHAIN_ID_ETH) || "")
+    )) || ""
+  );
+  const recipient = await Token.getAssociatedTokenAddress(
+    ASSOCIATED_TOKEN_PROGRAM_ID,
+    TOKEN_PROGRAM_ID,
+    solanaMintKey,
+    keypair.publicKey
+  );
+  // create the associated token account if it doesn't exist
+  const associatedAddressInfo = await connection.getAccountInfo(recipient);
+  if (!associatedAddressInfo) {
+    const transaction = new Transaction().add(
+      await Token.createAssociatedTokenAccountInstruction(
+        ASSOCIATED_TOKEN_PROGRAM_ID,
+        TOKEN_PROGRAM_ID,
+        solanaMintKey,
+        recipient,
+        keypair.publicKey, // owner
+        keypair.publicKey // payer
+      )
+    );
+    const { blockhash } = await connection.getRecentBlockhash();
+    transaction.recentBlockhash = blockhash;
+    transaction.feePayer = keypair.publicKey;
+    // sign, send, and confirm transaction
+    transaction.partialSign(keypair);
+    const txid = await connection.sendRawTransaction(transaction.serialize());
+    await connection.confirmTransaction(txid);
+  }
+  // create a signer for Eth
+  const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
+  const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
+  const amount = parseUnits("1", 18);
+  // approve the bridge to spend tokens
+  await approveEth(
+    CONTRACTS.DEVNET.ethereum.token_bridge,
+    TEST_ERC20,
+    signer,
+    amount
+  );
+  // transfer tokens
+  const receipt = await transferFromEth(
+    CONTRACTS.DEVNET.ethereum.token_bridge,
+    signer,
+    TEST_ERC20,
+    amount,
+    CHAIN_ID_SOLANA,
+    hexToUint8Array(
+      nativeToHexString(recipient.toString(), CHAIN_ID_SOLANA) || ""
+    )
+  );
+  // get the sequence from the logs (needed to fetch the vaa)
+  const sequence = await parseSequenceFromLogEth(
+    receipt,
+    CONTRACTS.DEVNET.ethereum.core
+  );
+  provider.destroy();
+  return sequence;
+}
+
+describe("Ethereum to Solana and Back", () => {
+  test("Attest Ethereum ERC-20 to Solana", (done) => {
+    (async () => {
+      try {
+        // create a signer for Eth
+        const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
+        const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
+        // attest the test token
+        const receipt = await attestFromEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          signer,
+          TEST_ERC20
+        );
+        // get the sequence from the logs (needed to fetch the vaa)
+        const sequence = parseSequenceFromLogEth(
+          receipt,
+          CONTRACTS.DEVNET.ethereum.core
+        );
+        const emitterAddress = getEmitterAddressEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge
+        );
+        // poll until the guardian(s) witness and sign the vaa
+        const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
+          WORMHOLE_RPC_HOSTS,
+          CHAIN_ID_ETH,
+          emitterAddress,
+          sequence,
+          {
+            transport: NodeHttpTransport(),
+          }
+        );
+        // create a keypair for Solana
+        const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
+        const payerAddress = keypair.publicKey.toString();
+        // post vaa to Solana
+        const connection = new Connection(SOLANA_HOST, "confirmed");
+        await postVaaSolana(
+          connection,
+          async (transaction) => {
+            transaction.partialSign(keypair);
+            return transaction;
+          },
+          CONTRACTS.DEVNET.solana.core,
+          payerAddress,
+          Buffer.from(signedVAA)
+        );
+        // create wormhole wrapped token (mint and metadata) on solana
+        const transaction = await createWrappedOnSolana(
+          connection,
+          CONTRACTS.DEVNET.solana.core,
+          CONTRACTS.DEVNET.solana.token_bridge,
+          payerAddress,
+          signedVAA
+        );
+        // sign, send, and confirm transaction
+        try {
+          transaction.partialSign(keypair);
+          const txid = await connection.sendRawTransaction(
+            transaction.serialize()
+          );
+          await connection.confirmTransaction(txid);
+        } catch (e) {
+          // this could fail because the token is already attested (in an unclean env)
+        }
+        provider.destroy();
+        done();
+      } catch (e) {
+        console.error(e);
+        done(
+          "An error occurred while trying to attest from Ethereum to Solana"
+        );
+      }
+    })();
+  });
+  test("Ethereum ERC-20 is attested on Solana", async () => {
+    const connection = new Connection(SOLANA_HOST, "confirmed");
+    const address = getForeignAssetSolana(
+      connection,
+      CONTRACTS.DEVNET.solana.token_bridge,
+      "ethereum",
+      tryNativeToUint8Array(TEST_ERC20, "ethereum")
+    );
+    expect(address).toBeTruthy();
+  });
+  test("Send Ethereum ERC-20 to Solana", (done) => {
+    (async () => {
+      try {
+        const DECIMALS: number = 18;
+        // create a keypair for Solana
+        const connection = new Connection(SOLANA_HOST, "confirmed");
+        const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
+        const payerAddress = keypair.publicKey.toString();
+        // determine destination address - an associated token account
+        const SolanaForeignAsset = await getForeignAssetSolana(
+          connection,
+          CONTRACTS.DEVNET.solana.token_bridge,
+          CHAIN_ID_ETH,
+          tryNativeToUint8Array(TEST_ERC20, CHAIN_ID_ETH)
+        );
+        const solanaMintKey = new PublicKey(SolanaForeignAsset || "");
+        const recipient = await Token.getAssociatedTokenAddress(
+          ASSOCIATED_TOKEN_PROGRAM_ID,
+          TOKEN_PROGRAM_ID,
+          solanaMintKey,
+          keypair.publicKey
+        );
+        // create the associated token account if it doesn't exist
+        const associatedAddressInfo = await connection.getAccountInfo(
+          recipient
+        );
+        if (!associatedAddressInfo) {
+          const transaction = new Transaction().add(
+            await Token.createAssociatedTokenAccountInstruction(
+              ASSOCIATED_TOKEN_PROGRAM_ID,
+              TOKEN_PROGRAM_ID,
+              solanaMintKey,
+              recipient,
+              keypair.publicKey, // owner
+              keypair.publicKey // payer
+            )
+          );
+          const { blockhash } = await connection.getRecentBlockhash();
+          transaction.recentBlockhash = blockhash;
+          transaction.feePayer = keypair.publicKey;
+          // sign, send, and confirm transaction
+          transaction.partialSign(keypair);
+          const txid = await connection.sendRawTransaction(
+            transaction.serialize()
+          );
+          await connection.confirmTransaction(txid);
+        }
+        // create a signer for Eth
+        const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
+        const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
+        const amount = parseUnits("1", DECIMALS);
+
+        // Get the initial wallet balance of ERC20 on Eth
+        let token = TokenImplementation__factory.connect(TEST_ERC20, signer);
+        const initialErc20BalOnEth = await token.balanceOf(
+          await signer.getAddress()
+        );
+        const initialErc20BalOnEthFormatted = formatUnits(
+          initialErc20BalOnEth._hex,
+          DECIMALS
+        );
+
+        // Get the initial balance on Solana
+        const tokenFilter: TokenAccountsFilter = {
+          programId: TOKEN_PROGRAM_ID,
+        };
+        let results = await connection.getParsedTokenAccountsByOwner(
+          keypair.publicKey,
+          tokenFilter
+        );
+        let initialSolanaBalance: number = 0;
+        for (const item of results.value) {
+          const tokenInfo = item.account.data.parsed.info;
+          const address = tokenInfo.mint;
+          const amount = tokenInfo.tokenAmount.uiAmount;
+          if (tokenInfo.mint === SolanaForeignAsset) {
+            initialSolanaBalance = amount;
+          }
+        }
+
+        // approve the bridge to spend tokens
+        await approveEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          TEST_ERC20,
+          signer,
+          amount
+        );
+        // transfer tokens
+        const receipt = await transferFromEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          signer,
+          TEST_ERC20,
+          amount,
+          CHAIN_ID_SOLANA,
+          tryNativeToUint8Array(recipient.toString(), CHAIN_ID_SOLANA)
+        );
+        // get the sequence from the logs (needed to fetch the vaa)
+        const sequence = parseSequenceFromLogEth(
+          receipt,
+          CONTRACTS.DEVNET.ethereum.core
+        );
+        const emitterAddress = getEmitterAddressEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge
+        );
+        // poll until the guardian(s) witness and sign the vaa
+        const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
+          WORMHOLE_RPC_HOSTS,
+          CHAIN_ID_ETH,
+          emitterAddress,
+          sequence,
+          {
+            transport: NodeHttpTransport(),
+          }
+        );
+        // post vaa to Solana
+        await postVaaSolana(
+          connection,
+          async (transaction) => {
+            transaction.partialSign(keypair);
+            return transaction;
+          },
+          CONTRACTS.DEVNET.solana.core,
+          payerAddress,
+          Buffer.from(signedVAA)
+        );
+        expect(
+          await getIsTransferCompletedSolana(
+            CONTRACTS.DEVNET.solana.token_bridge,
+            signedVAA,
+            connection
+          )
+        ).toBe(false);
+        // redeem tokens on solana
+        const transaction = await redeemOnSolana(
+          connection,
+          CONTRACTS.DEVNET.solana.core,
+          CONTRACTS.DEVNET.solana.token_bridge,
+          payerAddress,
+          signedVAA
+        );
+        // sign, send, and confirm transaction
+        transaction.partialSign(keypair);
+        const txid = await connection.sendRawTransaction(
+          transaction.serialize()
+        );
+        await connection.confirmTransaction(txid);
+        expect(
+          await getIsTransferCompletedSolana(
+            CONTRACTS.DEVNET.solana.token_bridge,
+            signedVAA,
+            connection
+          )
+        ).toBe(true);
+
+        // Get the final wallet balance of ERC20 on Eth
+        const finalErc20BalOnEth = await token.balanceOf(
+          await signer.getAddress()
+        );
+        const finalErc20BalOnEthFormatted = formatUnits(
+          finalErc20BalOnEth._hex,
+          DECIMALS
+        );
+        expect(
+          parseInt(initialErc20BalOnEthFormatted) -
+            parseInt(finalErc20BalOnEthFormatted) ===
+            1
+        ).toBe(true);
+
+        // Get final balance on Solana
+        results = await connection.getParsedTokenAccountsByOwner(
+          keypair.publicKey,
+          tokenFilter
+        );
+        let finalSolanaBalance: number = 0;
+        for (const item of results.value) {
+          const tokenInfo = item.account.data.parsed.info;
+          const address = tokenInfo.mint;
+          const amount = tokenInfo.tokenAmount.uiAmount;
+          if (tokenInfo.mint === SolanaForeignAsset) {
+            finalSolanaBalance = amount;
+          }
+        }
+        expect(finalSolanaBalance - initialSolanaBalance === 1).toBe(true);
+        provider.destroy();
+        done();
+      } catch (e) {
+        console.error(e);
+        done("An error occurred while trying to send from Ethereum to Solana");
+      }
+    })();
+  });
+  describe("Post VAA with retry", () => {
+    test("postVAA with retry, no failures", (done) => {
+      (async () => {
+        try {
+          // create a keypair for Solana
+          const connection = new Connection(SOLANA_HOST, "confirmed");
+          const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
+          const payerAddress = keypair.publicKey.toString();
+          const sequence = await transferFromEthToSolana();
+          const emitterAddress = getEmitterAddressEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge
+          );
+          // poll until the guardian(s) witness and sign the vaa
+          const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
+            WORMHOLE_RPC_HOSTS,
+            CHAIN_ID_ETH,
+            emitterAddress,
+            sequence,
+            {
+              transport: NodeHttpTransport(),
+            }
+          );
+          let maxFailures = 0;
+          // post vaa to Solana
+
+          const postPromise = postVaaWithRetry(
+            connection,
+            async (transaction) => {
+              await new Promise(function (resolve) {
+                //We delay here so the connection has time to get wrecked
+                setTimeout(function () {
+                  resolve(500);
+                });
+              });
+              transaction.partialSign(keypair);
+              return transaction;
+            },
+            CONTRACTS.DEVNET.solana.core,
+            payerAddress,
+            Buffer.from(signedVAA),
+            maxFailures
+          );
+
+          await postPromise;
+          // redeem tokens on solana
+          const transaction = await redeemOnSolana(
+            connection,
+            CONTRACTS.DEVNET.solana.core,
+            CONTRACTS.DEVNET.solana.token_bridge,
+            payerAddress,
+            signedVAA
+          );
+          // sign, send, and confirm transaction
+          transaction.partialSign(keypair);
+          const txid = await connection.sendRawTransaction(
+            transaction.serialize()
+          );
+          await connection.confirmTransaction(txid);
+          expect(
+            await getIsTransferCompletedSolana(
+              CONTRACTS.DEVNET.solana.token_bridge,
+              signedVAA,
+              connection
+            )
+          ).toBe(true);
+          done();
+        } catch (e) {
+          console.error(e);
+          done(
+            "An error occurred while happy-path testing post VAA with retry."
+          );
+        }
+      })();
+    });
+    test("Reject on signature failure", (done) => {
+      (async () => {
+        try {
+          // create a keypair for Solana
+          const connection = new Connection(SOLANA_HOST, "confirmed");
+          const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
+          const payerAddress = keypair.publicKey.toString();
+          const sequence = await transferFromEthToSolana();
+          const emitterAddress = getEmitterAddressEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge
+          );
+          // poll until the guardian(s) witness and sign the vaa
+          const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
+            WORMHOLE_RPC_HOSTS,
+            CHAIN_ID_ETH,
+            emitterAddress,
+            sequence,
+            {
+              transport: NodeHttpTransport(),
+            }
+          );
+          let maxFailures = 5;
+          // post vaa to Solana
+
+          let error = false;
+          try {
+            const postPromise = postVaaWithRetry(
+              connection,
+              async (transaction) => {
+                return Promise.reject();
+              },
+              CONTRACTS.DEVNET.solana.core,
+              payerAddress,
+              Buffer.from(signedVAA),
+              maxFailures
+            );
+
+            await postPromise;
+          } catch (e) {
+            error = true;
+          }
+          expect(error).toBe(true);
+          done();
+        } catch (e) {
+          console.error(e);
+          done(
+            "An error occurred while trying to send from Ethereum to Solana"
+          );
+        }
+      })();
+    });
+  });
+});

+ 1 - 95
sdk/js/src/token_bridge/__tests__/helpers.ts

@@ -1,109 +1,15 @@
-import { parseUnits } from "@ethersproject/units";
 import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
-import {
-  ASSOCIATED_TOKEN_PROGRAM_ID,
-  Token,
-  TOKEN_PROGRAM_ID,
-} from "@solana/spl-token";
-import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js";
 import { LCDClient, MnemonicKey, TxInfo } from "@terra-money/terra.js";
 import axios from "axios";
-import { ethers } from "ethers";
-import {
-  approveEth,
-  ChainId,
-  CHAIN_ID_ETH,
-  CHAIN_ID_SOLANA,
-  getForeignAssetSolana,
-  getSignedVAAWithRetry,
-  hexToUint8Array,
-  nativeToHexString,
-  parseSequenceFromLogEth,
-  transferFromEth,
-} from "../..";
+import { ChainId, getSignedVAAWithRetry } from "../..";
 import {
-  ETH_CORE_BRIDGE_ADDRESS,
-  ETH_NODE_URL,
-  ETH_PRIVATE_KEY,
-  ETH_TOKEN_BRIDGE_ADDRESS,
-  SOLANA_HOST,
-  SOLANA_PRIVATE_KEY,
-  SOLANA_TOKEN_BRIDGE_ADDRESS,
   TERRA_CHAIN_ID,
   TERRA_GAS_PRICES_URL,
-  TERRA_HOST,
   TERRA_NODE_URL,
   TERRA_PRIVATE_KEY,
-  TEST_ERC20,
   WORMHOLE_RPC_HOSTS,
 } from "./consts";
 
-export async function transferFromEthToSolana(): Promise<string> {
-  // create a keypair for Solana
-  const connection = new Connection(SOLANA_HOST, "confirmed");
-  const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
-  // determine destination address - an associated token account
-  const solanaMintKey = new PublicKey(
-    (await getForeignAssetSolana(
-      connection,
-      SOLANA_TOKEN_BRIDGE_ADDRESS,
-      CHAIN_ID_ETH,
-      hexToUint8Array(nativeToHexString(TEST_ERC20, CHAIN_ID_ETH) || "")
-    )) || ""
-  );
-  const recipient = await Token.getAssociatedTokenAddress(
-    ASSOCIATED_TOKEN_PROGRAM_ID,
-    TOKEN_PROGRAM_ID,
-    solanaMintKey,
-    keypair.publicKey
-  );
-  // create the associated token account if it doesn't exist
-  const associatedAddressInfo = await connection.getAccountInfo(recipient);
-  if (!associatedAddressInfo) {
-    const transaction = new Transaction().add(
-      await Token.createAssociatedTokenAccountInstruction(
-        ASSOCIATED_TOKEN_PROGRAM_ID,
-        TOKEN_PROGRAM_ID,
-        solanaMintKey,
-        recipient,
-        keypair.publicKey, // owner
-        keypair.publicKey // payer
-      )
-    );
-    const { blockhash } = await connection.getRecentBlockhash();
-    transaction.recentBlockhash = blockhash;
-    transaction.feePayer = keypair.publicKey;
-    // sign, send, and confirm transaction
-    transaction.partialSign(keypair);
-    const txid = await connection.sendRawTransaction(transaction.serialize());
-    await connection.confirmTransaction(txid);
-  }
-  // create a signer for Eth
-  const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
-  const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
-  const amount = parseUnits("1", 18);
-  // approve the bridge to spend tokens
-  await approveEth(ETH_TOKEN_BRIDGE_ADDRESS, TEST_ERC20, signer, amount);
-  // transfer tokens
-  const receipt = await transferFromEth(
-    ETH_TOKEN_BRIDGE_ADDRESS,
-    signer,
-    TEST_ERC20,
-    amount,
-    CHAIN_ID_SOLANA,
-    hexToUint8Array(
-      nativeToHexString(recipient.toString(), CHAIN_ID_SOLANA) || ""
-    )
-  );
-  // get the sequence from the logs (needed to fetch the vaa)
-  const sequence = await parseSequenceFromLogEth(
-    receipt,
-    ETH_CORE_BRIDGE_ADDRESS
-  );
-  provider.destroy();
-  return sequence;
-}
-
 export async function waitForTerraExecution(
   transaction: string,
   lcd: LCDClient

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

@@ -1,2901 +0,0 @@
-import { formatUnits, parseUnits } from "@ethersproject/units";
-import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
-import { describe, expect, jest, test } from "@jest/globals";
-import {
-  ASSOCIATED_TOKEN_PROGRAM_ID,
-  Token,
-  TOKEN_PROGRAM_ID,
-} from "@solana/spl-token";
-import {
-  Connection,
-  Keypair,
-  PublicKey,
-  TokenAccountsFilter,
-  Transaction,
-} from "@solana/web3.js";
-import {
-  LCDClient,
-  MnemonicKey,
-  MsgExecuteContract,
-} from "@terra-money/terra.js";
-import algosdk, {
-  Account,
-  decodeAddress,
-  getApplicationAddress,
-  makeApplicationCallTxnFromObject,
-  OnApplicationComplete,
-  waitForConfirmation,
-} from "algosdk";
-import axios from "axios";
-import { BigNumber, ethers, utils } from "ethers";
-import {
-  approveEth,
-  attestFromAlgorand,
-  attestFromEth,
-  attestFromSolana,
-  attestFromTerra,
-  CHAIN_ID_ALGORAND,
-  CHAIN_ID_ETH,
-  CHAIN_ID_SOLANA,
-  CHAIN_ID_TERRA,
-  createWrappedOnAlgorand,
-  createWrappedOnEth,
-  createWrappedOnSolana,
-  createWrappedOnTerra,
-  getEmitterAddressAlgorand,
-  getEmitterAddressEth,
-  getEmitterAddressSolana,
-  getEmitterAddressTerra,
-  getForeignAssetEth,
-  getForeignAssetSolana,
-  getForeignAssetTerra,
-  getIsTransferCompletedAlgorand,
-  getIsTransferCompletedEth,
-  getIsTransferCompletedSolana,
-  getIsTransferCompletedTerra,
-  getOriginalAssetAlgorand,
-  hexToUint8Array,
-  nativeToHexString,
-  parseSequenceFromLogAlgorand,
-  parseSequenceFromLogEth,
-  parseSequenceFromLogSolana,
-  parseSequenceFromLogTerra,
-  postVaaSolana,
-  redeemOnAlgorand,
-  redeemOnEth,
-  redeemOnSolana,
-  redeemOnTerra,
-  textToUint8Array,
-  TokenImplementation__factory,
-  transferFromAlgorand,
-  transferFromEth,
-  transferFromSolana,
-  transferFromTerra,
-  tryNativeToHexString,
-  tryNativeToUint8Array,
-  uint8ArrayToHex,
-  updateWrappedOnEth,
-  WormholeWrappedInfo,
-} from "../..";
-import { _parseVAAAlgorand } from "../../algorand";
-import {
-  createAsset,
-  getAlgoClient,
-  getBalance,
-  getBalances,
-  getForeignAssetFromVaaAlgorand,
-  getTempAccounts,
-  signSendAndConfirmAlgorand,
-} from "../../algorand/__tests__/testHelpers";
-import getSignedVAAWithRetry from "../../rpc/getSignedVAAWithRetry";
-import { postVaaWithRetry } from "../../solana/postVaa";
-import { setDefaultWasm } from "../../solana/wasm";
-import { safeBigIntToNumber } from "../../utils/bigint";
-import {
-  ETH_CORE_BRIDGE_ADDRESS,
-  ETH_NODE_URL,
-  ETH_PRIVATE_KEY,
-  ETH_TOKEN_BRIDGE_ADDRESS,
-  SOLANA_CORE_BRIDGE_ADDRESS,
-  SOLANA_HOST,
-  SOLANA_PRIVATE_KEY,
-  SOLANA_TOKEN_BRIDGE_ADDRESS,
-  TERRA_CHAIN_ID,
-  TERRA_GAS_PRICES_URL,
-  TERRA_NODE_URL,
-  TERRA_PRIVATE_KEY,
-  TERRA_TOKEN_BRIDGE_ADDRESS,
-  TEST_ERC20,
-  TEST_SOLANA_TOKEN,
-  WORMHOLE_RPC_HOSTS,
-} from "./consts";
-import {
-  getSignedVAABySequence,
-  getTerraGasPrices,
-  queryBalanceOnTerra,
-  transferFromEthToSolana,
-  waitForTerraExecution,
-} from "./helpers";
-
-const CORE_ID = BigInt(4);
-const TOKEN_BRIDGE_ID = BigInt(6);
-
-setDefaultWasm("node");
-
-jest.setTimeout(60000);
-
-// TODO: setup keypair and provider/signer before, destroy provider after
-// TODO: make the repeatable (can't attest an already attested token)
-
-describe("Integration Tests", () => {
-  describe("Ethereum to Solana", () => {
-    test("Attest Ethereum ERC-20 to Solana", (done) => {
-      (async () => {
-        try {
-          // create a signer for Eth
-          const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
-          const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
-          // attest the test token
-          const receipt = await attestFromEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            signer,
-            TEST_ERC20
-          );
-          // get the sequence from the logs (needed to fetch the vaa)
-          const sequence = parseSequenceFromLogEth(
-            receipt,
-            ETH_CORE_BRIDGE_ADDRESS
-          );
-          const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
-          // poll until the guardian(s) witness and sign the vaa
-          const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_ETH,
-            emitterAddress,
-            sequence,
-            {
-              transport: NodeHttpTransport(),
-            }
-          );
-          // create a keypair for Solana
-          const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
-          const payerAddress = keypair.publicKey.toString();
-          // post vaa to Solana
-          const connection = new Connection(SOLANA_HOST, "confirmed");
-          await postVaaSolana(
-            connection,
-            async (transaction) => {
-              transaction.partialSign(keypair);
-              return transaction;
-            },
-            SOLANA_CORE_BRIDGE_ADDRESS,
-            payerAddress,
-            Buffer.from(signedVAA)
-          );
-          // create wormhole wrapped token (mint and metadata) on solana
-          const transaction = await createWrappedOnSolana(
-            connection,
-            SOLANA_CORE_BRIDGE_ADDRESS,
-            SOLANA_TOKEN_BRIDGE_ADDRESS,
-            payerAddress,
-            signedVAA
-          );
-          // sign, send, and confirm transaction
-          try {
-            transaction.partialSign(keypair);
-            const txid = await connection.sendRawTransaction(
-              transaction.serialize()
-            );
-            await connection.confirmTransaction(txid);
-          } catch (e) {
-            // this could fail because the token is already attested (in an unclean env)
-          }
-          provider.destroy();
-          done();
-        } catch (e) {
-          console.error(e);
-          done(
-            "An error occurred while trying to attest from Ethereum to Solana"
-          );
-        }
-      })();
-    });
-    // TODO: it is attested
-    test("Send Ethereum ERC-20 to Solana", (done) => {
-      (async () => {
-        try {
-          const DECIMALS: number = 18;
-          // create a keypair for Solana
-          const connection = new Connection(SOLANA_HOST, "confirmed");
-          const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
-          const payerAddress = keypair.publicKey.toString();
-          // determine destination address - an associated token account
-          const SolanaForeignAsset = await getForeignAssetSolana(
-            connection,
-            SOLANA_TOKEN_BRIDGE_ADDRESS,
-            CHAIN_ID_ETH,
-            tryNativeToUint8Array(TEST_ERC20, CHAIN_ID_ETH)
-          );
-          const solanaMintKey = new PublicKey(SolanaForeignAsset || "");
-          const recipient = await Token.getAssociatedTokenAddress(
-            ASSOCIATED_TOKEN_PROGRAM_ID,
-            TOKEN_PROGRAM_ID,
-            solanaMintKey,
-            keypair.publicKey
-          );
-          // create the associated token account if it doesn't exist
-          const associatedAddressInfo = await connection.getAccountInfo(
-            recipient
-          );
-          if (!associatedAddressInfo) {
-            const transaction = new Transaction().add(
-              await Token.createAssociatedTokenAccountInstruction(
-                ASSOCIATED_TOKEN_PROGRAM_ID,
-                TOKEN_PROGRAM_ID,
-                solanaMintKey,
-                recipient,
-                keypair.publicKey, // owner
-                keypair.publicKey // payer
-              )
-            );
-            const { blockhash } = await connection.getRecentBlockhash();
-            transaction.recentBlockhash = blockhash;
-            transaction.feePayer = keypair.publicKey;
-            // sign, send, and confirm transaction
-            transaction.partialSign(keypair);
-            const txid = await connection.sendRawTransaction(
-              transaction.serialize()
-            );
-            await connection.confirmTransaction(txid);
-          }
-          // create a signer for Eth
-          const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
-          const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
-          const amount = parseUnits("1", DECIMALS);
-
-          // Get the initial wallet balance of ERC20 on Eth
-          let token = TokenImplementation__factory.connect(TEST_ERC20, signer);
-          const ETH_TEST_WALLET_PUBLIC_KEY =
-            "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1";
-          const initialErc20BalOnEth = await token.balanceOf(
-            ETH_TEST_WALLET_PUBLIC_KEY
-          );
-          const initialErc20BalOnEthFormatted = formatUnits(
-            initialErc20BalOnEth._hex,
-            DECIMALS
-          );
-
-          // Get the initial balance on Solana
-          const tokenFilter: TokenAccountsFilter = {
-            programId: TOKEN_PROGRAM_ID,
-          };
-          let results = await connection.getParsedTokenAccountsByOwner(
-            keypair.publicKey,
-            tokenFilter
-          );
-          let initialSolanaBalance: number = 0;
-          for (const item of results.value) {
-            const tokenInfo = item.account.data.parsed.info;
-            const address = tokenInfo.mint;
-            const amount = tokenInfo.tokenAmount.uiAmount;
-            if (tokenInfo.mint === SolanaForeignAsset) {
-              initialSolanaBalance = amount;
-            }
-          }
-
-          // approve the bridge to spend tokens
-          await approveEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            TEST_ERC20,
-            signer,
-            amount
-          );
-          // transfer tokens
-          const receipt = await transferFromEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            signer,
-            TEST_ERC20,
-            amount,
-            CHAIN_ID_SOLANA,
-            tryNativeToUint8Array(recipient.toString(), CHAIN_ID_SOLANA)
-          );
-          // get the sequence from the logs (needed to fetch the vaa)
-          const sequence = parseSequenceFromLogEth(
-            receipt,
-            ETH_CORE_BRIDGE_ADDRESS
-          );
-          const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
-          // poll until the guardian(s) witness and sign the vaa
-          const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_ETH,
-            emitterAddress,
-            sequence,
-            {
-              transport: NodeHttpTransport(),
-            }
-          );
-          // post vaa to Solana
-          await postVaaSolana(
-            connection,
-            async (transaction) => {
-              transaction.partialSign(keypair);
-              return transaction;
-            },
-            SOLANA_CORE_BRIDGE_ADDRESS,
-            payerAddress,
-            Buffer.from(signedVAA)
-          );
-          expect(
-            await getIsTransferCompletedSolana(
-              SOLANA_TOKEN_BRIDGE_ADDRESS,
-              signedVAA,
-              connection
-            )
-          ).toBe(false);
-          // redeem tokens on solana
-          const transaction = await redeemOnSolana(
-            connection,
-            SOLANA_CORE_BRIDGE_ADDRESS,
-            SOLANA_TOKEN_BRIDGE_ADDRESS,
-            payerAddress,
-            signedVAA
-          );
-          // sign, send, and confirm transaction
-          transaction.partialSign(keypair);
-          const txid = await connection.sendRawTransaction(
-            transaction.serialize()
-          );
-          await connection.confirmTransaction(txid);
-          expect(
-            await getIsTransferCompletedSolana(
-              SOLANA_TOKEN_BRIDGE_ADDRESS,
-              signedVAA,
-              connection
-            )
-          ).toBe(true);
-
-          // Get the final wallet balance of ERC20 on Eth
-          const finalErc20BalOnEth = await token.balanceOf(
-            ETH_TEST_WALLET_PUBLIC_KEY
-          );
-          const finalErc20BalOnEthFormatted = formatUnits(
-            finalErc20BalOnEth._hex,
-            DECIMALS
-          );
-          expect(
-            parseInt(initialErc20BalOnEthFormatted) -
-              parseInt(finalErc20BalOnEthFormatted) ===
-              1
-          ).toBe(true);
-
-          // Get final balance on Solana
-          results = await connection.getParsedTokenAccountsByOwner(
-            keypair.publicKey,
-            tokenFilter
-          );
-          let finalSolanaBalance: number = 0;
-          for (const item of results.value) {
-            const tokenInfo = item.account.data.parsed.info;
-            const address = tokenInfo.mint;
-            const amount = tokenInfo.tokenAmount.uiAmount;
-            if (tokenInfo.mint === SolanaForeignAsset) {
-              finalSolanaBalance = amount;
-            }
-          }
-          expect(finalSolanaBalance - initialSolanaBalance === 1).toBe(true);
-          provider.destroy();
-          done();
-        } catch (e) {
-          console.error(e);
-          done(
-            "An error occurred while trying to send from Ethereum to Solana"
-          );
-        }
-      })();
-    });
-  });
-  describe("Solana to Ethereum", () => {
-    test("Attest Solana SPL to Ethereum", (done) => {
-      (async () => {
-        try {
-          // create a keypair for Solana
-          const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
-          const payerAddress = keypair.publicKey.toString();
-          // attest the test token
-          const connection = new Connection(SOLANA_HOST, "confirmed");
-          const transaction = await attestFromSolana(
-            connection,
-            SOLANA_CORE_BRIDGE_ADDRESS,
-            SOLANA_TOKEN_BRIDGE_ADDRESS,
-            payerAddress,
-            TEST_SOLANA_TOKEN
-          );
-          // sign, send, and confirm transaction
-          transaction.partialSign(keypair);
-          const txid = await connection.sendRawTransaction(
-            transaction.serialize()
-          );
-          await connection.confirmTransaction(txid);
-          const info = await connection.getTransaction(txid);
-          if (!info) {
-            throw new Error(
-              "An error occurred while fetching the transaction info"
-            );
-          }
-          // get the sequence from the logs (needed to fetch the vaa)
-          const sequence = parseSequenceFromLogSolana(info);
-          const emitterAddress = await getEmitterAddressSolana(
-            SOLANA_TOKEN_BRIDGE_ADDRESS
-          );
-          // poll until the guardian(s) witness and sign the vaa
-          const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_SOLANA,
-            emitterAddress,
-            sequence,
-            {
-              transport: NodeHttpTransport(),
-            }
-          );
-          // create a signer for Eth
-          const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
-          const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
-          try {
-            await createWrappedOnEth(
-              ETH_TOKEN_BRIDGE_ADDRESS,
-              signer,
-              signedVAA
-            );
-          } catch (e) {
-            // this could fail because the token is already attested (in an unclean env)
-          }
-          provider.destroy();
-          done();
-        } catch (e) {
-          console.error(e);
-          done(
-            "An error occurred while trying to attest from Solana to Ethereum"
-          );
-        }
-      })();
-    });
-    // TODO: it is attested
-    test("Send Solana SPL to Ethereum", (done) => {
-      (async () => {
-        try {
-          // create a signer for Eth
-          const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
-          const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
-          const targetAddress = await signer.getAddress();
-          // create a keypair for Solana
-          const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
-          const payerAddress = keypair.publicKey.toString();
-          // find the associated token account
-          const fromAddress = (
-            await Token.getAssociatedTokenAddress(
-              ASSOCIATED_TOKEN_PROGRAM_ID,
-              TOKEN_PROGRAM_ID,
-              new PublicKey(TEST_SOLANA_TOKEN),
-              keypair.publicKey
-            )
-          ).toString();
-
-          const connection = new Connection(SOLANA_HOST, "confirmed");
-
-          // Get the initial solana token balance
-          const tokenFilter: TokenAccountsFilter = {
-            programId: TOKEN_PROGRAM_ID,
-          };
-          let results = await connection.getParsedTokenAccountsByOwner(
-            keypair.publicKey,
-            tokenFilter
-          );
-          let initialSolanaBalance: number = 0;
-          for (const item of results.value) {
-            const tokenInfo = item.account.data.parsed.info;
-            const address = tokenInfo.mint;
-            const amount = tokenInfo.tokenAmount.uiAmount;
-            if (tokenInfo.mint === TEST_SOLANA_TOKEN) {
-              initialSolanaBalance = amount;
-            }
-          }
-
-          // Get the initial wallet balance on Eth
-          const ETH_TEST_WALLET_PUBLIC_KEY =
-            "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1";
-          const originAssetHex = tryNativeToHexString(
-            TEST_SOLANA_TOKEN,
-            CHAIN_ID_SOLANA
-          );
-          if (!originAssetHex) {
-            throw new Error("originAssetHex is null");
-          }
-          const foreignAsset = await getForeignAssetEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            provider,
-            CHAIN_ID_SOLANA,
-            hexToUint8Array(originAssetHex)
-          );
-          if (!foreignAsset) {
-            throw new Error("foreignAsset is null");
-          }
-          let token = TokenImplementation__factory.connect(
-            foreignAsset,
-            signer
-          );
-          const initialBalOnEth = await token.balanceOf(
-            ETH_TEST_WALLET_PUBLIC_KEY
-          );
-          const initialBalOnEthFormatted = formatUnits(initialBalOnEth._hex, 9);
-
-          // transfer the test token
-          const amount = parseUnits("1", 9).toBigInt();
-          const transaction = await transferFromSolana(
-            connection,
-            SOLANA_CORE_BRIDGE_ADDRESS,
-            SOLANA_TOKEN_BRIDGE_ADDRESS,
-            payerAddress,
-            fromAddress,
-            TEST_SOLANA_TOKEN,
-            amount,
-            tryNativeToUint8Array(targetAddress, CHAIN_ID_ETH),
-            CHAIN_ID_ETH
-          );
-          // sign, send, and confirm transaction
-          transaction.partialSign(keypair);
-          const txid = await connection.sendRawTransaction(
-            transaction.serialize()
-          );
-          await connection.confirmTransaction(txid);
-          const info = await connection.getTransaction(txid);
-          if (!info) {
-            throw new Error(
-              "An error occurred while fetching the transaction info"
-            );
-          }
-          // get the sequence from the logs (needed to fetch the vaa)
-          const sequence = parseSequenceFromLogSolana(info);
-          const emitterAddress = await getEmitterAddressSolana(
-            SOLANA_TOKEN_BRIDGE_ADDRESS
-          );
-          // poll until the guardian(s) witness and sign the vaa
-          const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_SOLANA,
-            emitterAddress,
-            sequence,
-            {
-              transport: NodeHttpTransport(),
-            }
-          );
-          expect(
-            await getIsTransferCompletedEth(
-              ETH_TOKEN_BRIDGE_ADDRESS,
-              provider,
-              signedVAA
-            )
-          ).toBe(false);
-          await redeemOnEth(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA);
-          expect(
-            await getIsTransferCompletedEth(
-              ETH_TOKEN_BRIDGE_ADDRESS,
-              provider,
-              signedVAA
-            )
-          ).toBe(true);
-
-          // Get final balance on Solana
-          results = await connection.getParsedTokenAccountsByOwner(
-            keypair.publicKey,
-            tokenFilter
-          );
-          let finalSolanaBalance: number = 0;
-          for (const item of results.value) {
-            const tokenInfo = item.account.data.parsed.info;
-            const address = tokenInfo.mint;
-            const amount = tokenInfo.tokenAmount.uiAmount;
-            if (tokenInfo.mint === TEST_SOLANA_TOKEN) {
-              finalSolanaBalance = amount;
-            }
-          }
-          expect(initialSolanaBalance - finalSolanaBalance).toBeCloseTo(1);
-
-          // Get the final balance on Eth
-          const finalBalOnEth = await token.balanceOf(
-            ETH_TEST_WALLET_PUBLIC_KEY
-          );
-          const finalBalOnEthFormatted = formatUnits(finalBalOnEth._hex, 9);
-          expect(
-            parseInt(finalBalOnEthFormatted) -
-              parseInt(initialBalOnEthFormatted) ===
-              1
-          ).toBe(true);
-          provider.destroy();
-          done();
-        } catch (e) {
-          console.error(e);
-          done(
-            "An error occurred while trying to send from Solana to Ethereum"
-          );
-        }
-      })();
-    });
-  });
-  describe("Ethereum to Terra", () => {
-    test("Attest Ethereum ERC-20 to Terra", (done) => {
-      (async () => {
-        try {
-          // create a signer for Eth
-          const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
-          const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
-          // attest the test token
-          const receipt = await attestFromEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            signer,
-            TEST_ERC20
-          );
-          // get the sequence from the logs (needed to fetch the vaa)
-          const sequence = parseSequenceFromLogEth(
-            receipt,
-            ETH_CORE_BRIDGE_ADDRESS
-          );
-          const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
-          // poll until the guardian(s) witness and sign the vaa
-          const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_ETH,
-            emitterAddress,
-            sequence,
-            {
-              transport: NodeHttpTransport(),
-            }
-          );
-          const lcd = new LCDClient({
-            URL: TERRA_NODE_URL,
-            chainID: TERRA_CHAIN_ID,
-            isClassic: true,
-          });
-          const mk = new MnemonicKey({
-            mnemonic: TERRA_PRIVATE_KEY,
-          });
-          const wallet = lcd.wallet(mk);
-          const msg = await createWrappedOnTerra(
-            TERRA_TOKEN_BRIDGE_ADDRESS,
-            wallet.key.accAddress,
-            signedVAA
-          );
-          const gasPrices = await getTerraGasPrices();
-          const feeEstimate = await lcd.tx.estimateFee(
-            [
-              {
-                sequenceNumber: await wallet.sequence(),
-                publicKey: wallet.key.publicKey,
-              },
-            ],
-            {
-              msgs: [msg],
-              feeDenoms: ["uluna"],
-              gasPrices,
-            }
-          );
-          const tx = await wallet.createAndSignTx({
-            msgs: [msg],
-            memo: "test",
-            feeDenoms: ["uluna"],
-            gasPrices,
-            fee: feeEstimate,
-          });
-          try {
-            await lcd.tx.broadcast(tx);
-          } catch (e) {
-            // this could fail because the token is already attested (in an unclean env)
-          }
-          provider.destroy();
-          done();
-        } catch (e) {
-          console.error(e);
-          done(
-            "An error occurred while trying to attest from Ethereum to Terra"
-          );
-        }
-      })();
-    });
-    // TODO: it is attested
-    test("Send Ethereum ERC-20 to Terra", (done) => {
-      (async () => {
-        try {
-          // create a signer for Eth
-          const provider = new ethers.providers.WebSocketProvider(
-            ETH_NODE_URL
-          ) as any;
-          const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
-          const amount = parseUnits("1", 18);
-          const ERC20 = "0x2D8BE6BF0baA74e0A907016679CaE9190e80dD0A";
-          const TerraWalletAddress: string =
-            "terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v";
-          interface Erc20Balance {
-            balance: string;
-          }
-          const lcd = new LCDClient({
-            URL: TERRA_NODE_URL,
-            chainID: TERRA_CHAIN_ID,
-            isClassic: true,
-          });
-
-          // Get initial wallet balances
-          let token = TokenImplementation__factory.connect(ERC20, signer);
-          const ETH_TEST_WALLET_PUBLIC_KEY =
-            "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1";
-          const initialBalOnEth = await token.balanceOf(
-            ETH_TEST_WALLET_PUBLIC_KEY
-          );
-          let initialBalOnEthStr = ethers.utils.formatUnits(
-            initialBalOnEth,
-            18
-          );
-
-          // Get initial balance of ERC20 on Terra
-          const originAssetHex = tryNativeToHexString(ERC20, CHAIN_ID_ETH);
-          if (!originAssetHex) {
-            throw new Error("originAssetHex is null");
-          }
-          const foreignAsset = await getForeignAssetTerra(
-            TERRA_TOKEN_BRIDGE_ADDRESS,
-            lcd,
-            CHAIN_ID_ETH,
-            hexToUint8Array(originAssetHex)
-          );
-          if (!foreignAsset) {
-            throw new Error("foreignAsset is null");
-          }
-          const tokenDefinition: any = await lcd.wasm.contractQuery(
-            foreignAsset,
-            {
-              token_info: {},
-            }
-          );
-          let cw20BalOnTerra: Erc20Balance = await lcd.wasm.contractQuery(
-            foreignAsset,
-            {
-              balance: {
-                address: TerraWalletAddress,
-              },
-            }
-          );
-          let balAmount = ethers.utils.formatUnits(
-            cw20BalOnTerra.balance,
-            tokenDefinition.decimals
-          );
-          // let initialCW20BalOnTerra: number = parseInt(balAmount);
-
-          // approve the bridge to spend tokens
-          await approveEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            TEST_ERC20,
-            signer,
-            amount
-          );
-          const mk = new MnemonicKey({
-            mnemonic: TERRA_PRIVATE_KEY,
-          });
-          const wallet = lcd.wallet(mk);
-          // transfer tokens
-          const receipt = await transferFromEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            signer,
-            TEST_ERC20,
-            amount,
-            CHAIN_ID_TERRA,
-            tryNativeToUint8Array(wallet.key.accAddress, CHAIN_ID_TERRA)
-          );
-          // get the sequence from the logs (needed to fetch the vaa)
-          const sequence = parseSequenceFromLogEth(
-            receipt,
-            ETH_CORE_BRIDGE_ADDRESS
-          );
-          const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
-          // poll until the guardian(s) witness and sign the vaa
-          const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_ETH,
-            emitterAddress,
-            sequence,
-            {
-              transport: NodeHttpTransport(),
-            }
-          );
-          expect(
-            await getIsTransferCompletedTerra(
-              TERRA_TOKEN_BRIDGE_ADDRESS,
-              signedVAA,
-              lcd,
-              TERRA_GAS_PRICES_URL
-            )
-          ).toBe(false);
-          const msg = await redeemOnTerra(
-            TERRA_TOKEN_BRIDGE_ADDRESS,
-            wallet.key.accAddress,
-            signedVAA
-          );
-          const gasPrices = await getTerraGasPrices();
-          const feeEstimate = await lcd.tx.estimateFee(
-            [
-              {
-                sequenceNumber: await wallet.sequence(),
-                publicKey: wallet.key.publicKey,
-              },
-            ],
-            {
-              msgs: [msg],
-              memo: "localhost",
-              feeDenoms: ["uluna"],
-              gasPrices,
-            }
-          );
-          const tx = await wallet.createAndSignTx({
-            msgs: [msg],
-            memo: "localhost",
-            feeDenoms: ["uluna"],
-            gasPrices,
-            fee: feeEstimate,
-          });
-          await lcd.tx.broadcast(tx);
-          expect(
-            await getIsTransferCompletedTerra(
-              TERRA_TOKEN_BRIDGE_ADDRESS,
-              signedVAA,
-              lcd,
-              TERRA_GAS_PRICES_URL
-            )
-          ).toBe(true);
-
-          // Get wallet balance on Eth
-          const finalBalOnEth = await token.balanceOf(
-            ETH_TEST_WALLET_PUBLIC_KEY
-          );
-          let finalBalOnEthStr = ethers.utils.formatUnits(finalBalOnEth, 18);
-          expect(
-            parseInt(initialBalOnEthStr) - parseInt(finalBalOnEthStr)
-          ).toEqual(1);
-
-          // Get wallet balance on Tera
-          cw20BalOnTerra = await lcd.wasm.contractQuery(foreignAsset, {
-            balance: {
-              address: TerraWalletAddress,
-            },
-          });
-          balAmount = ethers.utils.formatUnits(
-            cw20BalOnTerra.balance,
-            tokenDefinition.decimals
-          );
-          // let finalCW20BalOnTerra: number = parseInt(balAmount);
-          provider.destroy();
-          done();
-        } catch (e) {
-          console.error(e);
-          done("An error occurred while trying to send from Ethereum to Terra");
-        }
-      })();
-    });
-  });
-  describe("Terra deposit and transfer tokens", () => {
-    test("Tokens transferred can't exceed tokens deposited", (done) => {
-      (async () => {
-        try {
-          const lcd = new LCDClient({
-            URL: TERRA_NODE_URL,
-            chainID: TERRA_CHAIN_ID,
-            isClassic: true,
-          });
-          const mk = new MnemonicKey({
-            mnemonic: TERRA_PRIVATE_KEY,
-          });
-          const wallet = lcd.wallet(mk);
-          const gasPrices = await getTerraGasPrices();
-          // deposit some tokens (separate transactions)
-          for (let i = 0; i < 3; i++) {
-            const deposit = new MsgExecuteContract(
-              wallet.key.accAddress,
-              TERRA_TOKEN_BRIDGE_ADDRESS,
-              {
-                deposit_tokens: {},
-              },
-              { uusd: "900000087654321" }
-            );
-            const feeEstimate = await lcd.tx.estimateFee(
-              [
-                {
-                  sequenceNumber: await wallet.sequence(),
-                  publicKey: wallet.key.publicKey,
-                },
-              ],
-              {
-                msgs: [deposit],
-                memo: "localhost",
-                feeDenoms: ["uluna"],
-                gasPrices,
-              }
-            );
-            const tx = await wallet.createAndSignTx({
-              msgs: [deposit],
-              memo: "localhost",
-              feeDenoms: ["uluna"],
-              gasPrices,
-              fee: feeEstimate,
-            });
-            await lcd.tx.broadcast(tx);
-          }
-          const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
-          const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
-          // attempt to transfer more than we've deposited
-          const transfer = new MsgExecuteContract(
-            wallet.key.accAddress,
-            TERRA_TOKEN_BRIDGE_ADDRESS,
-            {
-              initiate_transfer: {
-                asset: {
-                  amount: "5900000087654321",
-                  info: {
-                    native_token: {
-                      denom: "uusd",
-                    },
-                  },
-                },
-                recipient_chain: CHAIN_ID_ETH,
-                recipient: Buffer.from(signer.publicKey).toString("base64"),
-                fee: "0",
-                nonce: Math.round(Math.round(Math.random() * 100000)),
-              },
-            },
-            {}
-          );
-          let error = false;
-          try {
-            await lcd.tx.estimateFee(
-              [
-                {
-                  sequenceNumber: await wallet.sequence(),
-                  publicKey: wallet.key.publicKey,
-                },
-              ],
-              {
-                msgs: [transfer],
-                memo: "localhost",
-                feeDenoms: ["uluna"],
-                gasPrices,
-              }
-            );
-          } catch (e) {
-            error = e.response.data.message.includes("Overflow: Cannot Sub");
-          }
-          expect(error).toEqual(true);
-          // withdraw the tokens we deposited
-          const withdraw = new MsgExecuteContract(
-            wallet.key.accAddress,
-            TERRA_TOKEN_BRIDGE_ADDRESS,
-            {
-              withdraw_tokens: {
-                asset: {
-                  native_token: {
-                    denom: "uusd",
-                  },
-                },
-              },
-            },
-            {}
-          );
-          const feeEstimate = await lcd.tx.estimateFee(
-            [
-              {
-                sequenceNumber: await wallet.sequence(),
-                publicKey: wallet.key.publicKey,
-              },
-            ],
-            {
-              msgs: [withdraw],
-              memo: "localhost",
-              feeDenoms: ["uluna"],
-              gasPrices,
-            }
-          );
-          const tx = await wallet.createAndSignTx({
-            msgs: [withdraw],
-            memo: "test",
-            feeDenoms: ["uluna"],
-            gasPrices,
-            fee: feeEstimate,
-          });
-          await lcd.tx.broadcast(tx);
-          provider.destroy();
-          done();
-        } catch (e) {
-          console.error(e);
-          done(
-            "An error occurred while testing deposits to and transfers from Terra"
-          );
-        }
-      })();
-    });
-  });
-  describe("Post VAA with retry", () => {
-    test("postVAA with retry, no failures", (done) => {
-      (async () => {
-        try {
-          // create a keypair for Solana
-          const connection = new Connection(SOLANA_HOST, "confirmed");
-          const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
-          const payerAddress = keypair.publicKey.toString();
-          const sequence = await transferFromEthToSolana();
-          const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
-          // poll until the guardian(s) witness and sign the vaa
-          const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_ETH,
-            emitterAddress,
-            sequence,
-            {
-              transport: NodeHttpTransport(),
-            }
-          );
-          let maxFailures = 0;
-          // post vaa to Solana
-
-          const postPromise = postVaaWithRetry(
-            connection,
-            async (transaction) => {
-              await new Promise(function (resolve) {
-                //We delay here so the connection has time to get wrecked
-                setTimeout(function () {
-                  resolve(500);
-                });
-              });
-              transaction.partialSign(keypair);
-              return transaction;
-            },
-            SOLANA_CORE_BRIDGE_ADDRESS,
-            payerAddress,
-            Buffer.from(signedVAA),
-            maxFailures
-          );
-
-          await postPromise;
-          // redeem tokens on solana
-          const transaction = await redeemOnSolana(
-            connection,
-            SOLANA_CORE_BRIDGE_ADDRESS,
-            SOLANA_TOKEN_BRIDGE_ADDRESS,
-            payerAddress,
-            signedVAA
-          );
-          // sign, send, and confirm transaction
-          transaction.partialSign(keypair);
-          const txid = await connection.sendRawTransaction(
-            transaction.serialize()
-          );
-          await connection.confirmTransaction(txid);
-          expect(
-            await getIsTransferCompletedSolana(
-              SOLANA_TOKEN_BRIDGE_ADDRESS,
-              signedVAA,
-              connection
-            )
-          ).toBe(true);
-          done();
-        } catch (e) {
-          console.error(e);
-          done(
-            "An error occurred while happy-path testing post VAA with retry."
-          );
-        }
-      })();
-    });
-    test("Reject on signature failure", (done) => {
-      (async () => {
-        try {
-          // create a keypair for Solana
-          const connection = new Connection(SOLANA_HOST, "confirmed");
-          const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
-          const payerAddress = keypair.publicKey.toString();
-          const sequence = await transferFromEthToSolana();
-          const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
-          // poll until the guardian(s) witness and sign the vaa
-          const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_ETH,
-            emitterAddress,
-            sequence,
-            {
-              transport: NodeHttpTransport(),
-            }
-          );
-          let maxFailures = 5;
-          // post vaa to Solana
-
-          let error = false;
-          try {
-            const postPromise = postVaaWithRetry(
-              connection,
-              async (transaction) => {
-                return Promise.reject();
-              },
-              SOLANA_CORE_BRIDGE_ADDRESS,
-              payerAddress,
-              Buffer.from(signedVAA),
-              maxFailures
-            );
-
-            await postPromise;
-          } catch (e) {
-            error = true;
-          }
-          expect(error).toBe(true);
-          done();
-        } catch (e) {
-          console.error(e);
-          done(
-            "An error occurred while trying to send from Ethereum to Solana"
-          );
-        }
-      })();
-    });
-  });
-  describe("Terra to Ethereum", () => {
-    test("Attestation from Terra to ETH", (done) => {
-      (async () => {
-        try {
-          const lcd = new LCDClient({
-            URL: TERRA_NODE_URL,
-            chainID: TERRA_CHAIN_ID,
-            isClassic: true,
-          });
-          const mk = new MnemonicKey({
-            mnemonic: TERRA_PRIVATE_KEY,
-          });
-          const wallet = lcd.wallet(mk);
-          const Asset: string = "uluna";
-          const TerraWalletAddress: string =
-            "terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v";
-          const msg = await attestFromTerra(
-            TERRA_TOKEN_BRIDGE_ADDRESS,
-            TerraWalletAddress,
-            Asset
-          );
-          const gasPrices = await getTerraGasPrices();
-          const feeEstimate = await lcd.tx.estimateFee(
-            [
-              {
-                sequenceNumber: await wallet.sequence(),
-                publicKey: wallet.key.publicKey,
-              },
-            ],
-            {
-              msgs: [msg],
-              memo: "localhost",
-              feeDenoms: ["uusd"],
-              gasPrices,
-            }
-          );
-          const executeTx = await wallet.createAndSignTx({
-            msgs: [msg],
-            memo: "Testing...",
-            feeDenoms: ["uusd"],
-            gasPrices,
-            fee: feeEstimate,
-          });
-          const result = await lcd.tx.broadcast(executeTx);
-          const info = await waitForTerraExecution(result.txhash, lcd);
-          if (!info) {
-            throw new Error("info not found");
-          }
-          const sequence = parseSequenceFromLogTerra(info);
-          if (!sequence) {
-            throw new Error("Sequence not found");
-          }
-          const emitterAddress = await getEmitterAddressTerra(
-            TERRA_TOKEN_BRIDGE_ADDRESS
-          );
-          const signedVaa = await getSignedVAABySequence(
-            CHAIN_ID_TERRA,
-            sequence,
-            emitterAddress
-          );
-          const provider = new ethers.providers.WebSocketProvider(
-            ETH_NODE_URL
-          ) as any;
-          const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
-          let success: boolean = true;
-          try {
-            const cr = await createWrappedOnEth(
-              ETH_TOKEN_BRIDGE_ADDRESS,
-              signer,
-              signedVaa
-            );
-          } catch (e) {
-            success = false;
-          }
-          if (!success) {
-            const cr = await updateWrappedOnEth(
-              ETH_TOKEN_BRIDGE_ADDRESS,
-              signer,
-              signedVaa
-            );
-            success = true;
-          }
-        } catch (e) {
-          console.error("Attestation failure: ", e);
-        }
-        done();
-      })();
-    });
-    test("Transfer from Terra", (done) => {
-      (async () => {
-        try {
-          const lcd = new LCDClient({
-            URL: TERRA_NODE_URL,
-            chainID: TERRA_CHAIN_ID,
-            isClassic: true,
-          });
-          const mk = new MnemonicKey({
-            mnemonic: TERRA_PRIVATE_KEY,
-          });
-          const Asset: string = "uluna";
-          const FeeAsset: string = "uusd";
-          const Amount: string = "1000000";
-
-          // Get initial balance of luna on Terra
-          const initialTerraBalance: number = await queryBalanceOnTerra(Asset);
-
-          // Get initial balance of uusd on Terra
-          // const initialFeeBalance: number = await queryBalanceOnTerra(FeeAsset);
-
-          // Get initial balance of wrapped luna on Eth
-          const ETH_TEST_WALLET_PUBLIC_KEY =
-            "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1";
-          const provider = new ethers.providers.WebSocketProvider(
-            ETH_NODE_URL
-          ) as any;
-          const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
-          const originAssetHex = tryNativeToHexString(Asset, CHAIN_ID_TERRA);
-          if (!originAssetHex) {
-            throw new Error("originAssetHex is null");
-          }
-          const foreignAsset = await getForeignAssetEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            provider,
-            CHAIN_ID_TERRA,
-            hexToUint8Array(originAssetHex)
-          );
-          if (!foreignAsset) {
-            throw new Error("foreignAsset is null");
-          }
-          let token = TokenImplementation__factory.connect(
-            foreignAsset,
-            signer
-          );
-
-          // Get initial balance of wrapped luna on ethereum
-          const initialLunaBalOnEth = await token.balanceOf(
-            ETH_TEST_WALLET_PUBLIC_KEY
-          );
-          const initialLunaBalOnEthInt = parseInt(initialLunaBalOnEth._hex);
-
-          // Start transfer from Terra to Ethereum
-          const hexStr = tryNativeToHexString(
-            ETH_TEST_WALLET_PUBLIC_KEY,
-            CHAIN_ID_ETH
-          );
-          if (!hexStr) {
-            throw new Error("Failed to convert to hexStr");
-          }
-          const wallet = lcd.wallet(mk);
-          const msgs = await transferFromTerra(
-            wallet.key.accAddress,
-            TERRA_TOKEN_BRIDGE_ADDRESS,
-            Asset,
-            Amount,
-            CHAIN_ID_ETH,
-            hexToUint8Array(hexStr) // This needs to be ETH wallet
-          );
-          const gasPrices = await getTerraGasPrices();
-          const feeEstimate = await lcd.tx.estimateFee(
-            [
-              {
-                sequenceNumber: await wallet.sequence(),
-                publicKey: wallet.key.publicKey,
-              },
-            ],
-            {
-              msgs: msgs,
-              memo: "localhost",
-              feeDenoms: [FeeAsset],
-              gasPrices,
-            }
-          );
-          const executeTx = await wallet.createAndSignTx({
-            msgs: msgs,
-            memo: "Testing transfer...",
-            feeDenoms: [FeeAsset],
-            gasPrices,
-            fee: feeEstimate,
-          });
-          const result = await lcd.tx.broadcast(executeTx);
-          const info = await waitForTerraExecution(result.txhash, lcd);
-          if (!info) {
-            throw new Error("info not found");
-          }
-
-          // Get VAA in order to do redemption step
-          const sequence = parseSequenceFromLogTerra(info);
-          if (!sequence) {
-            throw new Error("Sequence not found");
-          }
-          const emitterAddress = await getEmitterAddressTerra(
-            TERRA_TOKEN_BRIDGE_ADDRESS
-          );
-          const signedVaa = await getSignedVAABySequence(
-            CHAIN_ID_TERRA,
-            sequence,
-            emitterAddress
-          );
-          const roe = await redeemOnEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            signer,
-            signedVaa
-          );
-          expect(
-            await getIsTransferCompletedEth(
-              ETH_TOKEN_BRIDGE_ADDRESS,
-              provider,
-              signedVaa
-            )
-          ).toBe(true);
-
-          // Test finished.  Check wallet balances
-          // Get final balance of uluna on Terra
-          const finalTerraBalance = await queryBalanceOnTerra(Asset);
-
-          // Get final balance of uusd on Terra
-          // const finalFeeBalance: number = await queryBalanceOnTerra(FeeAsset);
-          expect(initialTerraBalance - 1e6 === finalTerraBalance).toBe(true);
-          const lunaBalOnEthAfter = await token.balanceOf(
-            ETH_TEST_WALLET_PUBLIC_KEY
-          );
-          const lunaBalOnEthAfterInt = parseInt(lunaBalOnEthAfter._hex);
-          expect(initialLunaBalOnEthInt + 1e6 === lunaBalOnEthAfterInt).toBe(
-            true
-          );
-        } catch (e) {
-          console.error("Terra to Ethereum failure: ", e);
-          done("Terra to Ethereum Failure");
-          return;
-        }
-        done();
-      })();
-    });
-    test("Transfer wrapped luna back to Terra", (done) => {
-      (async () => {
-        try {
-          // Get initial wallet balances
-          const lcd = new LCDClient({
-            URL: TERRA_NODE_URL,
-            chainID: TERRA_CHAIN_ID,
-            isClassic: true,
-          });
-          const mk = new MnemonicKey({
-            mnemonic: TERRA_PRIVATE_KEY,
-          });
-          const Asset: string = "uluna";
-          const initialTerraBalance: number = await queryBalanceOnTerra(Asset);
-          const ETH_TEST_WALLET_PUBLIC_KEY =
-            "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1";
-          const provider = new ethers.providers.WebSocketProvider(
-            ETH_NODE_URL
-          ) as any;
-          const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
-          const originAssetHex = tryNativeToHexString(Asset, CHAIN_ID_TERRA);
-          if (!originAssetHex) {
-            throw new Error("originAssetHex is null");
-          }
-          const foreignAsset = await getForeignAssetEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            provider,
-            CHAIN_ID_TERRA,
-            hexToUint8Array(originAssetHex)
-          );
-          if (!foreignAsset) {
-            throw new Error("foreignAsset is null");
-          }
-          let token = TokenImplementation__factory.connect(
-            foreignAsset,
-            signer
-          );
-          const initialLunaBalOnEth = await token.balanceOf(
-            ETH_TEST_WALLET_PUBLIC_KEY
-          );
-          const initialLunaBalOnEthInt = parseInt(initialLunaBalOnEth._hex);
-          const Amount: string = "1000000";
-
-          // approve the bridge to spend tokens
-          await approveEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            foreignAsset,
-            signer,
-            Amount
-          );
-
-          // transfer wrapped luna from Ethereum to Terra
-          const wallet = lcd.wallet(mk);
-          const receipt = await transferFromEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            signer,
-            foreignAsset,
-            Amount,
-            CHAIN_ID_TERRA,
-            tryNativeToUint8Array(wallet.key.accAddress, CHAIN_ID_TERRA)
-          );
-
-          // get the sequence from the logs (needed to fetch the vaa)
-          const sequence = parseSequenceFromLogEth(
-            receipt,
-            ETH_CORE_BRIDGE_ADDRESS
-          );
-          const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
-
-          // poll until the guardian(s) witness and sign the vaa
-          const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_ETH,
-            emitterAddress,
-            sequence,
-            {
-              transport: NodeHttpTransport(),
-            }
-          );
-          const msg = await redeemOnTerra(
-            TERRA_TOKEN_BRIDGE_ADDRESS,
-            wallet.key.accAddress,
-            signedVAA
-          );
-          const gasPrices = await getTerraGasPrices();
-          const feeEstimate = await lcd.tx.estimateFee(
-            [
-              {
-                sequenceNumber: await wallet.sequence(),
-                publicKey: wallet.key.publicKey,
-              },
-            ],
-            {
-              msgs: [msg],
-              memo: "localhost",
-              feeDenoms: ["uusd"],
-              gasPrices,
-            }
-          );
-          const tx = await wallet.createAndSignTx({
-            msgs: [msg],
-            memo: "localhost",
-            feeDenoms: ["uusd"],
-            gasPrices,
-            fee: feeEstimate,
-          });
-          await lcd.tx.broadcast(tx);
-          expect(
-            await getIsTransferCompletedTerra(
-              TERRA_TOKEN_BRIDGE_ADDRESS,
-              signedVAA,
-              lcd,
-              TERRA_GAS_PRICES_URL
-            )
-          ).toBe(true);
-
-          // Check wallet balances after
-          const finalTerraBalance = await queryBalanceOnTerra(Asset);
-          expect(initialTerraBalance + 1e6 === finalTerraBalance).toBe(true);
-          const finalLunaBalOnEth = await token.balanceOf(
-            ETH_TEST_WALLET_PUBLIC_KEY
-          );
-          const finalLunaBalOnEthInt = parseInt(finalLunaBalOnEth._hex);
-          expect(initialLunaBalOnEthInt - 1e6 === finalLunaBalOnEthInt).toBe(
-            true
-          );
-          // const uusdBal = await queryBalanceOnTerra("uusd");
-        } catch (e) {
-          console.error("Transfer back failure: ", e);
-          done("Transfer back Failure");
-          return;
-        }
-        done();
-      })();
-    });
-  });
-  describe("Terra <=> Ethereum roundtrip", () => {
-    test("Transfer CW20 token from Terra to Ethereum and back again", (done) => {
-      (async () => {
-        try {
-          const CW20: string = "terra13nkgqrfymug724h8pprpexqj9h629sa3ncw7sh";
-          const Asset: string = "uluna";
-          const FeeAsset: string = "uusd";
-          const Amount: string = "1000000";
-          const TerraWalletAddress: string =
-            "terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v";
-
-          interface Cw20Balance {
-            balance: string;
-          }
-
-          const lcd = new LCDClient({
-            URL: TERRA_NODE_URL,
-            chainID: TERRA_CHAIN_ID,
-            isClassic: true,
-          });
-          const mk = new MnemonicKey({
-            mnemonic: TERRA_PRIVATE_KEY,
-          });
-          const wallet = lcd.wallet(mk);
-
-          // This is the attestation phase of the CW20 token
-          let msg = await attestFromTerra(
-            TERRA_TOKEN_BRIDGE_ADDRESS,
-            TerraWalletAddress,
-            CW20
-          );
-          const gasPrices = await getTerraGasPrices();
-          let feeEstimate = await lcd.tx.estimateFee(
-            [
-              {
-                sequenceNumber: await wallet.sequence(),
-                publicKey: wallet.key.publicKey,
-              },
-            ],
-            {
-              msgs: [msg],
-              memo: "localhost",
-              feeDenoms: [FeeAsset],
-              gasPrices,
-            }
-          );
-          let executeTx = await wallet.createAndSignTx({
-            msgs: [msg],
-            memo: "Testing...",
-            feeDenoms: [FeeAsset],
-            gasPrices,
-            fee: feeEstimate,
-          });
-          let result = await lcd.tx.broadcast(executeTx);
-          let info = await waitForTerraExecution(result.txhash, lcd);
-          if (!info) {
-            throw new Error("info not found");
-          }
-          let sequence = parseSequenceFromLogTerra(info);
-          if (!sequence) {
-            throw new Error("Sequence not found");
-          }
-          let emitterAddress = await getEmitterAddressTerra(
-            TERRA_TOKEN_BRIDGE_ADDRESS
-          );
-          let signedVaa = await getSignedVAABySequence(
-            CHAIN_ID_TERRA,
-            sequence,
-            emitterAddress
-          );
-          const provider = new ethers.providers.WebSocketProvider(
-            ETH_NODE_URL
-          ) as any;
-          const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
-          let success: boolean = true;
-          try {
-            const cr = await createWrappedOnEth(
-              ETH_TOKEN_BRIDGE_ADDRESS,
-              signer,
-              signedVaa
-            );
-          } catch (e) {
-            success = false;
-          }
-          if (!success) {
-            const cr = await updateWrappedOnEth(
-              ETH_TOKEN_BRIDGE_ADDRESS,
-              signer,
-              signedVaa
-            );
-            success = true;
-          }
-          // Attestation is complete
-
-          // Get initial balance of uusd on Terra
-          // const initialFeeBalance: number = await queryBalanceOnTerra(FeeAsset);
-
-          // Get wallet on eth
-          const originAssetHex = tryNativeToHexString(CW20, CHAIN_ID_TERRA);
-          if (!originAssetHex) {
-            throw new Error("originAssetHex is null");
-          }
-          const foreignAsset = await getForeignAssetEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            provider,
-            CHAIN_ID_TERRA,
-            hexToUint8Array(originAssetHex)
-          );
-          if (!foreignAsset) {
-            throw new Error("foreignAsset is null");
-          }
-          let token = TokenImplementation__factory.connect(
-            foreignAsset,
-            signer
-          );
-          const ETH_TEST_WALLET_PUBLIC_KEY =
-            "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1";
-          const initialCW20BalOnEth = await token.balanceOf(
-            ETH_TEST_WALLET_PUBLIC_KEY
-          );
-          let initialCW20BalOnEthInt = parseInt(initialCW20BalOnEth._hex);
-
-          // Get initial balance of CW20 on Terra
-          const tokenDefinition: any = await lcd.wasm.contractQuery(CW20, {
-            token_info: {},
-          });
-          let cw20BalOnTerra: Cw20Balance = await lcd.wasm.contractQuery(CW20, {
-            balance: {
-              address: TerraWalletAddress,
-            },
-          });
-          let amount = ethers.utils.formatUnits(
-            cw20BalOnTerra.balance,
-            tokenDefinition.decimals
-          );
-          let initialCW20BalOnTerra: number = parseInt(amount);
-          const hexStr = tryNativeToHexString(
-            ETH_TEST_WALLET_PUBLIC_KEY,
-            CHAIN_ID_ETH
-          );
-          if (!hexStr) {
-            throw new Error("Failed to convert to hexStr");
-          }
-          const msgs = await transferFromTerra(
-            wallet.key.accAddress,
-            TERRA_TOKEN_BRIDGE_ADDRESS,
-            CW20,
-            Amount,
-            CHAIN_ID_ETH,
-            hexToUint8Array(hexStr) // This needs to be ETH wallet
-          );
-          feeEstimate = await lcd.tx.estimateFee(
-            [
-              {
-                sequenceNumber: await wallet.sequence(),
-                publicKey: wallet.key.publicKey,
-              },
-            ],
-            {
-              msgs: msgs,
-              memo: "localhost",
-              feeDenoms: [FeeAsset],
-              gasPrices,
-            }
-          );
-          executeTx = await wallet.createAndSignTx({
-            msgs: msgs,
-            memo: "Testing transfer...",
-            feeDenoms: [FeeAsset],
-            gasPrices,
-            fee: feeEstimate,
-          });
-          result = await lcd.tx.broadcast(executeTx);
-          info = await waitForTerraExecution(result.txhash, lcd);
-          if (!info) {
-            throw new Error("info not found");
-          }
-          sequence = parseSequenceFromLogTerra(info);
-          if (!sequence) {
-            throw new Error("Sequence not found");
-          }
-          emitterAddress = await getEmitterAddressTerra(
-            TERRA_TOKEN_BRIDGE_ADDRESS
-          );
-          signedVaa = await getSignedVAABySequence(
-            CHAIN_ID_TERRA,
-            sequence,
-            emitterAddress
-          );
-          const roe = await redeemOnEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            signer,
-            signedVaa
-          );
-          expect(
-            await getIsTransferCompletedEth(
-              ETH_TOKEN_BRIDGE_ADDRESS,
-              provider,
-              signedVaa
-            )
-          ).toBe(true);
-
-          // Check the wallet balances
-          let finalCW20BalOnEth = await token.balanceOf(
-            ETH_TEST_WALLET_PUBLIC_KEY
-          );
-          let finalCW20BalOnEthInt = parseInt(finalCW20BalOnEth._hex);
-          expect(initialCW20BalOnEthInt + 1e6 === finalCW20BalOnEthInt).toBe(
-            true
-          );
-          cw20BalOnTerra = await lcd.wasm.contractQuery(CW20, {
-            balance: {
-              address: TerraWalletAddress,
-            },
-          });
-          amount = ethers.utils.formatUnits(
-            cw20BalOnTerra.balance,
-            tokenDefinition.decimals
-          );
-          let finalCW20BalOnTerra: number = parseInt(amount);
-          expect(initialCW20BalOnTerra - finalCW20BalOnTerra === 1).toBe(true);
-          // Done checking wallet balances
-
-          // Start the reverse transfer from Ethereum back to Terra
-          // Get initial wallet balances
-          initialCW20BalOnTerra = finalCW20BalOnTerra;
-          initialCW20BalOnEthInt = finalCW20BalOnEthInt;
-
-          // approve the bridge to spend tokens
-          await approveEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            foreignAsset,
-            signer,
-            Amount
-          );
-
-          // transfer token from Ethereum to Terra
-          const receipt = await transferFromEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            signer,
-            foreignAsset,
-            Amount,
-            CHAIN_ID_TERRA,
-            tryNativeToUint8Array(wallet.key.accAddress, CHAIN_ID_TERRA)
-          );
-
-          // get the sequence from the logs (needed to fetch the vaa)
-          sequence = parseSequenceFromLogEth(receipt, ETH_CORE_BRIDGE_ADDRESS);
-          emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
-
-          // poll until the guardian(s) witness and sign the vaa
-          const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_ETH,
-            emitterAddress,
-            sequence,
-            {
-              transport: NodeHttpTransport(),
-            }
-          );
-          msg = await redeemOnTerra(
-            TERRA_TOKEN_BRIDGE_ADDRESS,
-            wallet.key.accAddress,
-            signedVAA
-          );
-          feeEstimate = await lcd.tx.estimateFee(
-            [
-              {
-                sequenceNumber: await wallet.sequence(),
-                publicKey: wallet.key.publicKey,
-              },
-            ],
-            {
-              msgs: [msg],
-              memo: "localhost",
-              feeDenoms: ["uusd"],
-              gasPrices,
-            }
-          );
-          const tx = await wallet.createAndSignTx({
-            msgs: [msg],
-            memo: "localhost",
-            feeDenoms: ["uusd"],
-            gasPrices,
-            fee: feeEstimate,
-          });
-          await lcd.tx.broadcast(tx);
-          expect(
-            await getIsTransferCompletedTerra(
-              TERRA_TOKEN_BRIDGE_ADDRESS,
-              signedVAA,
-              lcd,
-              TERRA_GAS_PRICES_URL
-            )
-          ).toBe(true);
-
-          // Check wallet balances after transfer back
-          finalCW20BalOnEth = await token.balanceOf(ETH_TEST_WALLET_PUBLIC_KEY);
-          finalCW20BalOnEthInt = parseInt(finalCW20BalOnEth._hex);
-          expect(initialCW20BalOnEthInt - 1e6 === finalCW20BalOnEthInt).toBe(
-            true
-          );
-          cw20BalOnTerra = await lcd.wasm.contractQuery(CW20, {
-            balance: {
-              address: TerraWalletAddress,
-            },
-          });
-          amount = ethers.utils.formatUnits(
-            cw20BalOnTerra.balance,
-            tokenDefinition.decimals
-          );
-          finalCW20BalOnTerra = parseInt(amount);
-          expect(finalCW20BalOnTerra - initialCW20BalOnTerra === 1).toBe(true);
-          // Done checking wallet balances
-        } catch (e) {
-          console.error("CW20 Transfer failure: ", e);
-          done("CW20 Transfer Failure");
-          return;
-        }
-        done();
-      })();
-    });
-  });
-  describe("Algorand tests", () => {
-    test("Algorand transfer native ALGO to Eth and back again", (done) => {
-      (async () => {
-        try {
-          const client: algosdk.Algodv2 = getAlgoClient();
-          const tempAccts: Account[] = await getTempAccounts();
-          const numAccts: number = tempAccts.length;
-          expect(numAccts).toBeGreaterThan(0);
-          const wallet: Account = tempAccts[0];
-
-          // let accountInfo = await client.accountInformation(wallet.addr).do();
-          // Asset Index of native ALGO is 0
-          const AlgoIndex = BigInt(0);
-          // const b = await getBalances(client, wallet.addr);
-          const txs = await attestFromAlgorand(
-            client,
-            TOKEN_BRIDGE_ID,
-            CORE_ID,
-            wallet.addr,
-            AlgoIndex
-          );
-
-          const result = await signSendAndConfirmAlgorand(client, txs, wallet);
-
-          const sn = parseSequenceFromLogAlgorand(result);
-
-          // Now, try to send a NOP
-          const suggParams: algosdk.SuggestedParams = await client
-            .getTransactionParams()
-            .do();
-          const nopTxn = makeApplicationCallTxnFromObject({
-            from: wallet.addr,
-            appIndex: safeBigIntToNumber(TOKEN_BRIDGE_ID),
-            onComplete: OnApplicationComplete.NoOpOC,
-            appArgs: [textToUint8Array("nop")],
-            suggestedParams: suggParams,
-          });
-          const resp = await client
-            .sendRawTransaction(nopTxn.signTxn(wallet.sk))
-            .do();
-          await waitForConfirmation(client, resp.txId, 1);
-          // End of NOP
-
-          const emitterAddr = getEmitterAddressAlgorand(TOKEN_BRIDGE_ID);
-          const { vaaBytes } = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_ALGORAND,
-            emitterAddr,
-            sn,
-            { transport: NodeHttpTransport() }
-          );
-          const pvaa = _parseVAAAlgorand(vaaBytes);
-          const provider = new ethers.providers.WebSocketProvider(
-            ETH_NODE_URL
-          ) as any;
-          const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
-          let success: boolean = true;
-          try {
-            const cr = await createWrappedOnEth(
-              ETH_TOKEN_BRIDGE_ADDRESS,
-              signer,
-              vaaBytes
-            );
-          } catch (e) {
-            success = false;
-          }
-          if (!success) {
-            try {
-              const cr = await updateWrappedOnEth(
-                ETH_TOKEN_BRIDGE_ADDRESS,
-                signer,
-                vaaBytes
-              );
-              success = true;
-            } catch (e) {
-              console.error("failed to updateWrappedOnEth", e);
-            }
-          }
-          // Check wallet
-          const a = parseInt(AlgoIndex.toString());
-          const originAssetHex = (
-            "0000000000000000000000000000000000000000000000000000000000000000" +
-            a.toString(16)
-          ).slice(-64);
-          const foreignAsset = await getForeignAssetEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            provider,
-            CHAIN_ID_ALGORAND,
-            hexToUint8Array(originAssetHex)
-          );
-          if (!foreignAsset) {
-            throw new Error("foreignAsset is null");
-          }
-          let token = TokenImplementation__factory.connect(
-            foreignAsset,
-            signer
-          );
-
-          // Get initial balance on ethereum
-          const ETH_TEST_WALLET_PUBLIC_KEY =
-            "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1";
-          const initialBalOnEth = await token.balanceOf(
-            ETH_TEST_WALLET_PUBLIC_KEY
-          );
-          const initialBalOnEthInt = parseInt(initialBalOnEth._hex);
-
-          // Get initial balance on Algorand
-          let algoWalletBals: Map<number, number> = await getBalances(
-            client,
-            wallet.addr
-          );
-          const startingAlgoBal = algoWalletBals.get(
-            safeBigIntToNumber(AlgoIndex)
-          );
-          if (!startingAlgoBal) {
-            throw new Error("startingAlgoBal is undefined");
-          }
-
-          // Start transfer from Algorand to Ethereum
-          const hexStr = nativeToHexString(
-            ETH_TEST_WALLET_PUBLIC_KEY,
-            CHAIN_ID_ETH
-          );
-          if (!hexStr) {
-            throw new Error("Failed to convert to hexStr");
-          }
-          const AmountToTransfer: number = 12300;
-          const Fee: number = 0;
-          const transferTxs = await transferFromAlgorand(
-            client,
-            TOKEN_BRIDGE_ID,
-            CORE_ID,
-            wallet.addr,
-            AlgoIndex,
-            BigInt(AmountToTransfer),
-            hexStr,
-            CHAIN_ID_ETH,
-            BigInt(Fee)
-          );
-          const transferResult = await signSendAndConfirmAlgorand(
-            client,
-            transferTxs,
-            wallet
-          );
-          const txSid = parseSequenceFromLogAlgorand(transferResult);
-          const signedVaa = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_ALGORAND,
-            emitterAddr,
-            txSid,
-            { transport: NodeHttpTransport() }
-          );
-          const pv = _parseVAAAlgorand(signedVaa.vaaBytes);
-          const roe = await redeemOnEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            signer,
-            signedVaa.vaaBytes
-          );
-          expect(
-            await getIsTransferCompletedEth(
-              ETH_TOKEN_BRIDGE_ADDRESS,
-              provider,
-              signedVaa.vaaBytes
-            )
-          ).toBe(true);
-          // Test finished.  Check wallet balances
-          const balOnEthAfter = await token.balanceOf(
-            ETH_TEST_WALLET_PUBLIC_KEY
-          );
-          const balOnEthAfterInt = parseInt(balOnEthAfter._hex);
-          expect(balOnEthAfterInt - initialBalOnEthInt).toEqual(
-            AmountToTransfer
-          );
-
-          // Get final balance on Algorand
-          algoWalletBals = await getBalances(client, wallet.addr);
-          const finalAlgoBal = algoWalletBals.get(
-            safeBigIntToNumber(AlgoIndex)
-          );
-          if (!finalAlgoBal) {
-            throw new Error("finalAlgoBal is undefined");
-          }
-          // expect(startingAlgoBal - finalAlgoBal).toBe(AmountToTransfer);
-
-          // Attempt to transfer from Eth back to Algorand
-          const Amount: string = "100";
-
-          // approve the bridge to spend tokens
-          await approveEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            foreignAsset,
-            signer,
-            Amount
-          );
-          const receipt = await transferFromEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            signer,
-            foreignAsset,
-            Amount,
-            CHAIN_ID_ALGORAND,
-            decodeAddress(wallet.addr).publicKey
-          );
-          // get the sequence from the logs (needed to fetch the vaa)
-          const sequence = parseSequenceFromLogEth(
-            receipt,
-            ETH_CORE_BRIDGE_ADDRESS
-          );
-          const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
-
-          // poll until the guardian(s) witness and sign the vaa
-          const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_ETH,
-            emitterAddress,
-            sequence,
-            {
-              transport: NodeHttpTransport(),
-            }
-          );
-          algoWalletBals = await getBalances(client, wallet.addr);
-          const redeemTxs = await redeemOnAlgorand(
-            client,
-            TOKEN_BRIDGE_ID,
-            CORE_ID,
-            signedVAA,
-            wallet.addr
-          );
-          await signSendAndConfirmAlgorand(client, redeemTxs, wallet);
-          const completed = await getIsTransferCompletedAlgorand(
-            client,
-            TOKEN_BRIDGE_ID,
-            signedVAA
-          );
-          expect(completed).toBe(true);
-          // const newBal = await token.balanceOf(ETH_TEST_WALLET_PUBLIC_KEY);
-          // const newBalInt = parseInt(newBal._hex);
-          // expect(newBalInt).toBe(AmountToTransfer - parseInt(Amount));
-
-          // Get second final balance on Algorand
-          algoWalletBals = await getBalances(client, wallet.addr);
-          const secondFinalAlgoBal = algoWalletBals.get(
-            safeBigIntToNumber(AlgoIndex)
-          );
-          if (!secondFinalAlgoBal) {
-            throw new Error("secondFinalAlgoBal is undefined");
-          }
-          // expect(secondFinalAlgoBal - finalAlgoBal).toBe(
-          //   parseInt(Amount) * 100
-          // );
-          provider.destroy();
-        } catch (e) {
-          console.error("Algorand ALGO transfer error:", e);
-          done("Algorand ALGO transfer error");
-          return;
-        }
-        done();
-      })();
-    });
-    test("Algorand create chuckNorium, transfer to Eth and back again", (done) => {
-      (async () => {
-        try {
-          const client: algosdk.Algodv2 = getAlgoClient();
-          const tempAccts: Account[] = await getTempAccounts();
-          const numAccts: number = tempAccts.length;
-          expect(numAccts).toBeGreaterThan(0);
-          const wallet: Account = tempAccts[0];
-
-          // let accountInfo = await client.accountInformation(wallet.addr).do();
-
-          const assetIndex: number = await createAsset(wallet);
-          const attestTxs = await attestFromAlgorand(
-            client,
-            TOKEN_BRIDGE_ID,
-            CORE_ID,
-            wallet.addr,
-            BigInt(assetIndex)
-          );
-          const attestResult = await signSendAndConfirmAlgorand(
-            client,
-            attestTxs,
-            wallet
-          );
-          const attestSn = parseSequenceFromLogAlgorand(attestResult);
-
-          // Now, try to send a NOP
-          const suggParams: algosdk.SuggestedParams = await client
-            .getTransactionParams()
-            .do();
-          const nopTxn = makeApplicationCallTxnFromObject({
-            from: wallet.addr,
-            appIndex: safeBigIntToNumber(TOKEN_BRIDGE_ID),
-            onComplete: OnApplicationComplete.NoOpOC,
-            appArgs: [textToUint8Array("nop")],
-            suggestedParams: suggParams,
-          });
-          const resp = await client
-            .sendRawTransaction(nopTxn.signTxn(wallet.sk))
-            .do();
-          await waitForConfirmation(client, resp.txId, 1);
-          // End of NOP
-
-          const emitterAddr = getEmitterAddressAlgorand(TOKEN_BRIDGE_ID);
-          const { vaaBytes } = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_ALGORAND,
-            emitterAddr,
-            attestSn,
-            { transport: NodeHttpTransport() }
-          );
-          const provider = new ethers.providers.WebSocketProvider(
-            ETH_NODE_URL
-          ) as any;
-          const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
-          let success: boolean = true;
-          try {
-            const cr = await createWrappedOnEth(
-              ETH_TOKEN_BRIDGE_ADDRESS,
-              signer,
-              vaaBytes
-            );
-          } catch (e) {
-            success = false;
-          }
-          if (!success) {
-            try {
-              const cr = await updateWrappedOnEth(
-                ETH_TOKEN_BRIDGE_ADDRESS,
-                signer,
-                vaaBytes
-              );
-              success = true;
-            } catch (e) {
-              console.error("failed to updateWrappedOnEth", e);
-              done("failed to update attestation on Eth");
-              return;
-            }
-          }
-          // Check wallet
-          const a = parseInt(assetIndex.toString());
-          const originAssetHex = (
-            "0000000000000000000000000000000000000000000000000000000000000000" +
-            a.toString(16)
-          ).slice(-64);
-          const foreignAsset = await getForeignAssetEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            provider,
-            CHAIN_ID_ALGORAND,
-            hexToUint8Array(originAssetHex)
-          );
-          if (!foreignAsset) {
-            throw new Error("foreignAsset is null");
-          }
-          let token = TokenImplementation__factory.connect(
-            foreignAsset,
-            signer
-          );
-
-          // Get initial balance on ethereum
-          const ETH_TEST_WALLET_PUBLIC_KEY =
-            "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1";
-          // const initialBalOnEth = await token.balanceOf(
-          //   ETH_TEST_WALLET_PUBLIC_KEY
-          // );
-          // const initialBalOnEthInt = parseInt(initialBalOnEth._hex);
-
-          // Get initial balance on Algorand
-          let algoWalletBals: Map<number, number> = await getBalances(
-            client,
-            wallet.addr
-          );
-          const startingAlgoBal = algoWalletBals.get(assetIndex);
-          if (!startingAlgoBal) {
-            throw new Error("startingAlgoBal is undefined");
-          }
-
-          // Start transfer from Algorand to Ethereum
-          const hexStr = nativeToHexString(
-            ETH_TEST_WALLET_PUBLIC_KEY,
-            CHAIN_ID_ETH
-          );
-          if (!hexStr) {
-            throw new Error("Failed to convert to hexStr");
-          }
-          const AmountToTransfer: number = 12300;
-          const Fee: number = 0;
-          const transferTxs = await transferFromAlgorand(
-            client,
-            TOKEN_BRIDGE_ID,
-            CORE_ID,
-            wallet.addr,
-            BigInt(assetIndex),
-            BigInt(AmountToTransfer),
-            hexStr,
-            CHAIN_ID_ETH,
-            BigInt(Fee)
-          );
-          const transferResult = await signSendAndConfirmAlgorand(
-            client,
-            transferTxs,
-            wallet
-          );
-          const txSid = parseSequenceFromLogAlgorand(transferResult);
-          const signedVaa = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_ALGORAND,
-            emitterAddr,
-            txSid,
-            { transport: NodeHttpTransport() }
-          );
-          await redeemOnEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            signer,
-            signedVaa.vaaBytes
-          );
-          expect(
-            await getIsTransferCompletedEth(
-              ETH_TOKEN_BRIDGE_ADDRESS,
-              provider,
-              signedVaa.vaaBytes
-            )
-          ).toBe(true);
-          // Test finished.  Check wallet balances
-          const balOnEthAfter = await token.balanceOf(
-            ETH_TEST_WALLET_PUBLIC_KEY
-          );
-          const balOnEthAfterInt = parseInt(balOnEthAfter._hex);
-          const FinalAmt: number = AmountToTransfer / 100;
-          expect(balOnEthAfterInt).toEqual(FinalAmt);
-
-          // Get final balance on Algorand
-          algoWalletBals = await getBalances(client, wallet.addr);
-          const finalAlgoBal = algoWalletBals.get(assetIndex);
-          if (!finalAlgoBal) {
-            throw new Error("finalAlgoBal is undefined");
-          }
-          expect(startingAlgoBal - finalAlgoBal).toBe(AmountToTransfer);
-
-          // Attempt to transfer from Eth back to Algorand
-          const Amount: string = "100";
-
-          // approve the bridge to spend tokens
-          await approveEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            foreignAsset,
-            signer,
-            Amount
-          );
-          const receipt = await transferFromEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            signer,
-            foreignAsset,
-            Amount,
-            CHAIN_ID_ALGORAND,
-            decodeAddress(wallet.addr).publicKey
-          );
-          // get the sequence from the logs (needed to fetch the vaa)
-          const sequence = parseSequenceFromLogEth(
-            receipt,
-            ETH_CORE_BRIDGE_ADDRESS
-          );
-          const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
-
-          // poll until the guardian(s) witness and sign the vaa
-          const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_ETH,
-            emitterAddress,
-            sequence,
-            {
-              transport: NodeHttpTransport(),
-            }
-          );
-          const redeemTxs = await redeemOnAlgorand(
-            client,
-            TOKEN_BRIDGE_ID,
-            CORE_ID,
-            signedVAA,
-            wallet.addr
-          );
-          await signSendAndConfirmAlgorand(client, redeemTxs, wallet);
-          const completed = await getIsTransferCompletedAlgorand(
-            client,
-            TOKEN_BRIDGE_ID,
-            signedVAA
-          );
-          expect(completed).toBe(true);
-          const newBal = await token.balanceOf(ETH_TEST_WALLET_PUBLIC_KEY);
-          const newBalInt = parseInt(newBal._hex);
-          expect(newBalInt).toBe(FinalAmt - parseInt(Amount));
-
-          // Get second final balance on Algorand
-          algoWalletBals = await getBalances(client, wallet.addr);
-          const secondFinalAlgoBal = algoWalletBals.get(assetIndex);
-          if (!secondFinalAlgoBal) {
-            throw new Error("secondFinalAlgoBal is undefined");
-          }
-          expect(secondFinalAlgoBal - finalAlgoBal).toBe(
-            parseInt(Amount) * 100
-          );
-          provider.destroy();
-        } catch (e) {
-          console.error("Algorand chuckNorium transfer error:", e);
-          done("Algorand chuckNorium transfer error");
-          return;
-        }
-        done();
-      })();
-    });
-    test("Transfer wrapped Luna from Terra to Algorand and back again", (done) => {
-      (async () => {
-        try {
-          const tbAddr: string = getApplicationAddress(TOKEN_BRIDGE_ID);
-          const decTbAddr: Uint8Array = decodeAddress(tbAddr).publicKey;
-          const aa: string = uint8ArrayToHex(decTbAddr);
-          const client: algosdk.Algodv2 = getAlgoClient();
-          const tempAccts: Account[] = await getTempAccounts();
-          const numAccts: number = tempAccts.length;
-          expect(numAccts).toBeGreaterThan(0);
-          const algoWallet: Account = tempAccts[0];
-          const lcd = new LCDClient({
-            URL: TERRA_NODE_URL,
-            chainID: TERRA_CHAIN_ID,
-            isClassic: true,
-          });
-          const mk = new MnemonicKey({
-            mnemonic: TERRA_PRIVATE_KEY,
-          });
-          const terraWallet = lcd.wallet(mk);
-          const Asset: string = "uluna";
-          // const Asset: string = "uusd";
-          const FeeAsset: string = "uusd";
-          const Amount: string = "1000000";
-          const TerraWalletAddress: string =
-            "terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v";
-          const msg = await attestFromTerra(
-            TERRA_TOKEN_BRIDGE_ADDRESS,
-            TerraWalletAddress,
-            Asset
-          );
-          const gasPrices = await getTerraGasPrices();
-          let feeEstimate = await lcd.tx.estimateFee(
-            [
-              {
-                sequenceNumber: await terraWallet.sequence(),
-                publicKey: terraWallet.key.publicKey,
-              },
-            ],
-            {
-              msgs: [msg],
-              memo: "localhost",
-              feeDenoms: [FeeAsset],
-              gasPrices,
-            }
-          );
-          const executeAttest = await terraWallet.createAndSignTx({
-            msgs: [msg],
-            memo: "Testing...",
-            feeDenoms: [FeeAsset],
-            gasPrices,
-            fee: feeEstimate,
-          });
-          const attestResult = await lcd.tx.broadcast(executeAttest);
-          const attestInfo = await waitForTerraExecution(
-            attestResult.txhash,
-            lcd
-          );
-          if (!attestInfo) {
-            throw new Error("info not found");
-          }
-          const attestSn = parseSequenceFromLogTerra(attestInfo);
-          if (!attestSn) {
-            throw new Error("Sequence not found");
-          }
-          const emitterAddress = await getEmitterAddressTerra(
-            TERRA_TOKEN_BRIDGE_ADDRESS
-          );
-          const attestSignedVaa = await getSignedVAABySequence(
-            CHAIN_ID_TERRA,
-            attestSn,
-            emitterAddress
-          );
-          const createWrappedTxs = await createWrappedOnAlgorand(
-            client,
-            TOKEN_BRIDGE_ID,
-            CORE_ID,
-            algoWallet.addr,
-            attestSignedVaa
-          );
-          await signSendAndConfirmAlgorand(
-            client,
-            createWrappedTxs,
-            algoWallet
-          );
-
-          let assetIdCreated = await getForeignAssetFromVaaAlgorand(
-            client,
-            TOKEN_BRIDGE_ID,
-            attestSignedVaa
-          );
-          if (!assetIdCreated) {
-            throw new Error("Failed to create asset");
-          }
-
-          // Start of transfer from Terra to Algorand
-          // Get initial balance of luna on Terra
-          const initialTerraBalance: number = await queryBalanceOnTerra(Asset);
-
-          // Get initial balance of uusd on Terra
-          // const initialFeeBalance: number = await queryBalanceOnTerra(FeeAsset);
-
-          // Get initial balance of wrapped luna on Algorand
-          const originAssetHex = nativeToHexString(Asset, CHAIN_ID_TERRA);
-          if (!originAssetHex) {
-            throw new Error("originAssetHex is null");
-          }
-          // TODO:  Get wallet balance on Algorand
-
-          // Get Balances
-          const tbBals: Map<number, number> = await getBalances(
-            client,
-            algoWallet.addr
-            // "TPFKQBOR7RJ475XW6XMOZMSMBCZH6WNGFQNT7CM7NL2UMBCMBIU5PVBGPM"
-          );
-          let assetIdCreatedBegBal: number = 0;
-          const tempBal = tbBals.get(safeBigIntToNumber(assetIdCreated));
-          if (tempBal) {
-            assetIdCreatedBegBal = tempBal;
-          }
-
-          // Start transfer from Terra to Algorand
-          const txMsgs = await transferFromTerra(
-            terraWallet.key.accAddress,
-            TERRA_TOKEN_BRIDGE_ADDRESS,
-            Asset,
-            Amount,
-            CHAIN_ID_ALGORAND,
-            decodeAddress(algoWallet.addr).publicKey // This needs to be Algorand wallet
-          );
-          const executeTx = await terraWallet.createAndSignTx({
-            msgs: txMsgs,
-            memo: "Testing transfer...",
-            feeDenoms: [FeeAsset],
-            gasPrices,
-            fee: feeEstimate,
-          });
-          const txResult = await lcd.tx.broadcast(executeTx);
-          const txInfo = await waitForTerraExecution(txResult.txhash, lcd);
-          if (!txInfo) {
-            throw new Error("info not found");
-          }
-
-          // Get VAA in order to do redemption step
-          const txSn = parseSequenceFromLogTerra(txInfo);
-          if (!txSn) {
-            throw new Error("Sequence not found");
-          }
-          const txSignedVaa = await getSignedVAABySequence(
-            CHAIN_ID_TERRA,
-            txSn,
-            emitterAddress
-          );
-          const redeemTxs = await redeemOnAlgorand(
-            client,
-            TOKEN_BRIDGE_ID,
-            CORE_ID,
-            txSignedVaa,
-            algoWallet.addr
-          );
-          await signSendAndConfirmAlgorand(client, redeemTxs, algoWallet);
-          expect(
-            await getIsTransferCompletedAlgorand(
-              client,
-              TOKEN_BRIDGE_ID,
-              txSignedVaa
-            )
-          ).toBe(true);
-
-          // Test finished.  Check wallet balances
-          // Get Balances
-          const bals: Map<number, number> = await getBalances(
-            client,
-            algoWallet.addr
-          );
-          let assetIdCreatedEndBal: number = 0;
-          const tmpBal = bals.get(safeBigIntToNumber(assetIdCreated));
-          if (tmpBal) {
-            assetIdCreatedEndBal = tmpBal;
-          }
-          expect(assetIdCreatedEndBal - assetIdCreatedBegBal).toBe(
-            parseInt(Amount)
-          );
-
-          // Get final balance of uluna on Terra
-          const finalTerraBalance = await queryBalanceOnTerra(Asset);
-
-          // Get final balance of uusd on Terra
-          // const finalFeeBalance: number = await queryBalanceOnTerra(FeeAsset);
-          expect(initialTerraBalance - 1e6 === finalTerraBalance).toBe(true);
-
-          // Start of transfer back to Terra
-          const TransferBackAmount: number = 100000;
-
-          // transfer wrapped luna from Algorand to Terra
-          const terraHexStr = nativeToHexString(
-            terraWallet.key.accAddress,
-            CHAIN_ID_TERRA
-          );
-          if (!terraHexStr) {
-            throw new Error("Failed to convert to hexStr");
-          }
-          const Fee: number = 0;
-          const transferTxs = await transferFromAlgorand(
-            client,
-            TOKEN_BRIDGE_ID,
-            CORE_ID,
-            algoWallet.addr,
-            assetIdCreated,
-            BigInt(TransferBackAmount),
-            terraHexStr,
-            CHAIN_ID_TERRA,
-            BigInt(Fee)
-          );
-          const transferResult = await signSendAndConfirmAlgorand(
-            client,
-            transferTxs,
-            algoWallet
-          );
-          const txSid = parseSequenceFromLogAlgorand(transferResult);
-          const signedVaa = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_ALGORAND,
-            aa,
-            txSid,
-            { transport: NodeHttpTransport() }
-          );
-
-          const redeemMsg = await redeemOnTerra(
-            TERRA_TOKEN_BRIDGE_ADDRESS,
-            terraWallet.key.accAddress,
-            signedVaa.vaaBytes
-          );
-          feeEstimate = await lcd.tx.estimateFee(
-            [
-              {
-                sequenceNumber: await terraWallet.sequence(),
-                publicKey: terraWallet.key.publicKey,
-              },
-            ],
-            {
-              msgs: [redeemMsg],
-              memo: "localhost",
-              feeDenoms: [FeeAsset],
-              gasPrices,
-            }
-          );
-          const tx = await terraWallet.createAndSignTx({
-            msgs: [redeemMsg],
-            memo: "localhost",
-            feeDenoms: ["uusd"],
-            gasPrices,
-            fee: feeEstimate,
-          });
-          await lcd.tx.broadcast(tx);
-          expect(
-            await getIsTransferCompletedTerra(
-              TERRA_TOKEN_BRIDGE_ADDRESS,
-              signedVaa.vaaBytes,
-              lcd,
-              TERRA_GAS_PRICES_URL
-            )
-          ).toBe(true);
-
-          // Check wallet balances after
-          const finalLunaOnTerraBalance = await queryBalanceOnTerra(Asset);
-          expect(finalLunaOnTerraBalance - finalTerraBalance).toBe(
-            TransferBackAmount
-          );
-          const retBals: Map<number, number> = await getBalances(
-            client,
-            algoWallet.addr
-          );
-          let assetIdCreatedFinBal: number = 0;
-          const tBal = retBals.get(safeBigIntToNumber(assetIdCreated));
-          if (tBal) {
-            assetIdCreatedFinBal = tBal;
-          }
-          expect(assetIdCreatedEndBal - assetIdCreatedFinBal).toBe(
-            TransferBackAmount
-          );
-          const info: WormholeWrappedInfo = await getOriginalAssetAlgorand(
-            client,
-            TOKEN_BRIDGE_ID,
-            assetIdCreated
-          );
-          expect(info.chainId).toBe(CHAIN_ID_TERRA);
-          expect(info.isWrapped).toBe(true);
-        } catch (e) {
-          console.error("Terra <=> Algorand error:", e);
-          done("Terra <=> Algorand error");
-        }
-        done();
-      })();
-    });
-    test("Testing relay type redeem", (done) => {
-      (async () => {
-        try {
-          const client: algosdk.Algodv2 = getAlgoClient();
-          const tempAccts: Account[] = await getTempAccounts();
-          const numAccts: number = tempAccts.length;
-          expect(numAccts).toBeGreaterThan(0);
-          const algoWallet: Account = tempAccts[0];
-          const algoWalletBalance = await getBalance(
-            client,
-            algoWallet.addr,
-            BigInt(0)
-          );
-          expect(algoWalletBalance).toBeGreaterThan(0);
-          const relayerWallet: Account = tempAccts[1];
-          const relayerWalletBalance = await getBalance(
-            client,
-            relayerWallet.addr,
-            BigInt(0)
-          );
-          expect(relayerWalletBalance).toBeGreaterThan(0);
-          // ETH setup to transfer LUNA to Algorand
-
-          // create a signer for Eth
-          const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
-          const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
-          // attest the test token
-          const receipt = await attestFromEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            signer,
-            TEST_ERC20
-          );
-          // get the sequence from the logs (needed to fetch the vaa)
-          const sequence = parseSequenceFromLogEth(
-            receipt,
-            ETH_CORE_BRIDGE_ADDRESS
-          );
-          const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
-          // poll until the guardian(s) witness and sign the vaa
-          const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_ETH,
-            emitterAddress,
-            sequence,
-            {
-              transport: NodeHttpTransport(),
-            }
-          );
-          const createWrappedTxs = await createWrappedOnAlgorand(
-            client,
-            TOKEN_BRIDGE_ID,
-            CORE_ID,
-            algoWallet.addr,
-            signedVAA
-          );
-          await signSendAndConfirmAlgorand(
-            client,
-            createWrappedTxs,
-            algoWallet
-          );
-
-          let assetIdCreated = await getForeignAssetFromVaaAlgorand(
-            client,
-            TOKEN_BRIDGE_ID,
-            signedVAA
-          );
-          if (!assetIdCreated) {
-            throw new Error("Failed to create asset");
-          }
-
-          // Start of transfer from ETH to Algorand
-          // approve the bridge to spend tokens
-          const amount = parseUnits("2", 18);
-          const halfAmount = parseUnits("1", 18);
-          await approveEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            TEST_ERC20,
-            signer,
-            amount
-          );
-          // transfer half the tokens directly
-          const firstHalfReceipt = await transferFromEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            signer,
-            TEST_ERC20,
-            halfAmount,
-            CHAIN_ID_ALGORAND,
-            decodeAddress(algoWallet.addr).publicKey // This needs to be Algorand wallet
-          );
-          // get the sequence from the logs (needed to fetch the vaa)
-          const firstHalfSn = parseSequenceFromLogEth(
-            firstHalfReceipt,
-            ETH_CORE_BRIDGE_ADDRESS
-          );
-          const ethEmitterAddress = getEmitterAddressEth(
-            ETH_TOKEN_BRIDGE_ADDRESS
-          );
-          // poll until the guardian(s) witness and sign the vaa
-          const { vaaBytes: firstHalfVaa } = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_ETH,
-            ethEmitterAddress,
-            firstHalfSn,
-            {
-              transport: NodeHttpTransport(),
-            }
-          );
-
-          // Redeem half the amount on Algorand
-          const firstHalfRedeemTxs = await redeemOnAlgorand(
-            client,
-            TOKEN_BRIDGE_ID,
-            CORE_ID,
-            firstHalfVaa,
-            algoWallet.addr
-          );
-          await signSendAndConfirmAlgorand(
-            client,
-            firstHalfRedeemTxs,
-            algoWallet
-          );
-          expect(
-            await getIsTransferCompletedAlgorand(
-              client,
-              TOKEN_BRIDGE_ID,
-              firstHalfVaa
-            )
-          ).toBe(true);
-          // transfer second half of tokens via relayer
-          const secondHalfReceipt = await transferFromEth(
-            ETH_TOKEN_BRIDGE_ADDRESS,
-            signer,
-            TEST_ERC20,
-            halfAmount,
-            CHAIN_ID_ALGORAND,
-            decodeAddress(algoWallet.addr).publicKey // This needs to be Algorand wallet
-          );
-          // get the sequence from the logs (needed to fetch the vaa)
-          const secondHalfSn = parseSequenceFromLogEth(
-            secondHalfReceipt,
-            ETH_CORE_BRIDGE_ADDRESS
-          );
-          // poll until the guardian(s) witness and sign the vaa
-          const { vaaBytes: secondHalfVaa } = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_ETH,
-            ethEmitterAddress,
-            secondHalfSn,
-            {
-              transport: NodeHttpTransport(),
-            }
-          );
-
-          // Redeem second half the amount on Algorand
-          const redeemTxs = await redeemOnAlgorand(
-            client,
-            TOKEN_BRIDGE_ID,
-            CORE_ID,
-            secondHalfVaa,
-            relayerWallet.addr
-          );
-          await signSendAndConfirmAlgorand(client, redeemTxs, relayerWallet);
-          expect(
-            await getIsTransferCompletedAlgorand(
-              client,
-              TOKEN_BRIDGE_ID,
-              secondHalfVaa
-            )
-          ).toBe(true);
-          provider.destroy();
-        } catch (e) {
-          console.error("new test error:", e);
-          done("new test error");
-          return;
-        }
-        done();
-      })();
-    });
-
-    test("testing algorand payload3", (done) => {
-      (async () => {
-        try {
-          const tbAddr: string = getApplicationAddress(TOKEN_BRIDGE_ID);
-          const decTbAddr: Uint8Array = decodeAddress(tbAddr).publicKey;
-          const aa: string = uint8ArrayToHex(decTbAddr);
-
-          const client: algosdk.Algodv2 = getAlgoClient();
-          const tempAccts: Account[] = await getTempAccounts();
-          const numAccts: number = tempAccts.length;
-          expect(numAccts).toBeGreaterThan(0);
-          const algoWallet: Account = tempAccts[0];
-
-          const Fee: number = 0;
-          var testapp: number = 8;
-          var dest = utils
-            .hexZeroPad(BigNumber.from(testapp).toHexString(), 32)
-            .substring(2);
-
-          const transferTxs = await transferFromAlgorand(
-            client,
-            TOKEN_BRIDGE_ID,
-            CORE_ID,
-            algoWallet.addr,
-            BigInt(0),
-            BigInt(100),
-            dest,
-            CHAIN_ID_ALGORAND,
-            BigInt(Fee),
-            hexToUint8Array("ff")
-          );
-
-          const transferResult = await signSendAndConfirmAlgorand(
-            client,
-            transferTxs,
-            algoWallet
-          );
-          const txSid = parseSequenceFromLogAlgorand(transferResult);
-          const signedVaa = await getSignedVAAWithRetry(
-            WORMHOLE_RPC_HOSTS,
-            CHAIN_ID_ALGORAND,
-            aa,
-            txSid,
-            { transport: NodeHttpTransport() }
-          );
-
-          const txns = await redeemOnAlgorand(
-            client,
-            TOKEN_BRIDGE_ID,
-            CORE_ID,
-            signedVaa.vaaBytes,
-            algoWallet.addr
-          );
-
-          const wbefore = await getBalance(
-            client,
-            getApplicationAddress(testapp),
-            BigInt(0)
-          );
-
-          await signSendAndConfirmAlgorand(client, txns, algoWallet);
-          expect(
-            await getIsTransferCompletedAlgorand(
-              client,
-              TOKEN_BRIDGE_ID,
-              signedVaa.vaaBytes
-            )
-          ).toBe(true);
-          const wafter = await getBalance(
-            client,
-            getApplicationAddress(testapp),
-            BigInt(0)
-          );
-
-          expect(BigInt(wafter - wbefore) === BigInt(100));
-        } catch (e) {
-          console.error("new test error:", e);
-          done("new test error");
-          return;
-        }
-        done();
-      })();
-    });
-  });
-});

+ 280 - 0
sdk/js/src/token_bridge/__tests__/solana-integration.ts

@@ -0,0 +1,280 @@
+import { formatUnits, parseUnits } from "@ethersproject/units";
+import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
+import { describe, expect, jest, test } from "@jest/globals";
+import {
+  ASSOCIATED_TOKEN_PROGRAM_ID,
+  Token,
+  TOKEN_PROGRAM_ID,
+} from "@solana/spl-token";
+import {
+  Connection,
+  Keypair,
+  PublicKey,
+  TokenAccountsFilter,
+} from "@solana/web3.js";
+import { ethers } from "ethers";
+import {
+  attestFromSolana,
+  CHAIN_ID_ETH,
+  CHAIN_ID_SOLANA,
+  CONTRACTS,
+  createWrappedOnEth,
+  getEmitterAddressSolana,
+  getForeignAssetEth,
+  getIsTransferCompletedEth,
+  hexToUint8Array,
+  parseSequenceFromLogSolana,
+  redeemOnEth,
+  TokenImplementation__factory,
+  transferFromSolana,
+  tryNativeToHexString,
+  tryNativeToUint8Array,
+} from "../..";
+import getSignedVAAWithRetry from "../../rpc/getSignedVAAWithRetry";
+import { setDefaultWasm } from "../../solana/wasm";
+import {
+  ETH_NODE_URL,
+  ETH_PRIVATE_KEY3,
+  SOLANA_HOST,
+  SOLANA_PRIVATE_KEY,
+  TEST_SOLANA_TOKEN,
+  WORMHOLE_RPC_HOSTS,
+} from "./consts";
+
+setDefaultWasm("node");
+
+jest.setTimeout(60000);
+
+describe("Solana to Ethereum", () => {
+  test("Attest Solana SPL to Ethereum", (done) => {
+    (async () => {
+      try {
+        // create a keypair for Solana
+        const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
+        const payerAddress = keypair.publicKey.toString();
+        // attest the test token
+        const connection = new Connection(SOLANA_HOST, "confirmed");
+        const transaction = await attestFromSolana(
+          connection,
+          CONTRACTS.DEVNET.solana.core,
+          CONTRACTS.DEVNET.solana.token_bridge,
+          payerAddress,
+          TEST_SOLANA_TOKEN
+        );
+        // sign, send, and confirm transaction
+        transaction.partialSign(keypair);
+        const txid = await connection.sendRawTransaction(
+          transaction.serialize()
+        );
+        await connection.confirmTransaction(txid);
+        const info = await connection.getTransaction(txid);
+        if (!info) {
+          throw new Error(
+            "An error occurred while fetching the transaction info"
+          );
+        }
+        // get the sequence from the logs (needed to fetch the vaa)
+        const sequence = parseSequenceFromLogSolana(info);
+        const emitterAddress = await getEmitterAddressSolana(
+          CONTRACTS.DEVNET.solana.token_bridge
+        );
+        // poll until the guardian(s) witness and sign the vaa
+        const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
+          WORMHOLE_RPC_HOSTS,
+          CHAIN_ID_SOLANA,
+          emitterAddress,
+          sequence,
+          {
+            transport: NodeHttpTransport(),
+          }
+        );
+        // create a signer for Eth
+        const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
+        const signer = new ethers.Wallet(ETH_PRIVATE_KEY3, provider);
+        try {
+          await createWrappedOnEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            signer,
+            signedVAA
+          );
+        } catch (e) {
+          // this could fail because the token is already attested (in an unclean env)
+        }
+        provider.destroy();
+        done();
+      } catch (e) {
+        console.error(e);
+        done(
+          "An error occurred while trying to attest from Solana to Ethereum"
+        );
+      }
+    })();
+  });
+  test("Solana SPL is attested on Ethereum", async () => {
+    const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
+    const address = getForeignAssetEth(
+      CONTRACTS.DEVNET.ethereum.token_bridge,
+      provider,
+      "solana",
+      tryNativeToUint8Array(TEST_SOLANA_TOKEN, "solana")
+    );
+    expect(address).toBeTruthy();
+    expect(address).not.toBe(ethers.constants.AddressZero);
+    provider.destroy();
+  });
+  test("Send Solana SPL to Ethereum", (done) => {
+    (async () => {
+      try {
+        // create a signer for Eth
+        const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
+        const signer = new ethers.Wallet(ETH_PRIVATE_KEY3, provider);
+        const targetAddress = await signer.getAddress();
+        // create a keypair for Solana
+        const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
+        const payerAddress = keypair.publicKey.toString();
+        // find the associated token account
+        const fromAddress = (
+          await Token.getAssociatedTokenAddress(
+            ASSOCIATED_TOKEN_PROGRAM_ID,
+            TOKEN_PROGRAM_ID,
+            new PublicKey(TEST_SOLANA_TOKEN),
+            keypair.publicKey
+          )
+        ).toString();
+
+        const connection = new Connection(SOLANA_HOST, "confirmed");
+
+        // Get the initial solana token balance
+        const tokenFilter: TokenAccountsFilter = {
+          programId: TOKEN_PROGRAM_ID,
+        };
+        let results = await connection.getParsedTokenAccountsByOwner(
+          keypair.publicKey,
+          tokenFilter
+        );
+        let initialSolanaBalance: number = 0;
+        for (const item of results.value) {
+          const tokenInfo = item.account.data.parsed.info;
+          const address = tokenInfo.mint;
+          const amount = tokenInfo.tokenAmount.uiAmount;
+          if (tokenInfo.mint === TEST_SOLANA_TOKEN) {
+            initialSolanaBalance = amount;
+          }
+        }
+
+        // Get the initial wallet balance on Eth
+        const originAssetHex = tryNativeToHexString(
+          TEST_SOLANA_TOKEN,
+          CHAIN_ID_SOLANA
+        );
+        if (!originAssetHex) {
+          throw new Error("originAssetHex is null");
+        }
+        const foreignAsset = await getForeignAssetEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          provider,
+          CHAIN_ID_SOLANA,
+          hexToUint8Array(originAssetHex)
+        );
+        if (!foreignAsset) {
+          throw new Error("foreignAsset is null");
+        }
+        let token = TokenImplementation__factory.connect(foreignAsset, signer);
+        const initialBalOnEth = await token.balanceOf(
+          await signer.getAddress()
+        );
+        const initialBalOnEthFormatted = formatUnits(initialBalOnEth._hex, 9);
+
+        // transfer the test token
+        const amount = parseUnits("1", 9).toBigInt();
+        const transaction = await transferFromSolana(
+          connection,
+          CONTRACTS.DEVNET.solana.core,
+          CONTRACTS.DEVNET.solana.token_bridge,
+          payerAddress,
+          fromAddress,
+          TEST_SOLANA_TOKEN,
+          amount,
+          tryNativeToUint8Array(targetAddress, CHAIN_ID_ETH),
+          CHAIN_ID_ETH
+        );
+        // sign, send, and confirm transaction
+        transaction.partialSign(keypair);
+        const txid = await connection.sendRawTransaction(
+          transaction.serialize()
+        );
+        await connection.confirmTransaction(txid);
+        const info = await connection.getTransaction(txid);
+        if (!info) {
+          throw new Error(
+            "An error occurred while fetching the transaction info"
+          );
+        }
+        // get the sequence from the logs (needed to fetch the vaa)
+        const sequence = parseSequenceFromLogSolana(info);
+        const emitterAddress = await getEmitterAddressSolana(
+          CONTRACTS.DEVNET.solana.token_bridge
+        );
+        // poll until the guardian(s) witness and sign the vaa
+        const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
+          WORMHOLE_RPC_HOSTS,
+          CHAIN_ID_SOLANA,
+          emitterAddress,
+          sequence,
+          {
+            transport: NodeHttpTransport(),
+          }
+        );
+        expect(
+          await getIsTransferCompletedEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            provider,
+            signedVAA
+          )
+        ).toBe(false);
+        await redeemOnEth(
+          CONTRACTS.DEVNET.ethereum.token_bridge,
+          signer,
+          signedVAA
+        );
+        expect(
+          await getIsTransferCompletedEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            provider,
+            signedVAA
+          )
+        ).toBe(true);
+
+        // Get final balance on Solana
+        results = await connection.getParsedTokenAccountsByOwner(
+          keypair.publicKey,
+          tokenFilter
+        );
+        let finalSolanaBalance: number = 0;
+        for (const item of results.value) {
+          const tokenInfo = item.account.data.parsed.info;
+          const address = tokenInfo.mint;
+          const amount = tokenInfo.tokenAmount.uiAmount;
+          if (tokenInfo.mint === TEST_SOLANA_TOKEN) {
+            finalSolanaBalance = amount;
+          }
+        }
+        expect(initialSolanaBalance - finalSolanaBalance).toBeCloseTo(1);
+
+        // Get the final balance on Eth
+        const finalBalOnEth = await token.balanceOf(await signer.getAddress());
+        const finalBalOnEthFormatted = formatUnits(finalBalOnEth._hex, 9);
+        expect(
+          parseInt(finalBalOnEthFormatted) -
+            parseInt(initialBalOnEthFormatted) ===
+            1
+        ).toBe(true);
+        provider.destroy();
+        done();
+      } catch (e) {
+        console.error(e);
+        done("An error occurred while trying to send from Solana to Ethereum");
+      }
+    })();
+  });
+});

+ 1167 - 0
sdk/js/src/token_bridge/__tests__/terra-integration.ts

@@ -0,0 +1,1167 @@
+import { parseUnits } from "@ethersproject/units";
+import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
+import { describe, expect, jest, test } from "@jest/globals";
+import {
+  LCDClient,
+  MnemonicKey,
+  MsgExecuteContract,
+} from "@terra-money/terra.js";
+import { ethers } from "ethers";
+import {
+  approveEth,
+  attestFromEth,
+  attestFromTerra,
+  CHAIN_ID_ETH,
+  CHAIN_ID_TERRA,
+  CONTRACTS,
+  createWrappedOnEth,
+  createWrappedOnTerra,
+  getEmitterAddressEth,
+  getEmitterAddressTerra,
+  getForeignAssetEth,
+  getForeignAssetTerra,
+  getIsTransferCompletedEth,
+  getIsTransferCompletedTerra,
+  hexToUint8Array,
+  parseSequenceFromLogEth,
+  parseSequenceFromLogTerra,
+  redeemOnEth,
+  redeemOnTerra,
+  TokenImplementation__factory,
+  transferFromEth,
+  transferFromTerra,
+  tryNativeToHexString,
+  tryNativeToUint8Array,
+  updateWrappedOnEth,
+} from "../..";
+import getSignedVAAWithRetry from "../../rpc/getSignedVAAWithRetry";
+import { setDefaultWasm } from "../../solana/wasm";
+import {
+  ETH_NODE_URL,
+  ETH_PRIVATE_KEY4,
+  TERRA_CHAIN_ID,
+  TERRA_GAS_PRICES_URL,
+  TERRA_NODE_URL,
+  TERRA_PRIVATE_KEY,
+  TEST_ERC20,
+  WORMHOLE_RPC_HOSTS,
+} from "./consts";
+import {
+  getSignedVAABySequence,
+  getTerraGasPrices,
+  queryBalanceOnTerra,
+  waitForTerraExecution,
+} from "./helpers";
+
+setDefaultWasm("node");
+
+jest.setTimeout(60000);
+
+describe("Terra Integration Tests", () => {
+  describe("Terra deposit and transfer tokens", () => {
+    test("Tokens transferred can't exceed tokens deposited", (done) => {
+      (async () => {
+        try {
+          const lcd = new LCDClient({
+            URL: TERRA_NODE_URL,
+            chainID: TERRA_CHAIN_ID,
+            isClassic: true,
+          });
+          const mk = new MnemonicKey({
+            mnemonic: TERRA_PRIVATE_KEY,
+          });
+          const wallet = lcd.wallet(mk);
+          const gasPrices = await getTerraGasPrices();
+          // deposit some tokens (separate transactions)
+          for (let i = 0; i < 3; i++) {
+            const deposit = new MsgExecuteContract(
+              wallet.key.accAddress,
+              CONTRACTS.DEVNET.terra.token_bridge,
+              {
+                deposit_tokens: {},
+              },
+              { uusd: "900000087654321" }
+            );
+            const feeEstimate = await lcd.tx.estimateFee(
+              [
+                {
+                  sequenceNumber: await wallet.sequence(),
+                  publicKey: wallet.key.publicKey,
+                },
+              ],
+              {
+                msgs: [deposit],
+                memo: "localhost",
+                feeDenoms: ["uluna"],
+                gasPrices,
+              }
+            );
+            const tx = await wallet.createAndSignTx({
+              msgs: [deposit],
+              memo: "localhost",
+              feeDenoms: ["uluna"],
+              gasPrices,
+              fee: feeEstimate,
+            });
+            await lcd.tx.broadcast(tx);
+          }
+          const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
+          const signer = new ethers.Wallet(ETH_PRIVATE_KEY4, provider);
+          // attempt to transfer more than we've deposited
+          const transfer = new MsgExecuteContract(
+            wallet.key.accAddress,
+            CONTRACTS.DEVNET.terra.token_bridge,
+            {
+              initiate_transfer: {
+                asset: {
+                  amount: "5900000087654321",
+                  info: {
+                    native_token: {
+                      denom: "uusd",
+                    },
+                  },
+                },
+                recipient_chain: CHAIN_ID_ETH,
+                recipient: Buffer.from(await signer.getAddress()).toString(
+                  "base64"
+                ),
+                fee: "0",
+                nonce: Math.round(Math.round(Math.random() * 100000)),
+              },
+            },
+            {}
+          );
+          let error = false;
+          try {
+            await lcd.tx.estimateFee(
+              [
+                {
+                  sequenceNumber: await wallet.sequence(),
+                  publicKey: wallet.key.publicKey,
+                },
+              ],
+              {
+                msgs: [transfer],
+                memo: "localhost",
+                feeDenoms: ["uluna"],
+                gasPrices,
+              }
+            );
+          } catch (e) {
+            error = e.response.data.message.includes("Overflow: Cannot Sub");
+          }
+          expect(error).toEqual(true);
+          // withdraw the tokens we deposited
+          const withdraw = new MsgExecuteContract(
+            wallet.key.accAddress,
+            CONTRACTS.DEVNET.terra.token_bridge,
+            {
+              withdraw_tokens: {
+                asset: {
+                  native_token: {
+                    denom: "uusd",
+                  },
+                },
+              },
+            },
+            {}
+          );
+          const feeEstimate = await lcd.tx.estimateFee(
+            [
+              {
+                sequenceNumber: await wallet.sequence(),
+                publicKey: wallet.key.publicKey,
+              },
+            ],
+            {
+              msgs: [withdraw],
+              memo: "localhost",
+              feeDenoms: ["uluna"],
+              gasPrices,
+            }
+          );
+          const tx = await wallet.createAndSignTx({
+            msgs: [withdraw],
+            memo: "test",
+            feeDenoms: ["uluna"],
+            gasPrices,
+            fee: feeEstimate,
+          });
+          await lcd.tx.broadcast(tx);
+          provider.destroy();
+          done();
+        } catch (e) {
+          console.error(e);
+          done(
+            "An error occurred while testing deposits to and transfers from Terra"
+          );
+        }
+      })();
+    });
+  });
+  describe("Ethereum to Terra", () => {
+    test("Attest Ethereum ERC-20 to Terra", (done) => {
+      (async () => {
+        try {
+          // create a signer for Eth
+          const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
+          const signer = new ethers.Wallet(ETH_PRIVATE_KEY4, provider);
+          // attest the test token
+          const receipt = await attestFromEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            signer,
+            TEST_ERC20
+          );
+          // get the sequence from the logs (needed to fetch the vaa)
+          const sequence = parseSequenceFromLogEth(
+            receipt,
+            CONTRACTS.DEVNET.ethereum.core
+          );
+          const emitterAddress = getEmitterAddressEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge
+          );
+          // poll until the guardian(s) witness and sign the vaa
+          const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
+            WORMHOLE_RPC_HOSTS,
+            CHAIN_ID_ETH,
+            emitterAddress,
+            sequence,
+            {
+              transport: NodeHttpTransport(),
+            }
+          );
+          const lcd = new LCDClient({
+            URL: TERRA_NODE_URL,
+            chainID: TERRA_CHAIN_ID,
+            isClassic: true,
+          });
+          const mk = new MnemonicKey({
+            mnemonic: TERRA_PRIVATE_KEY,
+          });
+          const wallet = lcd.wallet(mk);
+          const msg = await createWrappedOnTerra(
+            CONTRACTS.DEVNET.terra.token_bridge,
+            wallet.key.accAddress,
+            signedVAA
+          );
+          const gasPrices = await getTerraGasPrices();
+          const feeEstimate = await lcd.tx.estimateFee(
+            [
+              {
+                sequenceNumber: await wallet.sequence(),
+                publicKey: wallet.key.publicKey,
+              },
+            ],
+            {
+              msgs: [msg],
+              feeDenoms: ["uluna"],
+              gasPrices,
+            }
+          );
+          const tx = await wallet.createAndSignTx({
+            msgs: [msg],
+            memo: "test",
+            feeDenoms: ["uluna"],
+            gasPrices,
+            fee: feeEstimate,
+          });
+          try {
+            await lcd.tx.broadcast(tx);
+          } catch (e) {
+            // this could fail because the token is already attested (in an unclean env)
+          }
+          provider.destroy();
+          done();
+        } catch (e) {
+          console.error(e);
+          done(
+            "An error occurred while trying to attest from Ethereum to Terra"
+          );
+        }
+      })();
+    });
+    test("Ethereum ERC-20 is attested on Terra", async () => {
+      const lcd = new LCDClient({
+        URL: TERRA_NODE_URL,
+        chainID: TERRA_CHAIN_ID,
+        isClassic: true,
+      });
+      const address = getForeignAssetTerra(
+        CONTRACTS.DEVNET.terra.token_bridge,
+        lcd,
+        "ethereum",
+        tryNativeToUint8Array(TEST_ERC20, "ethereum")
+      );
+      expect(address).toBeTruthy();
+    });
+    test("Send Ethereum ERC-20 to Terra", (done) => {
+      (async () => {
+        try {
+          // create a signer for Eth
+          const provider = new ethers.providers.WebSocketProvider(
+            ETH_NODE_URL
+          ) as any;
+          const signer = new ethers.Wallet(ETH_PRIVATE_KEY4, provider);
+          const amount = parseUnits("1", 18);
+          const ERC20 = "0x2D8BE6BF0baA74e0A907016679CaE9190e80dD0A";
+          const TerraWalletAddress: string =
+            "terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v";
+          interface Erc20Balance {
+            balance: string;
+          }
+          const lcd = new LCDClient({
+            URL: TERRA_NODE_URL,
+            chainID: TERRA_CHAIN_ID,
+            isClassic: true,
+          });
+
+          // Get initial wallet balances
+          let token = TokenImplementation__factory.connect(ERC20, signer);
+          const initialBalOnEth = await token.balanceOf(
+            await signer.getAddress()
+          );
+          let initialBalOnEthStr = ethers.utils.formatUnits(
+            initialBalOnEth,
+            18
+          );
+
+          // Get initial balance of ERC20 on Terra
+          const originAssetHex = tryNativeToHexString(ERC20, CHAIN_ID_ETH);
+          if (!originAssetHex) {
+            throw new Error("originAssetHex is null");
+          }
+          const foreignAsset = await getForeignAssetTerra(
+            CONTRACTS.DEVNET.terra.token_bridge,
+            lcd,
+            CHAIN_ID_ETH,
+            hexToUint8Array(originAssetHex)
+          );
+          if (!foreignAsset) {
+            throw new Error("foreignAsset is null");
+          }
+          const tokenDefinition: any = await lcd.wasm.contractQuery(
+            foreignAsset,
+            {
+              token_info: {},
+            }
+          );
+          let cw20BalOnTerra: Erc20Balance = await lcd.wasm.contractQuery(
+            foreignAsset,
+            {
+              balance: {
+                address: TerraWalletAddress,
+              },
+            }
+          );
+          let balAmount = ethers.utils.formatUnits(
+            cw20BalOnTerra.balance,
+            tokenDefinition.decimals
+          );
+          // let initialCW20BalOnTerra: number = parseInt(balAmount);
+
+          // approve the bridge to spend tokens
+          await approveEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            TEST_ERC20,
+            signer,
+            amount
+          );
+          const mk = new MnemonicKey({
+            mnemonic: TERRA_PRIVATE_KEY,
+          });
+          const wallet = lcd.wallet(mk);
+          // transfer tokens
+          const receipt = await transferFromEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            signer,
+            TEST_ERC20,
+            amount,
+            CHAIN_ID_TERRA,
+            tryNativeToUint8Array(wallet.key.accAddress, CHAIN_ID_TERRA)
+          );
+          // get the sequence from the logs (needed to fetch the vaa)
+          const sequence = parseSequenceFromLogEth(
+            receipt,
+            CONTRACTS.DEVNET.ethereum.core
+          );
+          const emitterAddress = getEmitterAddressEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge
+          );
+          // poll until the guardian(s) witness and sign the vaa
+          const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
+            WORMHOLE_RPC_HOSTS,
+            CHAIN_ID_ETH,
+            emitterAddress,
+            sequence,
+            {
+              transport: NodeHttpTransport(),
+            }
+          );
+          expect(
+            await getIsTransferCompletedTerra(
+              CONTRACTS.DEVNET.terra.token_bridge,
+              signedVAA,
+              lcd,
+              TERRA_GAS_PRICES_URL
+            )
+          ).toBe(false);
+          const msg = await redeemOnTerra(
+            CONTRACTS.DEVNET.terra.token_bridge,
+            wallet.key.accAddress,
+            signedVAA
+          );
+          const gasPrices = await getTerraGasPrices();
+          const feeEstimate = await lcd.tx.estimateFee(
+            [
+              {
+                sequenceNumber: await wallet.sequence(),
+                publicKey: wallet.key.publicKey,
+              },
+            ],
+            {
+              msgs: [msg],
+              memo: "localhost",
+              feeDenoms: ["uluna"],
+              gasPrices,
+            }
+          );
+          const tx = await wallet.createAndSignTx({
+            msgs: [msg],
+            memo: "localhost",
+            feeDenoms: ["uluna"],
+            gasPrices,
+            fee: feeEstimate,
+          });
+          await lcd.tx.broadcast(tx);
+          expect(
+            await getIsTransferCompletedTerra(
+              CONTRACTS.DEVNET.terra.token_bridge,
+              signedVAA,
+              lcd,
+              TERRA_GAS_PRICES_URL
+            )
+          ).toBe(true);
+
+          // Get wallet balance on Eth
+          const finalBalOnEth = await token.balanceOf(
+            await signer.getAddress()
+          );
+          let finalBalOnEthStr = ethers.utils.formatUnits(finalBalOnEth, 18);
+          expect(
+            parseInt(initialBalOnEthStr) - parseInt(finalBalOnEthStr)
+          ).toEqual(1);
+
+          // Get wallet balance on Tera
+          cw20BalOnTerra = await lcd.wasm.contractQuery(foreignAsset, {
+            balance: {
+              address: TerraWalletAddress,
+            },
+          });
+          balAmount = ethers.utils.formatUnits(
+            cw20BalOnTerra.balance,
+            tokenDefinition.decimals
+          );
+          // let finalCW20BalOnTerra: number = parseInt(balAmount);
+          provider.destroy();
+          done();
+        } catch (e) {
+          console.error(e);
+          done("An error occurred while trying to send from Ethereum to Terra");
+        }
+      })();
+    });
+  });
+  describe("Terra to Ethereum", () => {
+    test("Attestation from Terra to ETH", (done) => {
+      (async () => {
+        try {
+          const lcd = new LCDClient({
+            URL: TERRA_NODE_URL,
+            chainID: TERRA_CHAIN_ID,
+            isClassic: true,
+          });
+          const mk = new MnemonicKey({
+            mnemonic: TERRA_PRIVATE_KEY,
+          });
+          const wallet = lcd.wallet(mk);
+          const Asset: string = "uluna";
+          const TerraWalletAddress: string =
+            "terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v";
+          const msg = await attestFromTerra(
+            CONTRACTS.DEVNET.terra.token_bridge,
+            TerraWalletAddress,
+            Asset
+          );
+          const gasPrices = await getTerraGasPrices();
+          const feeEstimate = await lcd.tx.estimateFee(
+            [
+              {
+                sequenceNumber: await wallet.sequence(),
+                publicKey: wallet.key.publicKey,
+              },
+            ],
+            {
+              msgs: [msg],
+              memo: "localhost",
+              feeDenoms: ["uusd"],
+              gasPrices,
+            }
+          );
+          const executeTx = await wallet.createAndSignTx({
+            msgs: [msg],
+            memo: "Testing...",
+            feeDenoms: ["uusd"],
+            gasPrices,
+            fee: feeEstimate,
+          });
+          const result = await lcd.tx.broadcast(executeTx);
+          const info = await waitForTerraExecution(result.txhash, lcd);
+          if (!info) {
+            throw new Error("info not found");
+          }
+          const sequence = parseSequenceFromLogTerra(info);
+          if (!sequence) {
+            throw new Error("Sequence not found");
+          }
+          const emitterAddress = await getEmitterAddressTerra(
+            CONTRACTS.DEVNET.terra.token_bridge
+          );
+          const signedVaa = await getSignedVAABySequence(
+            CHAIN_ID_TERRA,
+            sequence,
+            emitterAddress
+          );
+          const provider = new ethers.providers.WebSocketProvider(
+            ETH_NODE_URL
+          ) as any;
+          const signer = new ethers.Wallet(ETH_PRIVATE_KEY4, provider);
+          let success: boolean = true;
+          try {
+            const cr = await createWrappedOnEth(
+              CONTRACTS.DEVNET.ethereum.token_bridge,
+              signer,
+              signedVaa
+            );
+          } catch (e) {
+            success = false;
+          }
+          if (!success) {
+            const cr = await updateWrappedOnEth(
+              CONTRACTS.DEVNET.ethereum.token_bridge,
+              signer,
+              signedVaa
+            );
+            success = true;
+          }
+        } catch (e) {
+          console.error("Attestation failure: ", e);
+        }
+        done();
+      })();
+    });
+    test("Transfer from Terra", (done) => {
+      (async () => {
+        try {
+          const lcd = new LCDClient({
+            URL: TERRA_NODE_URL,
+            chainID: TERRA_CHAIN_ID,
+            isClassic: true,
+          });
+          const mk = new MnemonicKey({
+            mnemonic: TERRA_PRIVATE_KEY,
+          });
+          const Asset: string = "uluna";
+          const FeeAsset: string = "uusd";
+          const Amount: string = "1000000";
+
+          // Get initial balance of luna on Terra
+          const initialTerraBalance: number = await queryBalanceOnTerra(Asset);
+
+          // Get initial balance of uusd on Terra
+          // const initialFeeBalance: number = await queryBalanceOnTerra(FeeAsset);
+
+          // Get initial balance of wrapped luna on Eth
+          const provider = new ethers.providers.WebSocketProvider(
+            ETH_NODE_URL
+          ) as any;
+          const signer = new ethers.Wallet(ETH_PRIVATE_KEY4, provider);
+          const originAssetHex = tryNativeToHexString(Asset, CHAIN_ID_TERRA);
+          if (!originAssetHex) {
+            throw new Error("originAssetHex is null");
+          }
+          const foreignAsset = await getForeignAssetEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            provider,
+            CHAIN_ID_TERRA,
+            hexToUint8Array(originAssetHex)
+          );
+          if (!foreignAsset) {
+            throw new Error("foreignAsset is null");
+          }
+          let token = TokenImplementation__factory.connect(
+            foreignAsset,
+            signer
+          );
+
+          // Get initial balance of wrapped luna on ethereum
+          const initialLunaBalOnEth = await token.balanceOf(
+            await signer.getAddress()
+          );
+          const initialLunaBalOnEthInt = parseInt(initialLunaBalOnEth._hex);
+
+          // Start transfer from Terra to Ethereum
+          const hexStr = tryNativeToHexString(
+            await signer.getAddress(),
+            CHAIN_ID_ETH
+          );
+          if (!hexStr) {
+            throw new Error("Failed to convert to hexStr");
+          }
+          const wallet = lcd.wallet(mk);
+          const msgs = await transferFromTerra(
+            wallet.key.accAddress,
+            CONTRACTS.DEVNET.terra.token_bridge,
+            Asset,
+            Amount,
+            CHAIN_ID_ETH,
+            hexToUint8Array(hexStr) // This needs to be ETH wallet
+          );
+          const gasPrices = await getTerraGasPrices();
+          const feeEstimate = await lcd.tx.estimateFee(
+            [
+              {
+                sequenceNumber: await wallet.sequence(),
+                publicKey: wallet.key.publicKey,
+              },
+            ],
+            {
+              msgs: msgs,
+              memo: "localhost",
+              feeDenoms: [FeeAsset],
+              gasPrices,
+            }
+          );
+          const executeTx = await wallet.createAndSignTx({
+            msgs: msgs,
+            memo: "Testing transfer...",
+            feeDenoms: [FeeAsset],
+            gasPrices,
+            fee: feeEstimate,
+          });
+          const result = await lcd.tx.broadcast(executeTx);
+          const info = await waitForTerraExecution(result.txhash, lcd);
+          if (!info) {
+            throw new Error("info not found");
+          }
+
+          // Get VAA in order to do redemption step
+          const sequence = parseSequenceFromLogTerra(info);
+          if (!sequence) {
+            throw new Error("Sequence not found");
+          }
+          const emitterAddress = await getEmitterAddressTerra(
+            CONTRACTS.DEVNET.terra.token_bridge
+          );
+          const signedVaa = await getSignedVAABySequence(
+            CHAIN_ID_TERRA,
+            sequence,
+            emitterAddress
+          );
+          const roe = await redeemOnEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            signer,
+            signedVaa
+          );
+          expect(
+            await getIsTransferCompletedEth(
+              CONTRACTS.DEVNET.ethereum.token_bridge,
+              provider,
+              signedVaa
+            )
+          ).toBe(true);
+
+          // Test finished.  Check wallet balances
+          // Get final balance of uluna on Terra
+          const finalTerraBalance = await queryBalanceOnTerra(Asset);
+
+          // Get final balance of uusd on Terra
+          // const finalFeeBalance: number = await queryBalanceOnTerra(FeeAsset);
+          expect(initialTerraBalance - 1e6 === finalTerraBalance).toBe(true);
+          const lunaBalOnEthAfter = await token.balanceOf(
+            await signer.getAddress()
+          );
+          const lunaBalOnEthAfterInt = parseInt(lunaBalOnEthAfter._hex);
+          expect(initialLunaBalOnEthInt + 1e6 === lunaBalOnEthAfterInt).toBe(
+            true
+          );
+        } catch (e) {
+          console.error("Terra to Ethereum failure: ", e);
+          done("Terra to Ethereum Failure");
+          return;
+        }
+        done();
+      })();
+    });
+    test("Transfer wrapped luna back to Terra", (done) => {
+      (async () => {
+        try {
+          // Get initial wallet balances
+          const lcd = new LCDClient({
+            URL: TERRA_NODE_URL,
+            chainID: TERRA_CHAIN_ID,
+            isClassic: true,
+          });
+          const mk = new MnemonicKey({
+            mnemonic: TERRA_PRIVATE_KEY,
+          });
+          const Asset: string = "uluna";
+          const initialTerraBalance: number = await queryBalanceOnTerra(Asset);
+          const provider = new ethers.providers.WebSocketProvider(
+            ETH_NODE_URL
+          ) as any;
+          const signer = new ethers.Wallet(ETH_PRIVATE_KEY4, provider);
+          const originAssetHex = tryNativeToHexString(Asset, CHAIN_ID_TERRA);
+          if (!originAssetHex) {
+            throw new Error("originAssetHex is null");
+          }
+          const foreignAsset = await getForeignAssetEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            provider,
+            CHAIN_ID_TERRA,
+            hexToUint8Array(originAssetHex)
+          );
+          if (!foreignAsset) {
+            throw new Error("foreignAsset is null");
+          }
+          let token = TokenImplementation__factory.connect(
+            foreignAsset,
+            signer
+          );
+          const initialLunaBalOnEth = await token.balanceOf(
+            await signer.getAddress()
+          );
+          const initialLunaBalOnEthInt = parseInt(initialLunaBalOnEth._hex);
+          const Amount: string = "1000000";
+
+          // approve the bridge to spend tokens
+          await approveEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            foreignAsset,
+            signer,
+            Amount
+          );
+
+          // transfer wrapped luna from Ethereum to Terra
+          const wallet = lcd.wallet(mk);
+          const receipt = await transferFromEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            signer,
+            foreignAsset,
+            Amount,
+            CHAIN_ID_TERRA,
+            tryNativeToUint8Array(wallet.key.accAddress, CHAIN_ID_TERRA)
+          );
+
+          // get the sequence from the logs (needed to fetch the vaa)
+          const sequence = parseSequenceFromLogEth(
+            receipt,
+            CONTRACTS.DEVNET.ethereum.core
+          );
+          const emitterAddress = getEmitterAddressEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge
+          );
+
+          // poll until the guardian(s) witness and sign the vaa
+          const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
+            WORMHOLE_RPC_HOSTS,
+            CHAIN_ID_ETH,
+            emitterAddress,
+            sequence,
+            {
+              transport: NodeHttpTransport(),
+            }
+          );
+          const msg = await redeemOnTerra(
+            CONTRACTS.DEVNET.terra.token_bridge,
+            wallet.key.accAddress,
+            signedVAA
+          );
+          const gasPrices = await getTerraGasPrices();
+          const feeEstimate = await lcd.tx.estimateFee(
+            [
+              {
+                sequenceNumber: await wallet.sequence(),
+                publicKey: wallet.key.publicKey,
+              },
+            ],
+            {
+              msgs: [msg],
+              memo: "localhost",
+              feeDenoms: ["uusd"],
+              gasPrices,
+            }
+          );
+          const tx = await wallet.createAndSignTx({
+            msgs: [msg],
+            memo: "localhost",
+            feeDenoms: ["uusd"],
+            gasPrices,
+            fee: feeEstimate,
+          });
+          await lcd.tx.broadcast(tx);
+          expect(
+            await getIsTransferCompletedTerra(
+              CONTRACTS.DEVNET.terra.token_bridge,
+              signedVAA,
+              lcd,
+              TERRA_GAS_PRICES_URL
+            )
+          ).toBe(true);
+
+          // Check wallet balances after
+          const finalTerraBalance = await queryBalanceOnTerra(Asset);
+          expect(initialTerraBalance + 1e6 === finalTerraBalance).toBe(true);
+          const finalLunaBalOnEth = await token.balanceOf(
+            await signer.getAddress()
+          );
+          const finalLunaBalOnEthInt = parseInt(finalLunaBalOnEth._hex);
+          expect(initialLunaBalOnEthInt - 1e6 === finalLunaBalOnEthInt).toBe(
+            true
+          );
+          // const uusdBal = await queryBalanceOnTerra("uusd");
+        } catch (e) {
+          console.error("Transfer back failure: ", e);
+          done("Transfer back Failure");
+          return;
+        }
+        done();
+      })();
+    });
+  });
+  describe("Terra <=> Ethereum roundtrip", () => {
+    test("Transfer CW20 token from Terra to Ethereum and back again", (done) => {
+      (async () => {
+        try {
+          const CW20: string = "terra13nkgqrfymug724h8pprpexqj9h629sa3ncw7sh";
+          const Asset: string = "uluna";
+          const FeeAsset: string = "uusd";
+          const Amount: string = "1000000";
+          const TerraWalletAddress: string =
+            "terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v";
+
+          interface Cw20Balance {
+            balance: string;
+          }
+
+          const lcd = new LCDClient({
+            URL: TERRA_NODE_URL,
+            chainID: TERRA_CHAIN_ID,
+            isClassic: true,
+          });
+          const mk = new MnemonicKey({
+            mnemonic: TERRA_PRIVATE_KEY,
+          });
+          const wallet = lcd.wallet(mk);
+
+          // This is the attestation phase of the CW20 token
+          let msg = await attestFromTerra(
+            CONTRACTS.DEVNET.terra.token_bridge,
+            TerraWalletAddress,
+            CW20
+          );
+          const gasPrices = await getTerraGasPrices();
+          let feeEstimate = await lcd.tx.estimateFee(
+            [
+              {
+                sequenceNumber: await wallet.sequence(),
+                publicKey: wallet.key.publicKey,
+              },
+            ],
+            {
+              msgs: [msg],
+              memo: "localhost",
+              feeDenoms: [FeeAsset],
+              gasPrices,
+            }
+          );
+          let executeTx = await wallet.createAndSignTx({
+            msgs: [msg],
+            memo: "Testing...",
+            feeDenoms: [FeeAsset],
+            gasPrices,
+            fee: feeEstimate,
+          });
+          let result = await lcd.tx.broadcast(executeTx);
+          let info = await waitForTerraExecution(result.txhash, lcd);
+          if (!info) {
+            throw new Error("info not found");
+          }
+          let sequence = parseSequenceFromLogTerra(info);
+          if (!sequence) {
+            throw new Error("Sequence not found");
+          }
+          let emitterAddress = await getEmitterAddressTerra(
+            CONTRACTS.DEVNET.terra.token_bridge
+          );
+          let signedVaa = await getSignedVAABySequence(
+            CHAIN_ID_TERRA,
+            sequence,
+            emitterAddress
+          );
+          const provider = new ethers.providers.WebSocketProvider(
+            ETH_NODE_URL
+          ) as any;
+          const signer = new ethers.Wallet(ETH_PRIVATE_KEY4, provider);
+          let success: boolean = true;
+          try {
+            const cr = await createWrappedOnEth(
+              CONTRACTS.DEVNET.ethereum.token_bridge,
+              signer,
+              signedVaa
+            );
+          } catch (e) {
+            success = false;
+          }
+          if (!success) {
+            const cr = await updateWrappedOnEth(
+              CONTRACTS.DEVNET.ethereum.token_bridge,
+              signer,
+              signedVaa
+            );
+            success = true;
+          }
+          // Attestation is complete
+
+          // Get initial balance of uusd on Terra
+          // const initialFeeBalance: number = await queryBalanceOnTerra(FeeAsset);
+
+          // Get wallet on eth
+          const originAssetHex = tryNativeToHexString(CW20, CHAIN_ID_TERRA);
+          if (!originAssetHex) {
+            throw new Error("originAssetHex is null");
+          }
+          const foreignAsset = await getForeignAssetEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            provider,
+            CHAIN_ID_TERRA,
+            hexToUint8Array(originAssetHex)
+          );
+          if (!foreignAsset) {
+            throw new Error("foreignAsset is null");
+          }
+          let token = TokenImplementation__factory.connect(
+            foreignAsset,
+            signer
+          );
+          const initialCW20BalOnEth = await token.balanceOf(
+            await signer.getAddress()
+          );
+          let initialCW20BalOnEthInt = parseInt(initialCW20BalOnEth._hex);
+
+          // Get initial balance of CW20 on Terra
+          const tokenDefinition: any = await lcd.wasm.contractQuery(CW20, {
+            token_info: {},
+          });
+          let cw20BalOnTerra: Cw20Balance = await lcd.wasm.contractQuery(CW20, {
+            balance: {
+              address: TerraWalletAddress,
+            },
+          });
+          let amount = ethers.utils.formatUnits(
+            cw20BalOnTerra.balance,
+            tokenDefinition.decimals
+          );
+          let initialCW20BalOnTerra: number = parseInt(amount);
+          const hexStr = tryNativeToHexString(
+            await signer.getAddress(),
+            CHAIN_ID_ETH
+          );
+          if (!hexStr) {
+            throw new Error("Failed to convert to hexStr");
+          }
+          const msgs = await transferFromTerra(
+            wallet.key.accAddress,
+            CONTRACTS.DEVNET.terra.token_bridge,
+            CW20,
+            Amount,
+            CHAIN_ID_ETH,
+            hexToUint8Array(hexStr) // This needs to be ETH wallet
+          );
+          feeEstimate = await lcd.tx.estimateFee(
+            [
+              {
+                sequenceNumber: await wallet.sequence(),
+                publicKey: wallet.key.publicKey,
+              },
+            ],
+            {
+              msgs: msgs,
+              memo: "localhost",
+              feeDenoms: [FeeAsset],
+              gasPrices,
+            }
+          );
+          executeTx = await wallet.createAndSignTx({
+            msgs: msgs,
+            memo: "Testing transfer...",
+            feeDenoms: [FeeAsset],
+            gasPrices,
+            fee: feeEstimate,
+          });
+          result = await lcd.tx.broadcast(executeTx);
+          info = await waitForTerraExecution(result.txhash, lcd);
+          if (!info) {
+            throw new Error("info not found");
+          }
+          sequence = parseSequenceFromLogTerra(info);
+          if (!sequence) {
+            throw new Error("Sequence not found");
+          }
+          emitterAddress = await getEmitterAddressTerra(
+            CONTRACTS.DEVNET.terra.token_bridge
+          );
+          signedVaa = await getSignedVAABySequence(
+            CHAIN_ID_TERRA,
+            sequence,
+            emitterAddress
+          );
+          const roe = await redeemOnEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            signer,
+            signedVaa
+          );
+          expect(
+            await getIsTransferCompletedEth(
+              CONTRACTS.DEVNET.ethereum.token_bridge,
+              provider,
+              signedVaa
+            )
+          ).toBe(true);
+
+          // Check the wallet balances
+          let finalCW20BalOnEth = await token.balanceOf(
+            await signer.getAddress()
+          );
+          let finalCW20BalOnEthInt = parseInt(finalCW20BalOnEth._hex);
+          expect(initialCW20BalOnEthInt + 1e6 === finalCW20BalOnEthInt).toBe(
+            true
+          );
+          cw20BalOnTerra = await lcd.wasm.contractQuery(CW20, {
+            balance: {
+              address: TerraWalletAddress,
+            },
+          });
+          amount = ethers.utils.formatUnits(
+            cw20BalOnTerra.balance,
+            tokenDefinition.decimals
+          );
+          let finalCW20BalOnTerra: number = parseInt(amount);
+          expect(initialCW20BalOnTerra - finalCW20BalOnTerra === 1).toBe(true);
+          // Done checking wallet balances
+
+          // Start the reverse transfer from Ethereum back to Terra
+          // Get initial wallet balances
+          initialCW20BalOnTerra = finalCW20BalOnTerra;
+          initialCW20BalOnEthInt = finalCW20BalOnEthInt;
+
+          // approve the bridge to spend tokens
+          await approveEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            foreignAsset,
+            signer,
+            Amount
+          );
+
+          // transfer token from Ethereum to Terra
+          const receipt = await transferFromEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge,
+            signer,
+            foreignAsset,
+            Amount,
+            CHAIN_ID_TERRA,
+            tryNativeToUint8Array(wallet.key.accAddress, CHAIN_ID_TERRA)
+          );
+
+          // get the sequence from the logs (needed to fetch the vaa)
+          sequence = parseSequenceFromLogEth(
+            receipt,
+            CONTRACTS.DEVNET.ethereum.core
+          );
+          emitterAddress = getEmitterAddressEth(
+            CONTRACTS.DEVNET.ethereum.token_bridge
+          );
+
+          // poll until the guardian(s) witness and sign the vaa
+          const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
+            WORMHOLE_RPC_HOSTS,
+            CHAIN_ID_ETH,
+            emitterAddress,
+            sequence,
+            {
+              transport: NodeHttpTransport(),
+            }
+          );
+          msg = await redeemOnTerra(
+            CONTRACTS.DEVNET.terra.token_bridge,
+            wallet.key.accAddress,
+            signedVAA
+          );
+          feeEstimate = await lcd.tx.estimateFee(
+            [
+              {
+                sequenceNumber: await wallet.sequence(),
+                publicKey: wallet.key.publicKey,
+              },
+            ],
+            {
+              msgs: [msg],
+              memo: "localhost",
+              feeDenoms: ["uusd"],
+              gasPrices,
+            }
+          );
+          const tx = await wallet.createAndSignTx({
+            msgs: [msg],
+            memo: "localhost",
+            feeDenoms: ["uusd"],
+            gasPrices,
+            fee: feeEstimate,
+          });
+          await lcd.tx.broadcast(tx);
+          expect(
+            await getIsTransferCompletedTerra(
+              CONTRACTS.DEVNET.terra.token_bridge,
+              signedVAA,
+              lcd,
+              TERRA_GAS_PRICES_URL
+            )
+          ).toBe(true);
+
+          // Check wallet balances after transfer back
+          finalCW20BalOnEth = await token.balanceOf(await signer.getAddress());
+          finalCW20BalOnEthInt = parseInt(finalCW20BalOnEth._hex);
+          expect(initialCW20BalOnEthInt - 1e6 === finalCW20BalOnEthInt).toBe(
+            true
+          );
+          cw20BalOnTerra = await lcd.wasm.contractQuery(CW20, {
+            balance: {
+              address: TerraWalletAddress,
+            },
+          });
+          amount = ethers.utils.formatUnits(
+            cw20BalOnTerra.balance,
+            tokenDefinition.decimals
+          );
+          finalCW20BalOnTerra = parseInt(amount);
+          expect(finalCW20BalOnTerra - initialCW20BalOnTerra === 1).toBe(true);
+          // Done checking wallet balances
+        } catch (e) {
+          console.error("CW20 Transfer failure: ", e);
+          done("CW20 Transfer Failure");
+          return;
+        }
+        done();
+      })();
+    });
+  });
+});

+ 32 - 18
sdk/js/src/token_bridge/__tests__/terra2-integration.ts

@@ -21,16 +21,13 @@ import {
   updateWrappedOnEth,
 } from "../..";
 import { tryNativeToUint8Array } from "../../utils";
-import { CHAIN_ID_ETH, CHAIN_ID_TERRA2 } from "../../utils/consts";
+import { CHAIN_ID_ETH, CHAIN_ID_TERRA2, CONTRACTS } from "../../utils/consts";
 import { attestFromEth, attestFromTerra } from "../attest";
 import { approveEth, transferFromEth, transferFromTerra } from "../transfer";
 import {
-  ETH_CORE_BRIDGE_ADDRESS,
   ETH_NODE_URL,
   ETH_PRIVATE_KEY2,
-  ETH_TOKEN_BRIDGE_ADDRESS,
   TERRA2_GAS_PRICES_URL,
-  TERRA2_TOKEN_BRIDGE_ADDRESS,
   TERRA2_PRIVATE_KEY,
   TEST_ERC20,
 } from "./consts";
@@ -47,7 +44,9 @@ const terraWalletAddress = terraWallet.key.accAddress;
 
 const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
 const signer = new ethers.Wallet(ETH_PRIVATE_KEY2, provider);
-const ethEmitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
+const ethEmitterAddress = getEmitterAddressEth(
+  CONTRACTS.DEVNET.ethereum.token_bridge
+);
 const ethTransferAmount = parseUnits("1", 18);
 
 let ethWalletAddress: string;
@@ -56,7 +55,7 @@ let terraEmitterAddress: string;
 beforeAll(async () => {
   ethWalletAddress = await signer.getAddress();
   terraEmitterAddress = await getEmitterAddressTerra(
-    TERRA2_TOKEN_BRIDGE_ADDRESS
+    CONTRACTS.DEVNET.terra2.token_bridge
   );
 });
 
@@ -96,7 +95,10 @@ const terraBroadcastTxAndGetSignedVaa = async (msgs: Msg[], wallet: Wallet) => {
 };
 
 const ethParseLogAndGetSignedVaa = async (receipt: ethers.ContractReceipt) => {
-  const sequence = parseSequenceFromLogEth(receipt, ETH_CORE_BRIDGE_ADDRESS);
+  const sequence = parseSequenceFromLogEth(
+    receipt,
+    CONTRACTS.DEVNET.ethereum.core
+  );
   return await getSignedVAABySequence(
     CHAIN_ID_ETH,
     sequence,
@@ -107,7 +109,7 @@ const ethParseLogAndGetSignedVaa = async (receipt: ethers.ContractReceipt) => {
 test("Attest and transfer token from Terra2 to Ethereum", async () => {
   // Attest
   const attestMsg = await attestFromTerra(
-    TERRA2_TOKEN_BRIDGE_ADDRESS,
+    CONTRACTS.DEVNET.terra2.token_bridge,
     terraWalletAddress,
     "uluna"
   );
@@ -116,14 +118,22 @@ test("Attest and transfer token from Terra2 to Ethereum", async () => {
     terraWallet
   );
   try {
-    await createWrappedOnEth(ETH_TOKEN_BRIDGE_ADDRESS, signer, attestSignedVaa);
+    await createWrappedOnEth(
+      CONTRACTS.DEVNET.ethereum.token_bridge,
+      signer,
+      attestSignedVaa
+    );
   } catch {
-    await updateWrappedOnEth(ETH_TOKEN_BRIDGE_ADDRESS, signer, attestSignedVaa);
+    await updateWrappedOnEth(
+      CONTRACTS.DEVNET.ethereum.token_bridge,
+      signer,
+      attestSignedVaa
+    );
   }
   // Transfer
   const transferMsgs = await transferFromTerra(
     terraWalletAddress,
-    TERRA2_TOKEN_BRIDGE_ADDRESS,
+    CONTRACTS.DEVNET.terra2.token_bridge,
     "uluna",
     "1000000",
     CHAIN_ID_ETH,
@@ -133,32 +143,36 @@ test("Attest and transfer token from Terra2 to Ethereum", async () => {
     transferMsgs,
     terraWallet
   );
-  await redeemOnEth(ETH_TOKEN_BRIDGE_ADDRESS, signer, transferSignedVaa);
+  await redeemOnEth(
+    CONTRACTS.DEVNET.ethereum.token_bridge,
+    signer,
+    transferSignedVaa
+  );
 });
 
 test("Attest and transfer token from Ethereum to Terra2", async () => {
   // Attest
   const attestReceipt = await attestFromEth(
-    ETH_TOKEN_BRIDGE_ADDRESS,
+    CONTRACTS.DEVNET.ethereum.token_bridge,
     signer,
     TEST_ERC20
   );
   const attestSignedVaa = await ethParseLogAndGetSignedVaa(attestReceipt);
   const createWrappedMsg = await createWrappedOnTerra(
-    TERRA2_TOKEN_BRIDGE_ADDRESS,
+    CONTRACTS.DEVNET.terra2.token_bridge,
     terraWalletAddress,
     attestSignedVaa
   );
   await terraBroadcastAndWaitForExecution([createWrappedMsg], terraWallet);
   // Transfer
   await approveEth(
-    ETH_TOKEN_BRIDGE_ADDRESS,
+    CONTRACTS.DEVNET.ethereum.token_bridge,
     TEST_ERC20,
     signer,
     ethTransferAmount
   );
   const transferReceipt = await transferFromEth(
-    ETH_TOKEN_BRIDGE_ADDRESS,
+    CONTRACTS.DEVNET.ethereum.token_bridge,
     signer,
     TEST_ERC20,
     ethTransferAmount,
@@ -167,14 +181,14 @@ test("Attest and transfer token from Ethereum to Terra2", async () => {
   );
   const transferSignedVaa = await ethParseLogAndGetSignedVaa(transferReceipt);
   const redeemMsg = await redeemOnTerra(
-    TERRA2_TOKEN_BRIDGE_ADDRESS,
+    CONTRACTS.DEVNET.terra2.token_bridge,
     terraWalletAddress,
     transferSignedVaa
   );
   await terraBroadcastAndWaitForExecution([redeemMsg], terraWallet);
   expect(
     await getIsTransferCompletedTerra(
-      TERRA2_TOKEN_BRIDGE_ADDRESS,
+      CONTRACTS.DEVNET.terra2.token_bridge,
       transferSignedVaa,
       lcd,
       TERRA2_GAS_PRICES_URL

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

@@ -358,7 +358,7 @@ const DEVNET = {
   terra: {
     core: "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5",
     token_bridge: "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4",
-    nft_bridge: undefined,
+    nft_bridge: "terra1plju286nnfj3z54wgcggd4enwaa9fgf5kgrgzl",
   },
   ethereum: {
     core: "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550",
@@ -456,8 +456,9 @@ const DEVNET = {
     nft_bridge: undefined,
   },
   terra2: {
-    core: undefined,
-    token_bridge: undefined,
+    core: "terra14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9ssrc8au",
+    token_bridge:
+      "terra1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrquka9l6",
     nft_bridge: undefined,
   },
   arbitrum: {

+ 1 - 1
sdk/js/tsconfig.json

@@ -12,5 +12,5 @@
     "lib": ["dom", "es5", "scripthost", "es2020.bigint"]
   },
   "include": ["src", "types"],
-  "exclude": ["node_modules", "**/__tests__/*"]
+  "exclude": ["node_modules", "**/__tests__/*", "**/*.test.ts"]
 }