sdk/js: parallelize tests

This commit is contained in:
Evan Gray 2022-07-16 03:47:52 +00:00 committed by Evan Gray
parent c5a7d3e517
commit 577d9b35f8
13 changed files with 3202 additions and 3482 deletions

View File

@ -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();

View File

@ -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";

View File

@ -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);
// 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);
// 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");
}
// let ownerEth = await erc721.ownerOf(10);
// expect(ownerEth).not.toBe(signer.address);
// 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"
);
// // TODO: check wrapped balance
const ownerInfo: any = await lcd.wasm.contractQuery(terra_addr, {
owner_of: { token_id: "10" },
});
expect(ownerInfo.owner).toBe(terraWallet.key.accAddress);
// // 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);
let ownerEth = await erc721.ownerOf(10);
expect(ownerEth).not.toBe(signer.address);
// // ensure that the transaction roundtrips back to the original native asset
// ownerEth = await erc721.ownerOf(10);
// expect(ownerEth).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);
// // TODO: the wrapped token should no longer exist
// 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) => {
// 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<61>");
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;
}

File diff suppressed because it is too large Load Diff

View File

@ -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 =

View File

@ -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"
);
}
})();
});
});
});

View File

@ -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 { ChainId, getSignedVAAWithRetry } from "../..";
import {
approveEth,
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
getForeignAssetSolana,
getSignedVAAWithRetry,
hexToUint8Array,
nativeToHexString,
parseSequenceFromLogEth,
transferFromEth,
} 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

File diff suppressed because it is too large Load Diff

View File

@ -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");
}
})();
});
});

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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: {

View File

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