Skip to content

use wormhole connect sdk (WIP) #57

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@
"cwd": "${workspaceRoot}",
"internalConsoleOptions": "openOnSessionStart",
"skipFiles": ["<node_internals>/**"]
},
{
"name": "relay",
"type": "node",
"request": "launch",
"runtimeExecutable": "node",
"runtimeArgs": [
"--inspect=8888",
"--nolazy",
"-r",
"ts-node/register/transpile-only",
],
"args": ["scripts/wh-transfer.ts"],
"cwd": "${workspaceRoot}",
"internalConsoleOptions": "openOnSessionStart",
"skipFiles": ["<node_internals>/**"]
}
]
}
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
"@improbable-eng/grpc-web": "^0.14.0",
"@improbable-eng/grpc-web-node-http-transport": "^0.15.0",
"@polkadot/api": "^10.9.1",
"@wormhole-foundation/connect-sdk": "^0.3.1",
"@wormhole-foundation/connect-sdk-evm": "^0.3.1",
"@wormhole-foundation/connect-sdk-evm-tokenbridge": "^0.3.1",
"axios": "^0.26.1",
"bech32": "^2.0.0",
"cors": "^2.8.5",
Expand Down
14 changes: 14 additions & 0 deletions scripts/wh-transfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { EvmPlatform } from '@wormhole-foundation/connect-sdk-evm';
import { Wormhole } from '@wormhole-foundation/connect-sdk';

import { FUJI_TOKEN } from '../src/consts';
import { TEST_ADDR_USER } from '../src/__tests__/utils/testConsts';
import { completeTransfer, transferFromFujiToKaruraTestnet } from '../src/__tests__/utils/wormhole';

(async () => {
const network = 'Testnet';
const wh = new Wormhole(network, [EvmPlatform]);

const txId = await transferFromFujiToKaruraTestnet(wh, '0.0001', FUJI_TOKEN.USDC, TEST_ADDR_USER);
await completeTransfer(wh, txId);
})();
3 changes: 3 additions & 0 deletions src/__tests__/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './testEndpoints';
export * from './testUtils';
export * from './testConsts';
2 changes: 2 additions & 0 deletions src/__tests__/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const start = (msg: string) => process.stdout.write(`▶️ ${msg} ... `);
export const ok = (msg?: string) => console.log(`✨ ${msg ?? ''}`);
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,92 +1,8 @@
import { ApiPromise } from '@polkadot/api';
import { CHAIN_ID_KARURA } from '@certusone/wormhole-sdk';
import { ERC20__factory } from '@acala-network/asset-router/dist/typechain-types';
import { JsonRpcProvider } from '@ethersproject/providers';
import { Wallet } from 'ethers';
import { expect } from 'vitest';
import { formatEther, parseEther, parseUnits } from 'ethers/lib/utils';
import axios from 'axios';
import request from 'supertest';

import { ETH_RPC, RELAYER_API, RELAYER_URL } from '../consts';
import { KARURA_USDC_ADDRESS, TEST_KEY } from './testConsts';
import { createApp } from '../app';
import { transferFromAvax } from '../utils';

export const transferFromFujiToKaruraTestnet = async (
amount: string,
sourceAsset: string,
recipientAddr: string,
) => {
const provider = new JsonRpcProvider(ETH_RPC.FUJI);
const wallet = new Wallet(TEST_KEY.USER, provider);

const bal = await wallet.getBalance();
if (bal.lt(parseEther('0.03'))) {
throw new Error(`${wallet.address} has insufficient balance on fuji! bal: ${formatEther(bal)}`);
}

return await transferFromAvax(
amount,
sourceAsset,
recipientAddr,
CHAIN_ID_KARURA,
wallet,
false,
);
};

export const encodeXcmDest = (_data: any) => {
// TODO: use api to encode
return '0x03010200a9200100d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d';
};

export const getBasiliskUsdcBalance = async (api: ApiPromise, addr: string) => {
const balance = await api.query.tokens.accounts(addr, 3);
return (balance as any).free.toBigInt();
};

export const transferToRouter = async (
routerAddr: string,
signer: Wallet,
tokenAddr = KARURA_USDC_ADDRESS,
amount = 0.001,
) => {
const token = ERC20__factory.connect(tokenAddr, signer);

const decimals = await token.decimals();
const routeAmount = parseUnits(String(amount), decimals);

const routerBal = await token.balanceOf(routerAddr);
if (routerBal.gt(0)) {
expect(routerBal.toBigInt()).to.eq(routeAmount.toBigInt());
} else {
const signerTokenBal = await token.balanceOf(signer.address);
if (signerTokenBal.lt(routeAmount)) {
throw new Error(`signer ${signer.address} has no enough token [${tokenAddr}] to transfer! ${signerTokenBal.toBigInt()} < ${routeAmount.toBigInt()}`);
}
await (await token.transfer(routerAddr, routeAmount)).wait();
}
};
export const mockXcmToRouter = transferToRouter;

export const expectError = (err: any, msg: any, code: number) => {
if (axios.isAxiosError(err)) {
expect(err.response?.status).to.equal(code);
expect(err.response?.data.error).to.deep.equal(msg);
} else { // HttpError from supertest
expect(err.status).to.equal(code);
expect(JSON.parse(err.text).error).to.deep.equal(msg);
}
};

export const expectErrorData = (err: any, expectFn: any) => {
expectFn(
axios.isAxiosError(err)
? err.response?.data
: JSON.parse(err.text),
);
};
import { RELAYER_API, RELAYER_URL } from '../../consts';
import { createApp } from '../../app';

/* ------------------------------------------------------------------ */
/* ---------------------- test endpoints ---------------------- */
Expand Down
60 changes: 60 additions & 0 deletions src/__tests__/utils/testUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ApiPromise } from '@polkadot/api';
import { ERC20__factory } from '@acala-network/asset-router/dist/typechain-types';
import { Wallet } from 'ethers';
import { expect } from 'vitest';
import { parseUnits } from 'ethers/lib/utils';
import axios from 'axios';

import { KARURA_USDC_ADDRESS } from './testConsts';

export const encodeXcmDest = (_data: any) => {
// TODO: use api to encode
return '0x03010200a9200100d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d';
};

export const getBasiliskUsdcBalance = async (api: ApiPromise, addr: string) => {
const balance = await api.query.tokens.accounts(addr, 3);
return (balance as any).free.toBigInt();
};

export const transferToRouter = async (
routerAddr: string,
signer: Wallet,
tokenAddr = KARURA_USDC_ADDRESS,
amount = 0.001,
) => {
const token = ERC20__factory.connect(tokenAddr, signer);

const decimals = await token.decimals();
const routeAmount = parseUnits(String(amount), decimals);

const routerBal = await token.balanceOf(routerAddr);
if (routerBal.gt(0)) {
expect(routerBal.toBigInt()).to.eq(routeAmount.toBigInt());
} else {
const signerTokenBal = await token.balanceOf(signer.address);
if (signerTokenBal.lt(routeAmount)) {
throw new Error(`signer ${signer.address} has no enough token [${tokenAddr}] to transfer! ${signerTokenBal.toBigInt()} < ${routeAmount.toBigInt()}`);
}
await (await token.transfer(routerAddr, routeAmount)).wait();
}
};
export const mockXcmToRouter = transferToRouter;

export const expectError = (err: any, msg: any, code: number) => {
if (axios.isAxiosError(err)) {
expect(err.response?.status).to.equal(code);
expect(err.response?.data.error).to.deep.equal(msg);
} else { // HttpError from supertest
expect(err.status).to.equal(code);
expect(JSON.parse(err.text).error).to.deep.equal(msg);
}
};

export const expectErrorData = (err: any, expectFn: any) => {
expectFn(
axios.isAxiosError(err)
? err.response?.data
: JSON.parse(err.text),
);
};
67 changes: 67 additions & 0 deletions src/__tests__/utils/whSigner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
Chain,
Network,
SignAndSendSigner,
SignedTx,
TxHash,
UnsignedTransaction,
} from '@wormhole-foundation/connect-sdk';
import { TransactionRequest } from '@ethersproject/abstract-provider';
import { Wallet, ethers } from 'ethers';

// Wormhole SignOnlySender EvmSigner
export class WhEvmSigner<N extends Network, C extends Chain> implements SignAndSendSigner<N, C> {
#wallet: ethers.Wallet;
#chain: C;

constructor(wallet: Wallet, chain: C) {
this.#wallet = wallet;
this.#chain = chain;
}

chain(): C {
return this.#chain;
}

address(): string {
return this.#wallet.address;
}

async _sign(txs: UnsignedTransaction[]): Promise<SignedTx[]> {
const signed: string[] = [];

let nonce = await this.#wallet.getTransactionCount('pending');

for (const tx of txs) {
const { transaction } = tx;
const gasPrice = await this.#wallet.provider.getGasPrice();

// TODO: estiamte gas doesn't seem to throw if tx fails, such as transfer amount too big
const gasLimit = await this.#wallet.provider.estimateGas(transaction);
const txReq: TransactionRequest = {
...transaction,
nonce,
gasLimit,
gasPrice,
};

txReq.chainId = Number(txReq.chainId); // TODO: remove me after upgrading to ethers V6

signed.push(await this.#wallet.signTransaction(txReq));

nonce += 1;
}
return signed;
}

async signAndSend(txs: UnsignedTransaction[]): Promise<TxHash[]> {
const signed = await this._sign(txs);

const hashes: string[] = [];
for (const s of signed) {
const tx = await this.#wallet.provider.sendTransaction(s);
hashes.push(tx.hash);
}
return hashes;
}
}
98 changes: 98 additions & 0 deletions src/__tests__/utils/wormhole.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import '@wormhole-foundation/connect-sdk-evm-tokenbridge';

import { AcalaJsonRpcProvider } from '@acala-network/eth-providers';
import { JsonRpcProvider } from '@ethersproject/providers';
import { Network, TokenTransfer, TransactionId, TransferState, Wormhole, normalizeAmount } from '@wormhole-foundation/connect-sdk';
import { Wallet } from 'ethers';
import { formatEther, parseEther } from 'ethers/lib/utils';
import { getEvmSigner } from '@wormhole-foundation/connect-sdk-evm/dist/cjs/testing';

import { ETH_RPC } from '../../consts';
import { TEST_KEY } from './testConsts';
import { WhEvmSigner } from './whSigner';
import { ok, start } from './logger';

export async function waitForAttestation<N extends Network = Network>(
wh: Wormhole<N>,
xfer: TokenTransfer<N>,
): Promise<TransactionId> {
const tracker = TokenTransfer.track(wh, TokenTransfer.getReceipt(xfer), 100000);

for await (const receipt of tracker) {
console.log('Current trasfer state: ', TransferState[receipt.state]);
if (receipt.state === TransferState.Attested) {
const txId = receipt.originTxs[0];
return txId;
}
}

throw new Error('tx never got attested');
}

export const transferFromFujiToKaruraTestnet = async (
wh: Wormhole<Network>,
amount: string,
sourceAsset: string,
recipientAddr: string,
): Promise<TransactionId> => {
start('checking user balance');
const provider = new JsonRpcProvider(ETH_RPC.FUJI);
const wallet = new Wallet(TEST_KEY.USER, provider);

const bal = await wallet.getBalance();
if (bal.lt(parseEther('0.03'))) {
throw new Error(`${wallet.address} has insufficient balance on fuji! bal: ${formatEther(bal)}`);
}
ok();

start('setting up wormhole context');
const srcChain = wh.getChain('Avalanche');
const dstChain = wh.getChain('Karura');
const signer = await getEvmSigner(await srcChain.getRpc(), TEST_KEY.USER);
ok();

start('constructing trasnfer tx');
const decimals = 6n;
const normalizedAmount = normalizeAmount(amount, decimals);
const xfer = await wh.tokenTransfer(
Wormhole.chainAddress(srcChain.chain, sourceAsset),
normalizedAmount, // Amount in base units
Wormhole.chainAddress(srcChain.chain, signer.address()), // Sender address on source chain
Wormhole.chainAddress(dstChain.chain, recipientAddr), // Recipient address on destination chain
false, // No Automatic transfer
);
ok();

start('initiating transfer');
await xfer.initiateTransfer(signer);
ok();

console.log('waiting for attestation ...');
const txId = await waitForAttestation(wh, xfer);

/* ---------- alternatively wait for attestation manually (no status update) ---------- */
// const srcTxids = await xfer.initiateTransfer(signer);
// console.log('srcTxids', srcTxids);

// const timeout = 600_000;
// const attestIds = await xfer.fetchAttestation(timeout);
// console.log('attestIds', attestIds);
/* ------------------------------------------------------- ---------- */

return txId;
};

export const completeTransfer = async (wh: Wormhole<Network>, txId: TransactionId) => {
start('re-constructing trasnfer tx');
const xfer = await TokenTransfer.from(wh, txId);
ok();

const dstChain = wh.getChain('Karura'); // TODO: get dstChain from VAA or by param?
const dstProvider = new AcalaJsonRpcProvider(ETH_RPC.KARURA_TESTNET);
const dstWallet = new Wallet(TEST_KEY.USER, dstProvider);
const dstSigner = new WhEvmSigner(dstWallet, dstChain.chain);

start('completing trasnfer');
const destTxids = await xfer.completeTransfer(dstSigner);
ok(JSON.stringify(destTxids));
};
1 change: 1 addition & 0 deletions src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const enum ETH_RPC {
FUJI = 'https://avalanche-fuji-c-chain.publicnode.com',
GOERLI = 'https://rpc.ankr.com/eth_goerli',
KARURA_TESTNET = 'https://eth-rpc-karura-testnet.aca-staging.network',
ACALA_TESTNET = 'https://eth-rpc-acala-testnet.aca-staging.network',
};

export const enum BSC_TOKEN {
Expand Down