parent
7e212fa739
commit
cb7e90a701
|
@ -5,6 +5,8 @@
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
added parseSequencesFromLog\*
|
added parseSequencesFromLog\*
|
||||||
|
Terra NFT token bridge
|
||||||
|
getIsTransferCompleted on NFT bridge
|
||||||
|
|
||||||
## 0.1.5
|
## 0.1.5
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -52,7 +52,8 @@
|
||||||
"ts-jest": "^27.0.7",
|
"ts-jest": "^27.0.7",
|
||||||
"tslint": "^6.1.3",
|
"tslint": "^6.1.3",
|
||||||
"tslint-config-prettier": "^1.18.0",
|
"tslint-config-prettier": "^1.18.0",
|
||||||
"typescript": "^4.3.5"
|
"typescript": "^4.3.5",
|
||||||
|
"web3": "^1.6.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@improbable-eng/grpc-web": "^0.14.0",
|
"@improbable-eng/grpc-web": "^0.14.0",
|
||||||
|
|
|
@ -5,3 +5,12 @@ export * from "./rpc";
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
export * from "./bridge";
|
export * from "./bridge";
|
||||||
export * from "./token_bridge";
|
export * from "./token_bridge";
|
||||||
|
|
||||||
|
export * as ethers_contracts from "./ethers-contracts";
|
||||||
|
export * as solana from "./solana";
|
||||||
|
export * as terra from "./terra";
|
||||||
|
export * as rpc from "./rpc";
|
||||||
|
export * as utils from "./utils";
|
||||||
|
export * as bridge from "./bridge";
|
||||||
|
export * as token_bridge from "./token_bridge";
|
||||||
|
export * as nft_bridge from "./nft_bridge";
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { describe, expect, it } from "@jest/globals";
|
||||||
|
import { Connection, PublicKey } from "@solana/web3.js";
|
||||||
|
|
||||||
|
// see devnet.md
|
||||||
|
export const ETH_NODE_URL = "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 = "http://localhost:8899";
|
||||||
|
export const SOLANA_PRIVATE_KEY = new Uint8Array([
|
||||||
|
14, 173, 153, 4, 176, 224, 201, 111, 32, 237, 183, 185, 159, 247, 22, 161, 89,
|
||||||
|
84, 215, 209, 212, 137, 10, 92, 157, 49, 29, 192, 101, 164, 152, 70, 87, 65,
|
||||||
|
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 = "http://localhost:1317";
|
||||||
|
export const TERRA_CHAIN_ID = "localterra";
|
||||||
|
export const TERRA_GAS_PRICES_URL = "http://localhost:3060/v1/txs/gas_prices";
|
||||||
|
export const TERRA_CORE_BRIDGE_ADDRESS =
|
||||||
|
"terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5";
|
||||||
|
export const TERRA_NFT_BRIDGE_ADDRESS =
|
||||||
|
"terra19zpyd046u4swqpksr3n44cej4j8pg6ah2y6dcg";
|
||||||
|
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";
|
||||||
|
export const TERRA_CW721_CODE_ID = 8;
|
||||||
|
export const TEST_CW721 = "terra1l425neayde0fzfcv3apkyk4zqagvflm6cmha9v";
|
||||||
|
export const TEST_SOLANA_TOKEN = "BVxyYhm498L79r4HMQ9sxZ5bi41DmJmeWZ7SCS7Cyvna";
|
||||||
|
export const WORMHOLE_RPC_HOSTS = ["http://localhost:7071"];
|
||||||
|
|
||||||
|
describe("consts should exist", () => {
|
||||||
|
it("has Solana test token", () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||||
|
return expect(
|
||||||
|
connection.getAccountInfo(new PublicKey(TEST_SOLANA_TOKEN))
|
||||||
|
).resolves.toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,604 @@
|
||||||
|
import { BlockTxBroadcastResult, Coins, LCDClient, MnemonicKey, Msg, MsgExecuteContract, StdFee, TxInfo, Wallet } 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 {
|
||||||
|
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 {
|
||||||
|
ChainId,
|
||||||
|
CHAIN_ID_ETH,
|
||||||
|
CHAIN_ID_TERRA,
|
||||||
|
getEmitterAddressEth,
|
||||||
|
getEmitterAddressTerra,
|
||||||
|
hexToUint8Array,
|
||||||
|
nativeToHexString,
|
||||||
|
parseSequenceFromLogEth,
|
||||||
|
parseSequenceFromLogTerra,
|
||||||
|
nft_bridge,
|
||||||
|
parseSequenceFromLogSolana,
|
||||||
|
getEmitterAddressSolana,
|
||||||
|
CHAIN_ID_SOLANA,
|
||||||
|
parseNFTPayload
|
||||||
|
} from "../..";
|
||||||
|
import getSignedVAAWithRetry from "../../rpc/getSignedVAAWithRetry";
|
||||||
|
import { importCoreWasm, importNftWasm, 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,
|
||||||
|
TERRA_CHAIN_ID,
|
||||||
|
TERRA_PRIVATE_KEY,
|
||||||
|
SOLANA_PRIVATE_KEY,
|
||||||
|
TEST_SOLANA_TOKEN,
|
||||||
|
SOLANA_HOST,
|
||||||
|
SOLANA_CORE_BRIDGE_ADDRESS,
|
||||||
|
SOLANA_NFT_BRIDGE_ADDRESS,
|
||||||
|
} 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";
|
||||||
|
const ERC721 = require("@openzeppelin/contracts/build/contracts/ERC721PresetMinterPauserAutoId.json");
|
||||||
|
|
||||||
|
setDefaultWasm("node");
|
||||||
|
|
||||||
|
jest.setTimeout(60000);
|
||||||
|
|
||||||
|
type Address = string;
|
||||||
|
|
||||||
|
// terra setup
|
||||||
|
const lcd = new LCDClient({
|
||||||
|
URL: TERRA_NODE_URL,
|
||||||
|
chainID: TERRA_CHAIN_ID,
|
||||||
|
});
|
||||||
|
const terraWallet: Wallet = lcd.wallet(new MnemonicKey({
|
||||||
|
mnemonic: TERRA_PRIVATE_KEY,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 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", () => {
|
||||||
|
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,
|
||||||
|
hexToUint8Array(
|
||||||
|
nativeToHexString(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,
|
||||||
|
hexToUint8Array(
|
||||||
|
nativeToHexString(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 Etheretum to Solana", (done) => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const { parse_vaa } = await importCoreWasm();
|
||||||
|
|
||||||
|
const fromAddress = (
|
||||||
|
await Token.getAssociatedTokenAddress(
|
||||||
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
new PublicKey(TEST_SOLANA_TOKEN),
|
||||||
|
keypair.publicKey
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// send to terra
|
||||||
|
const transaction1 = await _transferFromSolana(fromAddress, terraWallet.key.accAddress, CHAIN_ID_TERRA);
|
||||||
|
let signedVAA = await waitUntilSolanaTxObserved(transaction1);
|
||||||
|
|
||||||
|
// we get the solana token id from the VAA
|
||||||
|
const { tokenId } = parseNFTPayload(
|
||||||
|
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,
|
||||||
|
hexToUint8Array(
|
||||||
|
nativeToHexString(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, provider, CHAIN_ID_SOLANA,
|
||||||
|
hexToUint8Array(
|
||||||
|
nativeToHexString(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 } = parseNFTPayload(
|
||||||
|
Buffer.from(new Uint8Array(parse_vaa(signedVAA).payload))
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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}`);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
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,
|
||||||
|
hexToUint8Array(
|
||||||
|
nativeToHexString(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();
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// 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 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<StdFee> {
|
||||||
|
const feeEstimate = await lcd.tx.estimateFee(
|
||||||
|
terraWallet.key.accAddress,
|
||||||
|
msgs,
|
||||||
|
{
|
||||||
|
memo: "localhost",
|
||||||
|
feeDenoms: ["uluna"],
|
||||||
|
gasPrices,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return feeEstimate;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitUntilTerraTxObserved(txresult: BlockTxBroadcastResult): 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);
|
||||||
|
// 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(
|
||||||
|
SOLANA_NFT_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(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
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 StdFee(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 (info.code !== undefined) {
|
||||||
|
// 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,
|
||||||
|
terraWallet.key.accAddress,
|
||||||
|
lcd,
|
||||||
|
TERRA_GAS_PRICES_URL
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectReceivedOnEth(signedVAA: Uint8Array) {
|
||||||
|
return expect(
|
||||||
|
await nft_bridge.getIsTransferCompletedEth(
|
||||||
|
ETH_NFT_BRIDGE_ADDRESS,
|
||||||
|
provider,
|
||||||
|
signedVAA,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _transferFromEth(erc721: string, token_id: BigNumberish, address: string, chain: ChainId): Promise<ethers.ContractReceipt> {
|
||||||
|
return nft_bridge.transferFromEth(
|
||||||
|
ETH_NFT_BRIDGE_ADDRESS,
|
||||||
|
signer,
|
||||||
|
erc721,
|
||||||
|
token_id,
|
||||||
|
chain,
|
||||||
|
hexToUint8Array(
|
||||||
|
nativeToHexString(address, chain) || ""
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _transferFromTerra(terra_addr: string, token_id: string, address: string, chain: ChainId): Promise<BlockTxBroadcastResult> {
|
||||||
|
const gasPrices = await getGasPrices();
|
||||||
|
const msgs = await nft_bridge.transferFromTerra(
|
||||||
|
terraWallet.key.accAddress,
|
||||||
|
TERRA_NFT_BRIDGE_ADDRESS,
|
||||||
|
terra_addr,
|
||||||
|
token_id,
|
||||||
|
chain,
|
||||||
|
hexToUint8Array(nativeToHexString(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, targetAddress: string, chain: ChainId): Promise<TransactionResponse> {
|
||||||
|
const transaction = await nft_bridge.transferFromSolana(
|
||||||
|
connection,
|
||||||
|
SOLANA_CORE_BRIDGE_ADDRESS,
|
||||||
|
SOLANA_NFT_BRIDGE_ADDRESS,
|
||||||
|
payerAddress,
|
||||||
|
fromAddress.toString(),
|
||||||
|
TEST_SOLANA_TOKEN,
|
||||||
|
hexToUint8Array(
|
||||||
|
nativeToHexString(targetAddress, chain) || ""
|
||||||
|
),
|
||||||
|
chain
|
||||||
|
);
|
||||||
|
// 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(
|
||||||
|
ETH_NFT_BRIDGE_ADDRESS,
|
||||||
|
signer,
|
||||||
|
signedVAA
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _redeemOnTerra(signedVAA: Uint8Array): Promise<BlockTxBroadcastResult> {
|
||||||
|
const msg = await nft_bridge.redeemOnTerra(
|
||||||
|
TERRA_NFT_BRIDGE_ADDRESS,
|
||||||
|
terraWallet.key.accAddress,
|
||||||
|
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) {
|
||||||
|
const maxRetries = 5;
|
||||||
|
await postVaaSolanaWithRetry(
|
||||||
|
connection,
|
||||||
|
async (transaction) => {
|
||||||
|
transaction.partialSign(keypair);
|
||||||
|
return transaction;
|
||||||
|
},
|
||||||
|
SOLANA_CORE_BRIDGE_ADDRESS,
|
||||||
|
payerAddress,
|
||||||
|
Buffer.from(signedVAA),
|
||||||
|
maxRetries
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
import { PublicKey } from "@solana/web3.js";
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { LCDClient } from "@terra-money/terra.js";
|
||||||
import { ethers } from "ethers";
|
import { ethers } from "ethers";
|
||||||
|
import { fromUint8Array } from "js-base64";
|
||||||
import { CHAIN_ID_SOLANA } from "..";
|
import { CHAIN_ID_SOLANA } from "..";
|
||||||
import { NFTBridge__factory } from "../ethers-contracts";
|
import { NFTBridge__factory } from "../ethers-contracts";
|
||||||
import { importNftWasm } from "../solana/wasm";
|
import { importNftWasm } from "../solana/wasm";
|
||||||
|
@ -15,10 +17,10 @@ import { ChainId } from "../utils";
|
||||||
*/
|
*/
|
||||||
export async function getForeignAssetEth(
|
export async function getForeignAssetEth(
|
||||||
tokenBridgeAddress: string,
|
tokenBridgeAddress: string,
|
||||||
provider: ethers.providers.Web3Provider,
|
provider: ethers.Signer | ethers.providers.Provider,
|
||||||
originChain: ChainId,
|
originChain: ChainId,
|
||||||
originAsset: Uint8Array
|
originAsset: Uint8Array
|
||||||
) {
|
): Promise<string | null> {
|
||||||
const tokenBridge = NFTBridge__factory.connect(tokenBridgeAddress, provider);
|
const tokenBridge = NFTBridge__factory.connect(tokenBridgeAddress, provider);
|
||||||
try {
|
try {
|
||||||
if (originChain === CHAIN_ID_SOLANA) {
|
if (originChain === CHAIN_ID_SOLANA) {
|
||||||
|
@ -35,6 +37,40 @@ export async function getForeignAssetEth(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a foreign asset address on Terra for a provided native chain and asset address
|
||||||
|
* @param tokenBridgeAddress
|
||||||
|
* @param client
|
||||||
|
* @param originChain
|
||||||
|
* @param originAsset
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function getForeignAssetTerra(
|
||||||
|
tokenBridgeAddress: string,
|
||||||
|
client: LCDClient,
|
||||||
|
originChain: ChainId,
|
||||||
|
originAsset: Uint8Array,
|
||||||
|
): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const address =
|
||||||
|
originChain == CHAIN_ID_SOLANA ? "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=" : fromUint8Array(originAsset);
|
||||||
|
const result: { address: string } = await client.wasm.contractQuery(
|
||||||
|
tokenBridgeAddress,
|
||||||
|
{
|
||||||
|
wrapped_registry: {
|
||||||
|
chain: originChain,
|
||||||
|
address,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return result.address;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a foreign asset address on Solana for a provided native chain and asset address
|
* Returns a foreign asset address on Solana for a provided native chain and asset address
|
||||||
* @param tokenBridgeAddress
|
* @param tokenBridgeAddress
|
||||||
|
@ -47,7 +83,7 @@ export async function getForeignAssetSol(
|
||||||
originChain: ChainId,
|
originChain: ChainId,
|
||||||
originAsset: Uint8Array,
|
originAsset: Uint8Array,
|
||||||
tokenId: Uint8Array
|
tokenId: Uint8Array
|
||||||
) {
|
): Promise<string> {
|
||||||
const { wrapped_address } = await importNftWasm();
|
const { wrapped_address } = await importNftWasm();
|
||||||
const wrappedAddress = wrapped_address(
|
const wrappedAddress = wrapped_address(
|
||||||
tokenBridgeAddress,
|
tokenBridgeAddress,
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { ethers } from "ethers";
|
||||||
|
import { NFTBridge__factory } from "../ethers-contracts";
|
||||||
|
import { getSignedVAAHash } from "../bridge";
|
||||||
|
import { importCoreWasm } from "../solana/wasm";
|
||||||
|
import { Connection, PublicKey } from "@solana/web3.js";
|
||||||
|
import { LCDClient } from "@terra-money/terra.js";
|
||||||
|
import axios from "axios";
|
||||||
|
import { redeemOnTerra } from ".";
|
||||||
|
|
||||||
|
export async function getIsTransferCompletedEth(
|
||||||
|
nftBridgeAddress: string,
|
||||||
|
provider: ethers.providers.Provider,
|
||||||
|
signedVAA: Uint8Array
|
||||||
|
) {
|
||||||
|
const nftBridge = NFTBridge__factory.connect(nftBridgeAddress, provider);
|
||||||
|
const signedVAAHash = await getSignedVAAHash(signedVAA);
|
||||||
|
return await nftBridge.isTransferCompleted(signedVAAHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getIsTransferCompletedTerra(
|
||||||
|
nftBridgeAddress: string,
|
||||||
|
signedVAA: Uint8Array,
|
||||||
|
walletAddress: string,
|
||||||
|
client: LCDClient,
|
||||||
|
gasPriceUrl: string
|
||||||
|
) {
|
||||||
|
const msg = await redeemOnTerra(nftBridgeAddress, walletAddress, signedVAA);
|
||||||
|
// TODO: remove gasPriceUrl and just use the client's gas prices
|
||||||
|
const gasPrices = await axios.get(gasPriceUrl).then((result) => result.data);
|
||||||
|
try {
|
||||||
|
await client.tx.estimateFee(walletAddress, [msg], {
|
||||||
|
memo: "already redeemed calculation",
|
||||||
|
feeDenoms: ["uluna"],
|
||||||
|
gasPrices,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// redeemed if the VAA was already executed
|
||||||
|
return e.response.data.error.includes("VaaAlreadyExecuted");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getIsTransferCompletedSolana(
|
||||||
|
nftBridgeAddress: string,
|
||||||
|
signedVAA: Uint8Array,
|
||||||
|
connection: Connection
|
||||||
|
) {
|
||||||
|
const { claim_address } = await importCoreWasm();
|
||||||
|
const claimAddress = await claim_address(nftBridgeAddress, signedVAA);
|
||||||
|
const claimInfo = await connection.getAccountInfo(
|
||||||
|
new PublicKey(claimAddress),
|
||||||
|
"confirmed"
|
||||||
|
);
|
||||||
|
return !!claimInfo;
|
||||||
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
import { Connection, PublicKey } from "@solana/web3.js";
|
import { Connection, PublicKey } from "@solana/web3.js";
|
||||||
|
import { LCDClient } from "@terra-money/terra.js";
|
||||||
import { BigNumber, ethers } from "ethers";
|
import { BigNumber, ethers } from "ethers";
|
||||||
import { arrayify, zeroPad } from "ethers/lib/utils";
|
import { arrayify, zeroPad } from "ethers/lib/utils";
|
||||||
|
import { canonicalAddress, WormholeWrappedInfo } from "..";
|
||||||
import { TokenImplementation__factory } from "../ethers-contracts";
|
import { TokenImplementation__factory } from "../ethers-contracts";
|
||||||
import { importNftWasm } from "../solana/wasm";
|
import { importNftWasm } from "../solana/wasm";
|
||||||
import { ChainId, CHAIN_ID_SOLANA } from "../utils";
|
import { ChainId, CHAIN_ID_SOLANA, CHAIN_ID_TERRA } from "../utils";
|
||||||
import { getIsWrappedAssetEth } from "./getIsWrappedAsset";
|
import { getIsWrappedAssetEth } from "./getIsWrappedAsset";
|
||||||
|
|
||||||
export interface WormholeWrappedNFTInfo {
|
export interface WormholeWrappedNFTInfo {
|
||||||
|
@ -139,3 +141,32 @@ function bigToUint8Array(big: bigint) {
|
||||||
}
|
}
|
||||||
return u8;
|
return u8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getOriginalAssetTerra(
|
||||||
|
client: LCDClient,
|
||||||
|
wrappedAddress: string
|
||||||
|
): Promise<WormholeWrappedInfo> {
|
||||||
|
try {
|
||||||
|
const result: {
|
||||||
|
asset_address: string;
|
||||||
|
asset_chain: ChainId;
|
||||||
|
bridge: string;
|
||||||
|
} = await client.wasm.contractQuery(wrappedAddress, {
|
||||||
|
wrapped_asset_info: {},
|
||||||
|
});
|
||||||
|
if (result) {
|
||||||
|
return {
|
||||||
|
isWrapped: true,
|
||||||
|
chainId: result.asset_chain,
|
||||||
|
assetAddress: new Uint8Array(
|
||||||
|
Buffer.from(result.asset_address, "base64")
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
return {
|
||||||
|
isWrapped: false,
|
||||||
|
chainId: CHAIN_ID_TERRA,
|
||||||
|
assetAddress: zeroPad(canonicalAddress(wrappedAddress), 32),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export * from "./getForeignAsset";
|
export * from "./getForeignAsset";
|
||||||
export * from "./getIsWrappedAsset";
|
export * from "./getIsWrappedAsset";
|
||||||
|
export * from "./getIsTransferCompleted";
|
||||||
export * from "./getOriginalAsset";
|
export * from "./getOriginalAsset";
|
||||||
export * from "./redeem";
|
export * from "./redeem";
|
||||||
export * from "./transfer";
|
export * from "./transfer";
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
|
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
|
||||||
|
import { MsgExecuteContract } from "@terra-money/terra.js";
|
||||||
import { ethers } from "ethers";
|
import { ethers } from "ethers";
|
||||||
|
import { fromUint8Array } from "js-base64";
|
||||||
import { CHAIN_ID_SOLANA } from "..";
|
import { CHAIN_ID_SOLANA } from "..";
|
||||||
import { Bridge__factory } from "../ethers-contracts";
|
import { Bridge__factory } from "../ethers-contracts";
|
||||||
import { ixFromRust } from "../solana";
|
import { ixFromRust } from "../solana";
|
||||||
|
@ -9,14 +11,14 @@ export async function redeemOnEth(
|
||||||
tokenBridgeAddress: string,
|
tokenBridgeAddress: string,
|
||||||
signer: ethers.Signer,
|
signer: ethers.Signer,
|
||||||
signedVAA: Uint8Array
|
signedVAA: Uint8Array
|
||||||
) {
|
): Promise<ethers.ContractReceipt> {
|
||||||
const bridge = Bridge__factory.connect(tokenBridgeAddress, signer);
|
const bridge = Bridge__factory.connect(tokenBridgeAddress, signer);
|
||||||
const v = await bridge.completeTransfer(signedVAA);
|
const v = await bridge.completeTransfer(signedVAA);
|
||||||
const receipt = await v.wait();
|
const receipt = await v.wait();
|
||||||
return receipt;
|
return receipt;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function isNFTVAASolanaNative(signedVAA: Uint8Array) {
|
export async function isNFTVAASolanaNative(signedVAA: Uint8Array): Promise<boolean> {
|
||||||
const { parse_vaa } = await importCoreWasm();
|
const { parse_vaa } = await importCoreWasm();
|
||||||
const parsedVAA = parse_vaa(signedVAA);
|
const parsedVAA = parse_vaa(signedVAA);
|
||||||
const isSolanaNative =
|
const isSolanaNative =
|
||||||
|
@ -31,7 +33,7 @@ export async function redeemOnSolana(
|
||||||
tokenBridgeAddress: string,
|
tokenBridgeAddress: string,
|
||||||
payerAddress: string,
|
payerAddress: string,
|
||||||
signedVAA: Uint8Array
|
signedVAA: Uint8Array
|
||||||
) {
|
): Promise<Transaction> {
|
||||||
const isSolanaNative = await isNFTVAASolanaNative(signedVAA);
|
const isSolanaNative = await isNFTVAASolanaNative(signedVAA);
|
||||||
const { complete_transfer_wrapped_ix, complete_transfer_native_ix } =
|
const { complete_transfer_wrapped_ix, complete_transfer_native_ix } =
|
||||||
await importNftWasm();
|
await importNftWasm();
|
||||||
|
@ -74,7 +76,7 @@ export async function createMetaOnSolana(
|
||||||
tokenBridgeAddress: string,
|
tokenBridgeAddress: string,
|
||||||
payerAddress: string,
|
payerAddress: string,
|
||||||
signedVAA: Uint8Array
|
signedVAA: Uint8Array
|
||||||
) {
|
): Promise<Transaction> {
|
||||||
const { complete_transfer_wrapped_meta_ix } = await importNftWasm();
|
const { complete_transfer_wrapped_meta_ix } = await importNftWasm();
|
||||||
const ix = ixFromRust(
|
const ix = ixFromRust(
|
||||||
complete_transfer_wrapped_meta_ix(
|
complete_transfer_wrapped_meta_ix(
|
||||||
|
@ -90,3 +92,15 @@ export async function createMetaOnSolana(
|
||||||
transaction.feePayer = new PublicKey(payerAddress);
|
transaction.feePayer = new PublicKey(payerAddress);
|
||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function redeemOnTerra(
|
||||||
|
tokenBridgeAddress: string,
|
||||||
|
walletAddress: string,
|
||||||
|
signedVAA: Uint8Array
|
||||||
|
): Promise<MsgExecuteContract> {
|
||||||
|
return new MsgExecuteContract(walletAddress, tokenBridgeAddress, {
|
||||||
|
submit_vaa: {
|
||||||
|
data: fromUint8Array(signedVAA),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
||||||
import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js";
|
import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js";
|
||||||
|
import { MsgExecuteContract } from "@terra-money/terra.js";
|
||||||
import { ethers } from "ethers";
|
import { ethers } from "ethers";
|
||||||
import {
|
import {
|
||||||
NFTBridge__factory,
|
NFTBridge__factory,
|
||||||
|
@ -16,7 +17,7 @@ export async function transferFromEth(
|
||||||
tokenID: ethers.BigNumberish,
|
tokenID: ethers.BigNumberish,
|
||||||
recipientChain: ChainId,
|
recipientChain: ChainId,
|
||||||
recipientAddress: Uint8Array
|
recipientAddress: Uint8Array
|
||||||
) {
|
): Promise<ethers.ContractReceipt> {
|
||||||
//TODO: should we check if token attestation exists on the target chain
|
//TODO: should we check if token attestation exists on the target chain
|
||||||
const token = NFTImplementation__factory.connect(tokenAddress, signer);
|
const token = NFTImplementation__factory.connect(tokenAddress, signer);
|
||||||
await (await token.approve(tokenBridgeAddress, tokenID)).wait();
|
await (await token.approve(tokenBridgeAddress, tokenID)).wait();
|
||||||
|
@ -44,7 +45,7 @@ export async function transferFromSolana(
|
||||||
originAddress?: Uint8Array,
|
originAddress?: Uint8Array,
|
||||||
originChain?: ChainId,
|
originChain?: ChainId,
|
||||||
originTokenId?: Uint8Array
|
originTokenId?: Uint8Array
|
||||||
) {
|
): Promise<Transaction> {
|
||||||
const nonce = createNonce().readUInt32LE(0);
|
const nonce = createNonce().readUInt32LE(0);
|
||||||
const transferIx = await getBridgeFeeIx(
|
const transferIx = await getBridgeFeeIx(
|
||||||
connection,
|
connection,
|
||||||
|
@ -107,3 +108,41 @@ export async function transferFromSolana(
|
||||||
transaction.partialSign(messageKey);
|
transaction.partialSign(messageKey);
|
||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function transferFromTerra(
|
||||||
|
walletAddress: string,
|
||||||
|
tokenBridgeAddress: string,
|
||||||
|
tokenAddress: string,
|
||||||
|
tokenID: string,
|
||||||
|
recipientChain: ChainId,
|
||||||
|
recipientAddress: Uint8Array
|
||||||
|
): Promise<MsgExecuteContract[]> {
|
||||||
|
const nonce = Math.round(Math.random() * 100000);
|
||||||
|
return [
|
||||||
|
new MsgExecuteContract(
|
||||||
|
walletAddress,
|
||||||
|
tokenAddress,
|
||||||
|
{
|
||||||
|
approve: {
|
||||||
|
spender: tokenBridgeAddress,
|
||||||
|
token_id: tokenID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
new MsgExecuteContract(
|
||||||
|
walletAddress,
|
||||||
|
tokenBridgeAddress,
|
||||||
|
{
|
||||||
|
initiate_transfer: {
|
||||||
|
contract_addr: tokenAddress,
|
||||||
|
token_id: tokenID,
|
||||||
|
recipient_chain: recipientChain,
|
||||||
|
recipient: Buffer.from(recipientAddress).toString("base64"),
|
||||||
|
nonce: nonce,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue