wormhole/near/test/transferToNear.ts

317 lines
8.8 KiB
TypeScript

// NOTE: This script only supports transferring tokens that originated on NEAR
// TODO: get_foreign_asset instead of hash_lookup for non-NEAR originating tokens :)
// Prerequisites
// cd ethereum && npm ci
// cd sdk/js && npm ci && npm run build
// cd near
// npm ci
// Run with
// EVM_PK="" EVM_PROVIDER_URL="" EVM_CHAIN_NAME="" EVM_TOKEN="" TOKENS_TO_SEND="" NEAR_MNEMONIC="" NEAR_ACCOUNT="" npm run transferToNear
// or
// EVM_PK="" EVM_PROVIDER_URL="" EVM_CHAIN_NAME="" EVM_TOKEN="" TOKENS_TO_SEND="" NEAR_PK="" NEAR_ACCOUNT="" npm run transferToNear
// for Eth try EVM_PROVIDER_URL="https://rpc.ankr.com/eth" and EVM_CHAIN_NAME="ethereum"
// for BSC try EVM_PROVIDER_URL="https://rpc.ankr.com/bsc" and EVM_CHAIN_NAME="bsc"
// It is SUPER SUPER important to use the near-api-js that comes from inside wormhole-sdk or all heck breaks lose
import {
Account as nearAccount,
connect as nearConnect,
keyStores as nearKeyStores,
providers as nearProviders,
utils as nearUtils,
} from "@certusone/wormhole-sdk/node_modules/near-api-js";
import {
approveEth,
Bridge__factory,
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_NEAR,
coalesceChainId,
CONTRACTS,
EVMChainName,
getEmitterAddressEth,
getOriginalAssetEth,
getSignedVAAWithRetry,
hexToUint8Array,
isChain,
isEVMChain,
parseSequenceFromLogEth,
redeemOnNear,
transferFromEth,
uint8ArrayToHex,
} from "@certusone/wormhole-sdk";
import colors from "@colors/colors/safe";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
import BN from "bn.js";
import { ethers } from "ethers";
// @ts-ignore
import { parseSeedPhrase } from "near-seed-phrase";
import prompt from "prompt";
export const WORMHOLE_RPC_HOSTS = [
"https://wormhole-v2-mainnet-api.certus.one",
"https://wormhole.inotel.ro",
"https://wormhole-v2-mainnet-api.mcf.rocks",
"https://wormhole-v2-mainnet-api.chainlayer.network",
"https://wormhole-v2-mainnet-api.staking.fund",
"https://wormhole-v2-mainnet.01node.com",
];
if (!process.env.EVM_PK) {
console.log("EVM_PK is required");
process.exit(1);
}
if (!process.env.EVM_PROVIDER_URL) {
console.log(
"EVM_PROVIDER_URL is required (try https://rpc.ankr.com/eth for Eth)"
);
process.exit(1);
}
if (
!process.env.EVM_CHAIN_NAME ||
!isChain(process.env.EVM_CHAIN_NAME) ||
!isEVMChain(process.env.EVM_CHAIN_NAME) ||
!CONTRACTS.MAINNET[process.env.EVM_CHAIN_NAME].core ||
!CONTRACTS.MAINNET[process.env.EVM_CHAIN_NAME].token_bridge
) {
console.log(
"EVM_CHAIN_NAME is required and must be a valid Wormhole EVM chain name"
);
process.exit(1);
}
if (!process.env.EVM_TOKEN) {
console.log("EVM_TOKEN is required");
process.exit(1);
}
if (!process.env.NEAR_ACCOUNT) {
console.log("NEAR_ACCOUNT is required");
process.exit(1);
}
if (!process.env.TOKENS_TO_SEND) {
console.log("TOKENS_TO_SEND is required");
process.exit(1);
}
const EVM_PK: string = process.env.EVM_PK;
const EVM_TOKEN: string = process.env.EVM_TOKEN;
const CHAIN_NAME: EVMChainName = process.env.EVM_CHAIN_NAME;
const CHAIN_ID: ChainId = coalesceChainId(process.env.EVM_CHAIN_NAME);
const TOKENS_TO_SEND: bigint = BigInt(process.env.TOKENS_TO_SEND);
async function transferTest() {
let provider = new ethers.providers.JsonRpcProvider(
process.env.EVM_PROVIDER_URL
);
let signer = new ethers.Wallet(EVM_PK, provider);
let bridge = Bridge__factory.connect(
CONTRACTS.MAINNET[CHAIN_NAME].token_bridge as string,
signer
);
let nearNodeUrl = "https://rpc.mainnet.near.org";
let networkId = "mainnet";
// There are many kinds of keystores... in this case, I am using a InMemory one
let keyStore = new nearKeyStores.InMemoryKeyStore();
if (process.env.NEAR_MNEMONIC) {
let userKeys = parseSeedPhrase(process.env.NEAR_MNEMONIC);
let userKey = nearUtils.KeyPair.fromString(userKeys["secretKey"]);
keyStore.setKey(networkId, process.env.NEAR_ACCOUNT as string, userKey);
} else if (process.env.NEAR_PK) {
let userKey = nearUtils.KeyPair.fromString(process.env.NEAR_PK);
keyStore.setKey(networkId, process.env.NEAR_ACCOUNT as string, userKey);
} else {
console.log("NEAR_MNEMONIC or NEAR_PK is required");
process.exit(1);
}
// connect to near...
let near = await nearConnect({
headers: {},
keyStore,
networkId: networkId as string,
nodeUrl: nearNodeUrl as string,
});
console.log(
"Sending",
TOKENS_TO_SEND.toString(),
EVM_TOKEN,
"from",
await signer.getAddress(),
"to",
process.env.NEAR_ACCOUNT as string,
"on Near"
);
prompt.message = "";
const { input } = await prompt.get({
properties: {
input: {
description: colors.red(
"Are you sure you want to send tokens? THIS CANNOT BE UNDONE! [y/N]"
),
},
},
});
if (input !== "y") return;
// rpc handle
const userAccount = new nearAccount(
near.connection,
process.env.NEAR_ACCOUNT as string
);
const { assetAddress, chainId } = await getOriginalAssetEth(
CONTRACTS.MAINNET[CHAIN_NAME].token_bridge as string,
provider,
EVM_TOKEN,
CHAIN_NAME
);
console.log("Original asset:", chainId, uint8ArrayToHex(assetAddress));
const [success, nearTokenContract] = await userAccount.viewFunction(
CONTRACTS.MAINNET.near.token_bridge,
"hash_lookup",
{ hash: uint8ArrayToHex(assetAddress) }
);
console.log("Near token contract:", nearTokenContract);
const initialBalance = await userAccount.viewFunction(
nearTokenContract,
"ft_balance_of",
{ account_id: process.env.NEAR_ACCOUNT as string }
);
console.log(
"Balance of",
nearTokenContract,
"for",
process.env.NEAR_ACCOUNT,
":",
initialBalance
);
// So, near can have account names up to 64 bytes but wormhole can only have 32...
// as a result, we have to hash our account names to sha256's.. What we are doing
// here is doing a RPC call (does not require any interaction with the wallet and is free)
// that both tells us our account hash AND if we are already registered...
let account_hash = await userAccount.viewFunction(
CONTRACTS.MAINNET.near.token_bridge,
"hash_account",
{
account: userAccount.accountId,
}
);
console.log(account_hash);
let myAddress = account_hash[1];
if (!account_hash[0]) {
console.log("Registering the receiving account");
let myAddress2 = nearProviders.getTransactionLastResult(
await userAccount.functionCall({
contractId: CONTRACTS.MAINNET.near.token_bridge,
methodName: "register_account",
args: { account: process.env.NEAR_ACCOUNT as string },
gas: new BN("100000000000000"),
attachedDeposit: new BN("2000000000000000000000"), // 0.002 NEAR
})
);
console.log("account hash returned: " + myAddress2);
} else {
console.log("account already registered");
}
console.log("Approving...");
// approve the bridge to spend tokens
await approveEth(
CONTRACTS.MAINNET[CHAIN_NAME].token_bridge as string,
EVM_TOKEN,
signer,
TOKENS_TO_SEND
);
console.log("Transferring...");
// transfer tokens
let receipt = await transferFromEth(
CONTRACTS.MAINNET[CHAIN_NAME].token_bridge as string,
signer,
EVM_TOKEN,
TOKENS_TO_SEND,
CHAIN_ID_NEAR,
hexToUint8Array(myAddress)
);
console.log("EVM tx submitted", receipt.transactionHash);
const sequence = await parseSequenceFromLogEth(
receipt,
CONTRACTS.MAINNET[CHAIN_NAME].core as string
);
console.log(sequence);
const emitterAddress = getEmitterAddressEth(
CONTRACTS.MAINNET[CHAIN_NAME].token_bridge as string
);
console.log("emitterAddress:", emitterAddress, "sequence:", sequence);
console.log(
`If this script hangs, try https://wormhole-v2-mainnet-api.certus.one/v1/signed_vaa/${CHAIN_ID}/${emitterAddress}/${sequence.toString()}`
);
const confirmationsRequired = await bridge.finality();
console.log(
"Please be patient. Waiting",
confirmationsRequired,
"confirmations..."
);
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
CHAIN_ID,
emitterAddress,
sequence,
{
transport: NodeHttpTransport(),
}
);
console.log("VAA received!");
console.log(uint8ArrayToHex(signedVAA));
const redeemMsgs = await redeemOnNear(
userAccount.connection.provider,
userAccount.accountId,
CONTRACTS.MAINNET.near.token_bridge,
signedVAA
);
for (const msg of redeemMsgs) {
await userAccount.functionCall(msg);
}
console.log("Redeemed!");
const endingBalance = await userAccount.viewFunction(
nearTokenContract,
"ft_balance_of",
{ account_id: process.env.NEAR_ACCOUNT as string }
);
console.log("Balance of", nearTokenContract, "for", process.env.NEAR_ACCOUNT);
console.log("Before:", initialBalance);
console.log("After:", endingBalance);
}
transferTest();