sdk/js: Terra NFT bridge support (#699)

commit-id:5a5c9d29
This commit is contained in:
Csongor Kiss 2022-01-07 17:13:02 +01:00 committed by GitHub
parent 7e212fa739
commit cb7e90a701
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 6996 additions and 16 deletions

View File

@ -5,6 +5,8 @@
### Added
added parseSequencesFromLog\*
Terra NFT token bridge
getIsTransferCompleted on NFT bridge
## 0.1.5

6152
sdk/js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -52,7 +52,8 @@
"ts-jest": "^27.0.7",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typescript": "^4.3.5"
"typescript": "^4.3.5",
"web3": "^1.6.1"
},
"dependencies": {
"@improbable-eng/grpc-web": "^0.14.0",

View File

@ -5,3 +5,12 @@ export * from "./rpc";
export * from "./utils";
export * from "./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";

View File

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

View File

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

View File

@ -1,5 +1,7 @@
import { PublicKey } from "@solana/web3.js";
import { LCDClient } from "@terra-money/terra.js";
import { ethers } from "ethers";
import { fromUint8Array } from "js-base64";
import { CHAIN_ID_SOLANA } from "..";
import { NFTBridge__factory } from "../ethers-contracts";
import { importNftWasm } from "../solana/wasm";
@ -15,10 +17,10 @@ import { ChainId } from "../utils";
*/
export async function getForeignAssetEth(
tokenBridgeAddress: string,
provider: ethers.providers.Web3Provider,
provider: ethers.Signer | ethers.providers.Provider,
originChain: ChainId,
originAsset: Uint8Array
) {
): Promise<string | null> {
const tokenBridge = NFTBridge__factory.connect(tokenBridgeAddress, provider);
try {
if (originChain === CHAIN_ID_SOLANA) {
@ -35,6 +37,40 @@ export async function getForeignAssetEth(
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
* @param tokenBridgeAddress
@ -47,7 +83,7 @@ export async function getForeignAssetSol(
originChain: ChainId,
originAsset: Uint8Array,
tokenId: Uint8Array
) {
): Promise<string> {
const { wrapped_address } = await importNftWasm();
const wrappedAddress = wrapped_address(
tokenBridgeAddress,

View File

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

View File

@ -1,9 +1,11 @@
import { Connection, PublicKey } from "@solana/web3.js";
import { LCDClient } from "@terra-money/terra.js";
import { BigNumber, ethers } from "ethers";
import { arrayify, zeroPad } from "ethers/lib/utils";
import { canonicalAddress, WormholeWrappedInfo } from "..";
import { TokenImplementation__factory } from "../ethers-contracts";
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";
export interface WormholeWrappedNFTInfo {
@ -139,3 +141,32 @@ function bigToUint8Array(big: bigint) {
}
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),
};
}

View File

@ -1,5 +1,6 @@
export * from "./getForeignAsset";
export * from "./getIsWrappedAsset";
export * from "./getIsTransferCompleted";
export * from "./getOriginalAsset";
export * from "./redeem";
export * from "./transfer";

View File

@ -1,5 +1,7 @@
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
import { MsgExecuteContract } from "@terra-money/terra.js";
import { ethers } from "ethers";
import { fromUint8Array } from "js-base64";
import { CHAIN_ID_SOLANA } from "..";
import { Bridge__factory } from "../ethers-contracts";
import { ixFromRust } from "../solana";
@ -9,14 +11,14 @@ export async function redeemOnEth(
tokenBridgeAddress: string,
signer: ethers.Signer,
signedVAA: Uint8Array
) {
): Promise<ethers.ContractReceipt> {
const bridge = Bridge__factory.connect(tokenBridgeAddress, signer);
const v = await bridge.completeTransfer(signedVAA);
const receipt = await v.wait();
return receipt;
}
export async function isNFTVAASolanaNative(signedVAA: Uint8Array) {
export async function isNFTVAASolanaNative(signedVAA: Uint8Array): Promise<boolean> {
const { parse_vaa } = await importCoreWasm();
const parsedVAA = parse_vaa(signedVAA);
const isSolanaNative =
@ -31,7 +33,7 @@ export async function redeemOnSolana(
tokenBridgeAddress: string,
payerAddress: string,
signedVAA: Uint8Array
) {
): Promise<Transaction> {
const isSolanaNative = await isNFTVAASolanaNative(signedVAA);
const { complete_transfer_wrapped_ix, complete_transfer_native_ix } =
await importNftWasm();
@ -74,7 +76,7 @@ export async function createMetaOnSolana(
tokenBridgeAddress: string,
payerAddress: string,
signedVAA: Uint8Array
) {
): Promise<Transaction> {
const { complete_transfer_wrapped_meta_ix } = await importNftWasm();
const ix = ixFromRust(
complete_transfer_wrapped_meta_ix(
@ -90,3 +92,15 @@ export async function createMetaOnSolana(
transaction.feePayer = new PublicKey(payerAddress);
return transaction;
}
export async function redeemOnTerra(
tokenBridgeAddress: string,
walletAddress: string,
signedVAA: Uint8Array
): Promise<MsgExecuteContract> {
return new MsgExecuteContract(walletAddress, tokenBridgeAddress, {
submit_vaa: {
data: fromUint8Array(signedVAA),
},
});
}

View File

@ -1,5 +1,6 @@
import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js";
import { MsgExecuteContract } from "@terra-money/terra.js";
import { ethers } from "ethers";
import {
NFTBridge__factory,
@ -16,7 +17,7 @@ export async function transferFromEth(
tokenID: ethers.BigNumberish,
recipientChain: ChainId,
recipientAddress: Uint8Array
) {
): Promise<ethers.ContractReceipt> {
//TODO: should we check if token attestation exists on the target chain
const token = NFTImplementation__factory.connect(tokenAddress, signer);
await (await token.approve(tokenBridgeAddress, tokenID)).wait();
@ -44,7 +45,7 @@ export async function transferFromSolana(
originAddress?: Uint8Array,
originChain?: ChainId,
originTokenId?: Uint8Array
) {
): Promise<Transaction> {
const nonce = createNonce().readUInt32LE(0);
const transferIx = await getBridgeFeeIx(
connection,
@ -107,3 +108,41 @@ export async function transferFromSolana(
transaction.partialSign(messageKey);
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,
},
},
{}
),
];
}