wormhole/sdk/js/src/nft_bridge/__tests__/integration.ts

392 lines
11 KiB
TypeScript

import axios from "axios";
import Web3 from "web3";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
import {
afterEach,
beforeEach,
describe,
expect,
jest,
test,
} from "@jest/globals";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
getAssociatedTokenAddress,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { BigNumber, BigNumberish, ethers } from "ethers";
import {
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_TERRA,
CONTRACTS,
getEmitterAddressEth,
getEmitterAddressTerra,
parseSequenceFromLogEth,
parseSequenceFromLogTerra,
nft_bridge,
parseSequenceFromLogSolana,
getEmitterAddressSolana,
CHAIN_ID_SOLANA,
} from "../..";
import getSignedVAAWithRetry from "../../rpc/getSignedVAAWithRetry";
import {
ETH_NODE_URL,
ETH_PRIVATE_KEY,
TERRA_GAS_PRICES_URL,
WORMHOLE_RPC_HOSTS,
TERRA_CW721_CODE_ID,
TERRA_NODE_URL,
TERRA_CHAIN_ID,
TERRA_PRIVATE_KEY,
SOLANA_PRIVATE_KEY,
TEST_SOLANA_TOKEN,
SOLANA_HOST,
} from "./consts";
import {
NFTImplementation,
NFTImplementation__factory,
} from "../../ethers-contracts";
import sha3 from "js-sha3";
import {
Connection,
Keypair,
PublicKey,
TransactionResponse,
} from "@solana/web3.js";
import { postVaaSolanaWithRetry } from "../../solana";
import { tryNativeToUint8Array } from "../../utils";
import { arrayify } from "ethers/lib/utils";
import { parseVaa } from "../../vaa/wormhole";
import { parseNftTransferVaa } from "../../vaa";
const ERC721 = require("@openzeppelin/contracts/build/contracts/ERC721PresetMinterPauserAutoId.json");
jest.setTimeout(60000);
type Address = string;
// ethereum setup
const web3 = new Web3(ETH_NODE_URL);
let provider: ethers.providers.WebSocketProvider;
let signer: ethers.Wallet;
// solana setup
const connection = new Connection(SOLANA_HOST, "confirmed");
const keypair = Keypair.fromSecretKey(SOLANA_PRIVATE_KEY);
const payerAddress = keypair.publicKey.toString();
beforeEach(() => {
provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider); // corresponds to accounts[1]
});
afterEach(() => {
provider.destroy();
});
describe("Integration Tests", () => {
// 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 fromAddress = await getAssociatedTokenAddress(
new PublicKey(TEST_SOLANA_TOKEN),
keypair.publicKey
);
// send to eth
const transaction1 = await _transferFromSolana(
fromAddress,
TEST_SOLANA_TOKEN,
signer.address,
CHAIN_ID_ETH
);
let signedVAA = await waitUntilSolanaTxObserved(transaction1);
// we get the solana token id from the VAA
const { tokenId } = parseNftTransferVaa(signedVAA);
await _redeemOnEth(signedVAA);
const eth_addr = await nft_bridge.getForeignAssetEth(
CONTRACTS.DEVNET.ethereum.nft_bridge,
provider,
CHAIN_ID_SOLANA,
tryNativeToUint8Array(TEST_SOLANA_TOKEN, CHAIN_ID_SOLANA)
);
if (!eth_addr) {
throw new Error("Ethereum address is null");
}
const transaction3 = await _transferFromEth(
eth_addr,
tokenId,
fromAddress.toString(),
CHAIN_ID_SOLANA
);
signedVAA = await waitUntilEthTxObserved(transaction3);
const { name, symbol } = parseNftTransferVaa(signedVAA);
// if the names match up here, it means all the spl caches work
expect(name).toBe("Not a PUNK🎸");
expect(symbol).toBe("PUNK🎸");
await _redeemOnSolana(signedVAA);
done();
} catch (e) {
console.error(e);
done(
`An error occured while trying to transfer from Solana to Ethereum: ${e}`
);
}
})();
});
});
////////////////////////////////////////////////////////////////////////////////
// Utils
async function deployNFTOnEth(
name: string,
symbol: string,
uri: string,
how_many: number
): Promise<NFTImplementation> {
const accounts = await web3.eth.getAccounts();
const nftContract = new web3.eth.Contract(ERC721.abi);
let nft = await nftContract
.deploy({
data: ERC721.bytecode,
arguments: [name, symbol, uri],
})
.send({
from: accounts[1],
gas: 5000000,
});
// The eth contracts mints tokens with sequential ids, so in order to get to a
// specific id, we need to mint multiple nfts. We need this to test that
// foreign ids on terra get converted to the decimal stringified form of the
// original id.
for (var i = 0; i < how_many; i++) {
await nft.methods.mint(accounts[1]).send({
from: accounts[1],
gas: 1000000,
});
}
return NFTImplementation__factory.connect(nft.options.address, signer);
}
async function waitUntilEthTxObserved(
receipt: ethers.ContractReceipt
): Promise<Uint8Array> {
// get the sequence from the logs (needed to fetch the vaa)
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,
CHAIN_ID_ETH,
emitterAddress,
sequence,
{
transport: NodeHttpTransport(),
}
);
return signedVAA;
}
async function waitUntilSolanaTxObserved(
response: TransactionResponse
): Promise<Uint8Array> {
// get the sequence from the logs (needed to fetch the vaa)
const sequence = parseSequenceFromLogSolana(response);
const emitterAddress = await getEmitterAddressSolana(
CONTRACTS.DEVNET.solana.nft_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(),
}
);
return signedVAA;
}
async function expectReceivedOnEth(signedVAA: Uint8Array) {
return expect(
await nft_bridge.getIsTransferCompletedEth(
CONTRACTS.DEVNET.ethereum.nft_bridge,
provider,
signedVAA
)
);
}
async function _transferFromEth(
erc721: string,
token_id: BigNumberish,
address: string,
chain: ChainId
): Promise<ethers.ContractReceipt> {
return nft_bridge.transferFromEth(
CONTRACTS.DEVNET.ethereum.nft_bridge,
signer,
erc721,
token_id,
chain,
tryNativeToUint8Array(address, chain)
);
}
async function _transferFromSolana(
fromAddress: PublicKey,
tokenAddress: string,
targetAddress: string,
chain: ChainId,
originAddress?: Uint8Array,
originChain?: ChainId,
originTokenId?: Uint8Array
): Promise<TransactionResponse> {
const transaction = await nft_bridge.transferFromSolana(
connection,
CONTRACTS.DEVNET.solana.core,
CONTRACTS.DEVNET.solana.nft_bridge,
payerAddress,
fromAddress.toString(),
tokenAddress,
tryNativeToUint8Array(targetAddress, chain),
chain,
originAddress,
originChain,
originTokenId
);
// 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");
}
return info;
}
async function _redeemOnEth(
signedVAA: Uint8Array
): Promise<ethers.ContractReceipt> {
return nft_bridge.redeemOnEth(
CONTRACTS.DEVNET.ethereum.nft_bridge,
signer,
signedVAA
);
}
async function _redeemOnSolana(signedVAA: Uint8Array) {
const maxRetries = 5;
await postVaaSolanaWithRetry(
connection,
async (transaction) => {
transaction.partialSign(keypair);
return transaction;
},
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;
}