317 lines
8.8 KiB
TypeScript
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();
|