sdk/js: Added getIsTransferCompleted

Change-Id: I034595b800ee2b881b9c2a9ab16d6e2a8e4a42e2
This commit is contained in:
Kevin Peters 2021-12-07 21:40:48 +00:00 committed by Evan Gray
parent 0888297b82
commit 70c173af75
8 changed files with 336 additions and 18 deletions

View File

@ -1,12 +1,12 @@
{
"name": "@certusone/wormhole-sdk",
"version": "0.0.8",
"version": "0.1.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@certusone/wormhole-sdk",
"version": "0.0.8",
"version": "0.1.1",
"license": "Apache-2.0",
"dependencies": {
"@improbable-eng/grpc-web": "^0.14.0",
@ -14,6 +14,7 @@
"@solana/web3.js": "^1.24.0",
"@terra-money/terra.js": "^2.0.14",
"@terra-money/wallet-provider": "^2.2.0",
"axios": "^0.24.0",
"bech32": "^2.0.0",
"js-base64": "^3.6.1",
"protobufjs": "^6.11.2",
@ -2370,6 +2371,14 @@
"node": ">=12"
}
},
"node_modules/@terra-money/terra.js/node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"dependencies": {
"follow-redirects": "^1.14.0"
}
},
"node_modules/@terra-money/wallet-provider": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@terra-money/wallet-provider/-/wallet-provider-2.2.0.tgz",
@ -2948,11 +2957,11 @@
"dev": true
},
"node_modules/axios": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"dependencies": {
"follow-redirects": "^1.10.0"
"follow-redirects": "^1.14.4"
}
},
"node_modules/babel-jest": {
@ -4201,9 +4210,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.2.tgz",
"integrity": "sha512-yLR6WaE2lbF0x4K2qE2p9PEXKLDjUjnR/xmjS3wHAYxtlsI9MLLBJUZirAHKzUZDGLxje7w/cXR49WOUo4rbsA==",
"version": "1.14.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
"integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==",
"funding": [
{
"type": "individual",
@ -10224,6 +10233,16 @@
"tmp": "^0.2.1",
"utf-8-validate": "^5.0.5",
"ws": "^7.4.2"
},
"dependencies": {
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"requires": {
"follow-redirects": "^1.14.0"
}
}
}
},
"@terra-money/wallet-provider": {
@ -10736,11 +10755,11 @@
"dev": true
},
"axios": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"requires": {
"follow-redirects": "^1.10.0"
"follow-redirects": "^1.14.4"
}
},
"babel-jest": {
@ -11741,9 +11760,9 @@
}
},
"follow-redirects": {
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.2.tgz",
"integrity": "sha512-yLR6WaE2lbF0x4K2qE2p9PEXKLDjUjnR/xmjS3wHAYxtlsI9MLLBJUZirAHKzUZDGLxje7w/cXR49WOUo4rbsA=="
"version": "1.14.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
"integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA=="
},
"form-data": {
"version": "3.0.1",

View File

@ -60,6 +60,7 @@
"@solana/web3.js": "^1.24.0",
"@terra-money/terra.js": "^2.0.14",
"@terra-money/wallet-provider": "^2.2.0",
"axios": "^0.24.0",
"bech32": "^2.0.0",
"js-base64": "^3.6.1",
"protobufjs": "^6.11.2",

View File

@ -0,0 +1,18 @@
import { importCoreWasm } from "../solana/wasm";
import { ethers } from "ethers";
import { uint8ArrayToHex } from "..";
export async function getSignedVAAHash(signedVAA: Uint8Array) {
const { parse_vaa } = await importCoreWasm();
const parsedVAA = parse_vaa(signedVAA);
const body = [
ethers.utils.defaultAbiCoder.encode(["uint32"], [parsedVAA.timestamp]).substring(2 + (64 - 8)),
ethers.utils.defaultAbiCoder.encode(["uint32"], [parsedVAA.nonce]).substring(2 + (64 - 8)),
ethers.utils.defaultAbiCoder.encode(["uint16"], [parsedVAA.emitter_chain]).substring(2 + (64 - 4)),
ethers.utils.defaultAbiCoder.encode(["bytes32"], [parsedVAA.emitter_address]).substring(2),
ethers.utils.defaultAbiCoder.encode(["uint64"], [parsedVAA.sequence]).substring(2 + (64 - 16)),
ethers.utils.defaultAbiCoder.encode(["uint8"], [parsedVAA.consistency_level]).substring(2 + (64 - 2)),
uint8ArrayToHex(parsedVAA.payload),
];
return ethers.utils.solidityKeccak256(["bytes"], [ethers.utils.solidityKeccak256(["bytes"], ["0x" + body.join("")])]);
}

View File

@ -1,3 +1,4 @@
export * from "./getClaimAddress";
export * from "./getEmitterAddress";
export * from "./getSignedVAAHash";
export * from "./parseSequenceFromLog";

View File

@ -20,6 +20,15 @@ export const SOLANA_CORE_BRIDGE_ADDRESS =
"Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o";
export const SOLANA_TOKEN_BRIDGE_ADDRESS =
"B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE";
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_TOKEN_BRIDGE_ADDRESS =
"terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4";
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 TEST_ERC20 = "0x2D8BE6BF0baA74e0A907016679CaE9190e80dD0A";
export const TEST_SOLANA_TOKEN = "2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ";
export const WORMHOLE_RPC_HOSTS = ["http://localhost:7071"];

View File

@ -1,12 +1,14 @@
import { parseUnits } from "@ethersproject/units";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
import { describe, jest, test } from "@jest/globals";
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, Transaction } from "@solana/web3.js";
import { LCDClient, MnemonicKey } from "@terra-money/terra.js";
import axios from "axios";
import { ethers } from "ethers";
import {
approveEth,
@ -14,11 +16,16 @@ import {
attestFromSolana,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
createWrappedOnEth,
createWrappedOnSolana,
createWrappedOnTerra,
getEmitterAddressEth,
getEmitterAddressSolana,
getForeignAssetSolana,
getIsTransferCompletedEth,
getIsTransferCompletedSolana,
getIsTransferCompletedTerra,
hexToUint8Array,
nativeToHexString,
parseSequenceFromLogEth,
@ -26,6 +33,7 @@ import {
postVaaSolana,
redeemOnEth,
redeemOnSolana,
redeemOnTerra,
transferFromEth,
transferFromSolana,
} from "../..";
@ -40,6 +48,11 @@ import {
SOLANA_HOST,
SOLANA_PRIVATE_KEY,
SOLANA_TOKEN_BRIDGE_ADDRESS,
TERRA_CHAIN_ID,
TERRA_GAS_PRICES_URL,
TERRA_NODE_URL,
TERRA_PRIVATE_KEY,
TERRA_TOKEN_BRIDGE_ADDRESS,
TEST_ERC20,
TEST_SOLANA_TOKEN,
WORMHOLE_RPC_HOSTS,
@ -51,7 +64,6 @@ jest.setTimeout(60000);
// TODO: setup keypair and provider/signer before, destroy provider after
// TODO: make the repeatable (can't attest an already attested token)
// TODO: add Terra
describe("Integration Tests", () => {
describe("Ethereum to Solana", () => {
@ -223,6 +235,13 @@ describe("Integration Tests", () => {
payerAddress,
Buffer.from(signedVAA)
);
expect(
await getIsTransferCompletedSolana(
SOLANA_TOKEN_BRIDGE_ADDRESS,
signedVAA,
connection
)
).toBe(false);
// redeem tokens on solana
const transaction = await redeemOnSolana(
connection,
@ -237,6 +256,13 @@ describe("Integration Tests", () => {
transaction.serialize()
);
await connection.confirmTransaction(txid);
expect(
await getIsTransferCompletedSolana(
SOLANA_TOKEN_BRIDGE_ADDRESS,
signedVAA,
connection
)
).toBe(true);
provider.destroy();
done();
} catch (e) {
@ -377,7 +403,21 @@ describe("Integration Tests", () => {
transport: NodeHttpTransport(),
}
);
expect(
await getIsTransferCompletedEth(
ETH_TOKEN_BRIDGE_ADDRESS,
provider,
signedVAA
)
).toBe(false);
await redeemOnEth(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA);
expect(
await getIsTransferCompletedEth(
ETH_TOKEN_BRIDGE_ADDRESS,
provider,
signedVAA
)
).toBe(true);
provider.destroy();
done();
} catch (e) {
@ -390,4 +430,178 @@ describe("Integration Tests", () => {
});
// TODO: it has increased balance
});
describe("Ethereum to Terra", () => {
test("Attest Ethereum ERC-20 to Terra", (done) => {
(async () => {
try {
// create a signer for Eth
const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
const signer = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
// attest the test token
const receipt = await attestFromEth(
ETH_TOKEN_BRIDGE_ADDRESS,
signer,
TEST_ERC20
);
// get the sequence from the logs (needed to fetch the vaa)
const sequence = parseSequenceFromLogEth(
receipt,
ETH_CORE_BRIDGE_ADDRESS
);
const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
// poll until the guardian(s) witness and sign the vaa
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
CHAIN_ID_ETH,
emitterAddress,
sequence,
{
transport: NodeHttpTransport(),
}
);
const lcd = new LCDClient({
URL: TERRA_NODE_URL,
chainID: TERRA_CHAIN_ID,
});
const mk = new MnemonicKey({
mnemonic: TERRA_PRIVATE_KEY,
});
const wallet = lcd.wallet(mk);
const msg = await createWrappedOnTerra(
TERRA_TOKEN_BRIDGE_ADDRESS,
wallet.key.accAddress,
signedVAA
);
const gasPrices = await axios
.get(TERRA_GAS_PRICES_URL)
.then((result) => result.data);
const feeEstimate = await lcd.tx.estimateFee(
wallet.key.accAddress,
[msg],
{
feeDenoms: ["uluna"],
gasPrices,
}
);
const tx = await wallet.createAndSignTx({
msgs: [msg],
memo: "test",
feeDenoms: ["uluna"],
gasPrices,
fee: feeEstimate,
});
await lcd.tx.broadcast(tx);
provider.destroy();
done();
} catch (e) {
console.error(e);
done(
"An error occurred while trying to attest from Ethereum to Terra"
);
}
})();
});
// TODO: it is attested
test("Send Ethereum ERC-20 to Terra", (done) => {
(async () => {
try {
// create a signer for Eth
const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
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
);
const lcd = new LCDClient({
URL: TERRA_NODE_URL,
chainID: TERRA_CHAIN_ID,
});
const mk = new MnemonicKey({
mnemonic: TERRA_PRIVATE_KEY,
});
const wallet = lcd.wallet(mk);
// transfer tokens
const receipt = await transferFromEth(
ETH_TOKEN_BRIDGE_ADDRESS,
signer,
TEST_ERC20,
amount,
CHAIN_ID_TERRA,
hexToUint8Array(
nativeToHexString(wallet.key.accAddress, CHAIN_ID_TERRA) || ""
)
);
// get the sequence from the logs (needed to fetch the vaa)
const sequence = parseSequenceFromLogEth(
receipt,
ETH_CORE_BRIDGE_ADDRESS
);
const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
// poll until the guardian(s) witness and sign the vaa
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
CHAIN_ID_ETH,
emitterAddress,
sequence,
{
transport: NodeHttpTransport(),
}
);
expect(
await getIsTransferCompletedTerra(
TERRA_TOKEN_BRIDGE_ADDRESS,
signedVAA,
wallet.key.accAddress,
lcd,
TERRA_GAS_PRICES_URL
)
).toBe(false);
const msg = await redeemOnTerra(
TERRA_TOKEN_BRIDGE_ADDRESS,
wallet.key.accAddress,
signedVAA
);
const gasPrices = await axios
.get(TERRA_GAS_PRICES_URL)
.then((result) => result.data);
const feeEstimate = await lcd.tx.estimateFee(
wallet.key.accAddress,
[msg],
{
memo: "localhost",
feeDenoms: ["uluna"],
gasPrices,
}
);
const tx = await wallet.createAndSignTx({
msgs: [msg],
memo: "localhost",
feeDenoms: ["uluna"],
gasPrices,
fee: feeEstimate,
});
await lcd.tx.broadcast(tx);
expect(
await getIsTransferCompletedTerra(
TERRA_TOKEN_BRIDGE_ADDRESS,
signedVAA,
wallet.key.accAddress,
lcd,
TERRA_GAS_PRICES_URL
)
).toBe(true);
provider.destroy();
done();
} catch (e) {
console.error(e);
done("An error occurred while trying to send from Ethereum to Terra");
}
})();
});
// TODO: it has increased balance
});
});

View File

@ -0,0 +1,55 @@
import { ethers } from "ethers";
import { Bridge__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(
tokenBridgeAddress: string,
provider: ethers.providers.Provider,
signedVAA: Uint8Array
) {
const tokenBridge = Bridge__factory.connect(tokenBridgeAddress, provider);
const signedVAAHash = await getSignedVAAHash(signedVAA);
return await tokenBridge.isTransferCompleted(signedVAAHash);
}
export async function getIsTransferCompletedTerra(
tokenBridgeAddress: string,
signedVAA: Uint8Array,
walletAddress: string,
client: LCDClient,
gasPriceUrl: string
) {
const msg = await redeemOnTerra(tokenBridgeAddress, 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(
tokenBridgeAddress: string,
signedVAA: Uint8Array,
connection: Connection
) {
const { claim_address } = await importCoreWasm();
const claimAddress = await claim_address(tokenBridgeAddress, signedVAA);
const claimInfo = await connection.getAccountInfo(
new PublicKey(claimAddress),
"confirmed"
);
return !!claimInfo;
}

View File

@ -1,6 +1,7 @@
export * from "./attest";
export * from "./createWrapped";
export * from "./getForeignAsset";
export * from "./getIsTransferCompleted";
export * from "./getIsWrappedAsset";
export * from "./getOriginalAsset";
export * from "./redeem";