diff --git a/src/api/route.ts b/src/api/route.ts index ae84347..cb895db 100644 --- a/src/api/route.ts +++ b/src/api/route.ts @@ -1,8 +1,7 @@ import { Bridge__factory } from '@certusone/wormhole-sdk/lib/cjs/ethers-contracts'; -import { ChainId, hexToUint8Array } from '@certusone/wormhole-sdk'; import { Factory__factory } from '@acala-network/asset-router/dist/typechain-types'; -import { JsonRpcProvider } from '@ethersproject/providers'; import { XcmInstructionsStruct } from '@acala-network/asset-router/dist/typechain-types/src/Factory'; +import { hexToUint8Array } from '@certusone/wormhole-sdk'; import { DEST_PARA_ID_TO_ROUTER_WORMHOLE_CHAIN_ID, @@ -11,12 +10,10 @@ import { RelayAndRouteParams, RouteParamsWormhole, RouteParamsXcm, - _prepareRoute, checkShouldRelayBeforeRouting, - getApi, + getChainConfig, getEthExtrinsic, getRouterChainTokenAddr, - getSigner, logger, prepareRouteWormhole, prepareRouteXcm, @@ -25,13 +22,13 @@ import { } from '../utils'; export const routeXcm = async (routeParamsXcm: RouteParamsXcm): Promise => { - const { chainConfig, signer } = await prepareRouteXcm(routeParamsXcm); + const { chainConfig } = await prepareRouteXcm(routeParamsXcm); const xcmInstruction: XcmInstructionsStruct = { dest: routeParamsXcm.dest, weight: '0x00', }; - const factory = Factory__factory.connect(chainConfig.factoryAddr, signer); + const factory = Factory__factory.connect(chainConfig.factoryAddr, chainConfig.wallet); const routerChainTokenAddr = await getRouterChainTokenAddr(routeParamsXcm.originAddr, chainConfig); const tx = await factory.deployXcmRouterAndRoute( @@ -46,22 +43,22 @@ export const routeXcm = async (routeParamsXcm: RouteParamsXcm): Promise }; export const _populateRelayTx = async (params: RelayAndRouteParams) => { - const routerChainId = DEST_PARA_ID_TO_ROUTER_WORMHOLE_CHAIN_ID[params.destParaId] as ChainId; - const { chainConfig, signer } = await _prepareRoute(routerChainId); - await checkShouldRelayBeforeRouting(params, chainConfig, signer); + const routerChainId = DEST_PARA_ID_TO_ROUTER_WORMHOLE_CHAIN_ID[params.destParaId]; + const chainConfig = await getChainConfig(routerChainId); + await checkShouldRelayBeforeRouting(params, chainConfig); - const bridge = Bridge__factory.connect(chainConfig.tokenBridgeAddr, signer); + const bridge = Bridge__factory.connect(chainConfig.tokenBridgeAddr, chainConfig.wallet); return await bridge.populateTransaction.completeTransfer(hexToUint8Array(params.signedVAA)); }; export const _populateRouteTx = async (routeParamsXcm: RelayAndRouteParams) => { - const { chainConfig, signer } = await prepareRouteXcm(routeParamsXcm); + const { chainConfig } = await prepareRouteXcm(routeParamsXcm); const xcmInstruction: XcmInstructionsStruct = { dest: routeParamsXcm.dest, weight: '0x00', }; - const factory = Factory__factory.connect(chainConfig.factoryAddr, signer); + const factory = Factory__factory.connect(chainConfig.factoryAddr, chainConfig.wallet); const routerChainTokenAddr = await getRouterChainTokenAddr(routeParamsXcm.originAddr, chainConfig); return await factory.populateTransaction.deployXcmRouterAndRoute( @@ -77,17 +74,8 @@ export const relayAndRouteBatch = async (params: RelayAndRouteParams): Promise => { - const routerChainId = DEST_PARA_ID_TO_ROUTER_WORMHOLE_CHAIN_ID[params.destParaId] as ChainId; - const { chainConfig, signer } = await _prepareRoute(routerChainId); - await checkShouldRelayBeforeRouting(params, chainConfig, signer); + const routerChainId = DEST_PARA_ID_TO_ROUTER_WORMHOLE_CHAIN_ID[params.destParaId]; + const chainConfig = await getChainConfig(routerChainId); + await checkShouldRelayBeforeRouting(params, chainConfig); const wormholeReceipt = await relayEVM(chainConfig, params.signedVAA); logger.debug({ txHash: wormholeReceipt.transactionHash }, 'relay finished'); @@ -117,16 +105,16 @@ export const relayAndRoute = async (params: RelayAndRouteParams): Promise<[strin export const routeWormhole = async (routeParamsWormhole: RouteParamsWormhole): Promise => { const { chainConfig, - signer, routerChainTokenAddr, wormholeInstructions, } = await prepareRouteWormhole(routeParamsWormhole); + const { feeAddr, tokenBridgeAddr, factoryAddr, wallet } = chainConfig; - const factory = Factory__factory.connect(chainConfig.factoryAddr, signer); + const factory = Factory__factory.connect(factoryAddr, wallet); const tx = await factory.deployWormholeRouterAndRoute( - chainConfig.feeAddr, + feeAddr, wormholeInstructions, - chainConfig.tokenBridgeAddr, + tokenBridgeAddr, routerChainTokenAddr, ); const receipt = await tx.wait(); diff --git a/src/utils/configureEnv.ts b/src/utils/configureEnv.ts index 6d15569..545a5a9 100644 --- a/src/utils/configureEnv.ts +++ b/src/utils/configureEnv.ts @@ -1,95 +1,103 @@ import { ADDRESSES } from '@acala-network/asset-router/dist/consts'; -import { CHAIN_ID_ACALA, CHAIN_ID_KARURA, ChainId } from '@certusone/wormhole-sdk'; +import { AcalaJsonRpcProvider } from '@acala-network/eth-providers'; +import { ApiPromise } from '@polkadot/api'; +import { CHAIN_ID_ACALA, CHAIN_ID_KARURA } from '@certusone/wormhole-sdk'; +import { Wallet } from 'ethers'; import dotenv from 'dotenv'; -import { ROUTER_CHAIN_ID } from './utils'; +import { ROUTER_CHAIN_ID, getApi } from './utils'; dotenv.config({ path: '.env' }); -export type RelayerEnvironment = { - supportedChains: ChainConfig[]; -}; - export type ChainConfig = { chainId: ROUTER_CHAIN_ID; - ethRpc: string; - nodeUrl: string; - walletPrivateKey: string; + provider: AcalaJsonRpcProvider; + wallet: Wallet; + api: ApiPromise; + relayerSubstrateAddr: string; tokenBridgeAddr: string; feeAddr: string; factoryAddr: string; isTestnet: boolean; }; -const isTestnet = Boolean(Number(process.env.TESTNET_MODE ?? 1)); - -export function prepareEnvironment(): RelayerEnvironment { - const supportedChains: ChainConfig[] = []; - supportedChains.push(configKarura()); - supportedChains.push(configAcala()); - - return { supportedChains }; -} - -function configKarura(): ChainConfig { - const requiredEnvVars = [ - 'KARURA_ETH_RPC', - 'KARURA_PRIVATE_KEY', - 'KARURA_NODE_URL', - ]; +export type RelayerEnvironment = ChainConfig[]; - for (const envVar of requiredEnvVars) { +const ensureEnvVars = (envVars: string[]) => { + for (const envVar of envVars) { if (!process.env[envVar]) { console.error(`Missing environment variable ${envVar}`); process.exit(1); } } +}; + +const isTestnet = Boolean(Number(process.env.TESTNET_MODE ?? 1)); + +export const prepareEnvironment = (): Promise => Promise.all([ + configKarura(), + configAcala(), +]); + +const configKarura = async (): Promise => { + ensureEnvVars([ + 'KARURA_ETH_RPC', + 'KARURA_PRIVATE_KEY', + 'KARURA_NODE_URL', + ]); const addresses = isTestnet ? ADDRESSES.KARURA_TESTNET : ADDRESSES.KARURA; + const provider = new AcalaJsonRpcProvider(process.env.KARURA_ETH_RPC!); + const wallet = new Wallet(process.env.KARURA_PRIVATE_KEY!, provider); + const { substrateAddr, api } = await getApi( + process.env.KARURA_PRIVATE_KEY!, + process.env.KARURA_NODE_URL!, + ); + return { chainId: CHAIN_ID_KARURA, - ethRpc: process.env.KARURA_ETH_RPC!, - nodeUrl: process.env.KARURA_NODE_URL!, - walletPrivateKey: process.env.KARURA_PRIVATE_KEY!, + provider, + wallet, + api, + relayerSubstrateAddr: substrateAddr, isTestnet, ...addresses, }; -} +}; -function configAcala(): ChainConfig { - const requiredEnvVars = [ +const configAcala = async (): Promise => { + ensureEnvVars([ 'ACALA_ETH_RPC', 'ACALA_PRIVATE_KEY', 'ACALA_NODE_URL', - ]; - - for (const envVar of requiredEnvVars) { - if (!process.env[envVar]) { - console.error(`Missing environment variable ${envVar}`); - process.exit(1); - } - } + ]); const addresses = isTestnet ? ADDRESSES.ACALA_TESTNET : ADDRESSES.ACALA; + const provider = new AcalaJsonRpcProvider(process.env.ACALA_ETH_RPC!); + const wallet = new Wallet(process.env.ACALA_PRIVATE_KEY!, provider); + const { substrateAddr, api } = await getApi( + process.env.ACALA_PRIVATE_KEY!, + process.env.ACALA_NODE_URL!, + ); + return { chainId: CHAIN_ID_ACALA, - ethRpc: process.env.ACALA_ETH_RPC!, - nodeUrl: process.env.ACALA_NODE_URL!, - walletPrivateKey: process.env.ACALA_PRIVATE_KEY!, + provider, + wallet, + api, + relayerSubstrateAddr: substrateAddr, isTestnet, ...addresses, }; -} - -const env: RelayerEnvironment = prepareEnvironment(); +}; -// TODO: maybe export signer and api directly from here? -export const getChainConfig = (chainId: ChainId): ChainConfig | undefined => ( - env.supportedChains.find((x) => x.chainId === chainId) +const env = prepareEnvironment(); +export const getChainConfig = async (chainId: ROUTER_CHAIN_ID): Promise => ( + (await env).find((x) => x.chainId === chainId)! ); diff --git a/src/utils/relay.ts b/src/utils/relay.ts index 57f020f..5834a13 100644 --- a/src/utils/relay.ts +++ b/src/utils/relay.ts @@ -1,4 +1,4 @@ -import { BigNumber, ContractReceipt, Signer } from 'ethers'; +import { BigNumber, ContractReceipt } from 'ethers'; import { Bridge__factory } from '@certusone/wormhole-sdk/lib/cjs/ethers-contracts'; import { ERC20__factory, FeeRegistry__factory } from '@acala-network/asset-router/dist/typechain-types'; import { @@ -13,7 +13,6 @@ import { RELAYER_SUPPORTED_ADDRESSES_AND_THRESHOLDS } from '../consts'; import { RelayAndRouteParams } from './validate'; import { RelayError } from '../middlewares/error'; import { VaaInfo, parseVaaPayload } from './wormhole'; -import { getSigner } from './utils'; import { logger } from './logger'; interface ShouldRelayResult { @@ -75,10 +74,10 @@ const VAA_MAX_DECIMALS = 8; export const checkShouldRelayBeforeRouting = async ( params: RelayAndRouteParams, chainConfig: ChainConfig, - signer: Signer, ) => { - const tokenBridge = Bridge__factory.connect(chainConfig.tokenBridgeAddr, signer); - const feeRegistry = FeeRegistry__factory.connect(chainConfig.feeAddr, signer); + const { tokenBridgeAddr, feeAddr, wallet } = chainConfig; + const tokenBridge = Bridge__factory.connect(tokenBridgeAddr, wallet); + const feeRegistry = FeeRegistry__factory.connect(feeAddr, wallet); const vaaInfo = await parseVaaPayload(hexToUint8Array(params.signedVAA)); const { @@ -97,7 +96,7 @@ export const checkShouldRelayBeforeRouting = async ( throw new RelayError('unsupported token', { ...vaaInfo, amount: BigNumber.from(vaaAmount) }); } - const erc20 = ERC20__factory.connect(wrappedAddr, signer); + const erc20 = ERC20__factory.connect(wrappedAddr, wallet); const originDecimals = await erc20.decimals(); const realAmount = originDecimals <= VAA_MAX_DECIMALS ? vaaAmount @@ -112,11 +111,9 @@ export const relayEVM = async ( chainConfig: ChainConfig, signedVAA: string, ): Promise => { - const signer = await getSigner(chainConfig); - const receipt = await redeemOnEth( chainConfig.tokenBridgeAddr, - signer, + chainConfig.wallet, hexToUint8Array(signedVAA), ); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 715e86e..de7d68e 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,39 +1,34 @@ -import { AcalaJsonRpcProvider } from '@acala-network/eth-providers'; import { ApiPromise, Keyring, WsProvider } from '@polkadot/api'; -import { BigNumber, Contract, PopulatedTransaction, Signer, Wallet, ethers } from 'ethers'; +import { BigNumber, Contract, PopulatedTransaction, Wallet } from 'ethers'; import { CHAIN_ID_ACALA, CHAIN_ID_AVAX, CHAIN_ID_KARURA, CONTRACTS, hexToUint8Array } from '@certusone/wormhole-sdk'; import { DispatchError } from '@polkadot/types/interfaces'; import { ISubmittableResult } from '@polkadot/types/types'; import { JsonRpcProvider } from '@ethersproject/providers'; +import { SubmittableExtrinsic } from '@polkadot/api/types'; import { SubstrateSigner } from '@acala-network/bodhi'; import { options } from '@acala-network/api'; import { parseUnits } from 'ethers/lib/utils'; +// import { waitReady } from '@polkadot/wasm-crypto'; +import { cryptoWaitReady } from '@polkadot/util-crypto'; -import { ChainConfig } from './configureEnv'; import { RelayerError } from '../middlewares'; -import { SubmittableExtrinsic } from '@polkadot/api/types'; import { bridgeToken, getSignedVAAFromSequence } from './wormhole'; export type ROUTER_CHAIN_ID = typeof CHAIN_ID_KARURA | typeof CHAIN_ID_ACALA; -export const getSigner = async (config: ChainConfig): Promise => { - const provider = new AcalaJsonRpcProvider(config.ethRpc); - return new ethers.Wallet(config.walletPrivateKey, provider); -}; - -// TODO: reuse api -export const getApi = async (config: ChainConfig) => { +export const getApi = async (privateKey: string, nodeUrl: string) => { + await cryptoWaitReady(); const keyring = new Keyring({ type: 'sr25519' }); - const keyPair = keyring.addFromSeed(hexToUint8Array(config.walletPrivateKey)); - const addr = keyPair.address; + const keyPair = keyring.addFromSeed(hexToUint8Array(privateKey)); + const substrateAddr = keyPair.address; const api = await ApiPromise.create(options({ - provider: new WsProvider(config.nodeUrl), + provider: new WsProvider(nodeUrl), })); api.setSigner(new SubstrateSigner(api.registry, keyPair)); - return { addr, api }; + return { substrateAddr, api }; }; export const parseAmount = async (tokenAddr: string, amount: string, provider: any): Promise => {