diff --git a/.env.sample b/.env.sample index db53999..62902f1 100644 --- a/.env.sample +++ b/.env.sample @@ -12,12 +12,20 @@ SOLANA_SEND_RPCS="https://mainnet.block-engine.jito.wtf/api/v1/transactions,http AVALANCHE_RPC="https://1rpc.io/avax/c" ARBITRUM_RPC="https://arb1.arbitrum.io/rpc" BASE_RPC="https://mainnet.base.org" -BSC_RPC="https://rpc.ankr.com/bsc " +BSC_RPC="https://rpc.ankr.com/bsc" ETHEREUM_FLASHBOT_RPC="https://rpc.flashbots.net/fast" ETHEREUM_RPC="https://rpc.ankr.com/eth" OPTIMISM_RPC="https://mainnet.optimism.io" POLYGON_RPC="https://polygon-rpc.com/" +AVALANCHE_2ND_RPC="https://1rpc.io/avax/c" +ARBITRUM_2ND_RPC="https://arb1.arbitrum.io/rpc" +BASE_2ND_RPC="https://mainnet.base.org" +BSC_2ND_RPC="https://rpc.ankr.com/bsc" +ETHEREUM_2ND_RPC="https://rpc.ankr.com/eth" +OPTIMISM_2ND_RPC="https://mainnet.optimism.io" +POLYGON_2ND_RPC="https://polygon-rpc.com/" + DISABLE_UNLOCKER="false" BLACKLISTED_REFERRERS="" diff --git a/src/config/rpc.ts b/src/config/rpc.ts index afd25bd..cd76157 100644 --- a/src/config/rpc.ts +++ b/src/config/rpc.ts @@ -12,12 +12,19 @@ export type RpcConfig = { evmEndpoints: { ethereumFlashBot: string; ethereum: string; + ethereum2nd: string; bsc: string; + bsc2nd: string; polygon: string; + polygon2nd: string; avalanche: string; + avalanche2nd: string; arbitrum: string; + arbitrum2nd: string; optimism: string; + optimism2nd: string; base: string; + base2nd: string; }; jupV6Endpoint: string; oneInchApiKey: string; @@ -41,13 +48,20 @@ export const rpcConfig: RpcConfig = { }, evmEndpoints: { avalanche: process.env.AVALANCHE_RPC || 'https://1rpc.io/avax/c', + avalanche2nd: process.env.AVALANCHE_2ND_RPC || 'https://1rpc.io/avax/c', arbitrum: process.env.ARBITRUM_RPC || 'https://arb1.arbitrum.io/rpc', + arbitrum2nd: process.env.ARBITRUM_2ND_RPC || 'https://arb1.arbitrum.io/rpc', base: process.env.BASE_RPC || 'https://mainnet.base.org', + base2nd: process.env.BASE_2ND_RPC || 'https://mainnet.base.org', bsc: process.env.BSC_RPC || 'https://rpc.ankr.com/bsc ', + bsc2nd: process.env.BSC_2ND_RPC || 'https://rpc.ankr.com/bsc', ethereumFlashBot: process.env.ETHEREUM_FLASHBOT_RPC || 'https://rpc.flashbots.net/fast', ethereum: process.env.ETHEREUM_RPC || 'https://rpc.ankr.com/eth', + ethereum2nd: process.env.ETHEREUM_2ND_RPC || 'https://rpc.ankr.com/eth', optimism: process.env.OPTIMISM_RPC || 'https://mainnet.optimism.io', + optimism2nd: process.env.OPTIMISM_2ND_RPC || 'https://mainnet.optimism.io', polygon: process.env.POLYGON_RPC || 'https://polygon-rpc.com/', + polygon2nd: process.env.POLYGON_2ND_RPC || 'https://polygon-rpc.com/', }, jupV6Endpoint: process.env.JUP_V6_ENDPOINT || 'https://quote-api.jup.ag/v6', oneInchApiKey: process.env.ONE_INCH_API_KEY || '', diff --git a/src/index.ts b/src/index.ts index c8c1821..1a87f28 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,7 +25,7 @@ import { Unlocker } from './driver/unlocker'; import { WalletsHelper } from './driver/wallet-helper'; import { Relayer } from './relayer'; import { SimpleFulfillerConfig } from './simple'; -import { makeEvmProviders } from './utils/evm-providers'; +import { makeEvmProviders, makeSecondEvmProviders } from './utils/evm-providers'; import { FeeService } from './utils/fees'; import { ChainFinality } from './utils/finality'; import logger from './utils/logger'; @@ -71,6 +71,7 @@ export async function main() { }, 60_000); const evmProviders = makeEvmProviders(supportedChainIds, rpcConfig); + const secondaryEvmProviders = makeSecondEvmProviders(supportedChainIds, rpcConfig); const solanaConnection = new Connection(rpcConfig.solana.solanaMainRpc, { commitment: 'confirmed', }); @@ -155,7 +156,13 @@ export async function main() { tokenList, solanaTxSender, ); - const chainFinalitySvc = new ChainFinality(solanaConnection, contracts, rpcConfig, evmProviders); + const chainFinalitySvc = new ChainFinality( + solanaConnection, + contracts, + rpcConfig, + evmProviders, + secondaryEvmProviders, + ); const relayer = new Relayer( rpcConfig, mayanEndpoints, diff --git a/src/utils/evm-providers.ts b/src/utils/evm-providers.ts index 7fbf30f..0665151 100644 --- a/src/utils/evm-providers.ts +++ b/src/utils/evm-providers.ts @@ -1,4 +1,4 @@ -import { ethers } from "ethers6"; +import { ethers } from 'ethers6'; import { CHAIN_ID_ARBITRUM, CHAIN_ID_AVAX, @@ -7,8 +7,8 @@ import { CHAIN_ID_ETH, CHAIN_ID_OPTIMISM, CHAIN_ID_POLYGON, -} from "../config/chains"; -import { RpcConfig } from "../config/rpc"; +} from '../config/chains'; +import { RpcConfig } from '../config/rpc'; export type EvmProviders = { [evmNetworkId: number | string]: ethers.JsonRpcProvider }; @@ -49,3 +49,41 @@ export function makeEvmProviders(chainIds: number[], rpcConfig: RpcConfig): EvmP return result; } + +export function makeSecondEvmProviders(chainIds: number[], rpcConfig: RpcConfig): EvmProviders { + const result: { [key: number]: ethers.JsonRpcProvider } = {}; + + for (const chainId of chainIds) { + if (chainId === CHAIN_ID_BSC) { + result[chainId] = new ethers.JsonRpcProvider(rpcConfig.evmEndpoints.bsc2nd, 56, { + staticNetwork: ethers.Network.from(56), + }); + } else if (chainId === CHAIN_ID_POLYGON) { + result[chainId] = new ethers.JsonRpcProvider(rpcConfig.evmEndpoints.polygon2nd, 137, { + staticNetwork: ethers.Network.from(137), + }); + } else if (chainId === CHAIN_ID_ETH) { + result[chainId] = new ethers.JsonRpcProvider(rpcConfig.evmEndpoints.ethereum2nd, 1, { + staticNetwork: ethers.Network.from(1), + }); + } else if (chainId === CHAIN_ID_AVAX) { + result[chainId] = new ethers.JsonRpcProvider(rpcConfig.evmEndpoints.avalanche2nd, 43114, { + staticNetwork: ethers.Network.from(43114), + }); + } else if (chainId === CHAIN_ID_ARBITRUM) { + result[chainId] = new ethers.JsonRpcProvider(rpcConfig.evmEndpoints.arbitrum2nd, 42161, { + staticNetwork: ethers.Network.from(42161), + }); + } else if (chainId === CHAIN_ID_OPTIMISM) { + result[chainId] = new ethers.JsonRpcProvider(rpcConfig.evmEndpoints.optimism2nd, 10, { + staticNetwork: ethers.Network.from(10), + }); + } else if (chainId === CHAIN_ID_BASE) { + result[chainId] = new ethers.JsonRpcProvider(rpcConfig.evmEndpoints.base2nd, 8453, { + staticNetwork: ethers.Network.from(8453), + }); + } + } + + return result; +} diff --git a/src/utils/finality.ts b/src/utils/finality.ts index b800b3d..6bcfaac 100644 --- a/src/utils/finality.ts +++ b/src/utils/finality.ts @@ -24,12 +24,12 @@ export class ChainFinality { [chainId: number]: number; }; - private readonly finalizedBlocks = { - [CHAIN_ID_ETH]: 30, - [CHAIN_ID_BSC]: 60, - [CHAIN_ID_POLYGON]: 240, + private readonly finalizedBlocks: { [chainId: number]: number } = { + [CHAIN_ID_ETH]: 20, + [CHAIN_ID_BSC]: 8, + [CHAIN_ID_POLYGON]: 200, [CHAIN_ID_AVAX]: 2, - [CHAIN_ID_ARBITRUM]: 0.3, + [CHAIN_ID_ARBITRUM]: 2, [CHAIN_ID_OPTIMISM]: 2, [CHAIN_ID_BASE]: 2, }; @@ -42,11 +42,12 @@ export class ChainFinality { private readonly contracts: ContractsConfig, private readonly rpcConfig: RpcConfig, private readonly evmProviders: EvmProviders, + private readonly secondaryEvmProviders: EvmProviders, ) { this.blockGenerationTimeSecond = { [CHAIN_ID_ETH]: 13, [CHAIN_ID_BSC]: 3, - [CHAIN_ID_POLYGON]: 16, + [CHAIN_ID_POLYGON]: 2, [CHAIN_ID_AVAX]: 2, [CHAIN_ID_ARBITRUM]: 100, [CHAIN_ID_OPTIMISM]: 3, @@ -55,8 +56,8 @@ export class ChainFinality { this.minimumBlocksToFinality = { [CHAIN_ID_ETH]: 1, - [CHAIN_ID_BSC]: 1, - [CHAIN_ID_POLYGON]: 4, + [CHAIN_ID_BSC]: 3, + [CHAIN_ID_POLYGON]: 16, [CHAIN_ID_AVAX]: 1, [CHAIN_ID_ARBITRUM]: 1, [CHAIN_ID_OPTIMISM]: 1, @@ -72,11 +73,22 @@ export class ChainFinality { const timeToFinalize = await this.timeToFinalizeSeconds(chainId, sourceTxHash, swapValueUsd); if (timeToFinalize <= 0) { - const tx = await this.evmProviders[chainId].getTransactionReceipt(sourceTxHash); - if (!tx || tx.status === 0) { - throw new Error('Transaction not found or has error in waiting for chain finality'); + if ([CHAIN_ID_ETH, CHAIN_ID_POLYGON].includes(chainId)) { + const [tx, tx2] = await Promise.all([ + await this.evmProviders[chainId].getTransactionReceipt(sourceTxHash), + await this.secondaryEvmProviders[chainId].getTransactionReceipt(sourceTxHash), + ]); + if (!tx || tx.status !== 1 || !tx2 || tx2.status !== 1) { + throw new Error('Transaction (eth)not found or has error in waiting for chain finality'); + } + return; + } else { + const tx = await this.evmProviders[chainId].getTransactionReceipt(sourceTxHash); + if (!tx || tx.status !== 1) { + throw new Error('Transaction not found or has error in waiting for chain finality'); + } + return; } - return; } await delay(timeToFinalize * 1000); @@ -109,14 +121,6 @@ export class ChainFinality { } } - private async getEvmCurrentFinalizedBlockNumber( - provider: ethers.JsonRpcProvider, - wChainId: number, - ): Promise { - const resOpt = await provider.send('eth_getBlockByNumber', ['latest', false]); - return parseInt(resOpt.number) - this.blockGenerationTimeSecond[wChainId]; - } - private async getEvmLatestBlockNumber(provider: ethers.JsonRpcProvider): Promise { const resOpt = await provider.send('eth_getBlockByNumber', ['latest', false]); return parseInt(resOpt.number); @@ -128,13 +132,7 @@ export class ChainFinality { throw new Error('Transaction not found in timeToFinalizeSeconds'); } - const finalizedBlockNumber = await this.getEvmCurrentFinalizedBlockNumber( - this.evmProviders[wChainId], - wChainId, - ); - const lastBlockNumber = await this.getEvmLatestBlockNumber(this.evmProviders[wChainId]); - - const blockCountToFinalize = lastBlockNumber - finalizedBlockNumber; + const finalizedBlockNumber = tx.blockNumber! + this.finalizedBlocks[wChainId]; let safeBlockForDriver = finalizedBlockNumber; @@ -144,17 +142,231 @@ export class ChainFinality { const factor = (swapValueUsd - this.minSwapValueUsd) / (this.maxSwapValueUsd - this.minSwapValueUsd); const blocksToSemiFinalize = this.minimumBlocksToFinality[wChainId] + - (blockCountToFinalize - this.minimumBlocksToFinality[wChainId]) * factor; + (this.finalizedBlocks[wChainId] - this.minimumBlocksToFinality[wChainId]) * factor; safeBlockForDriver = tx.blockNumber! + blocksToSemiFinalize; } - if (lastBlockNumber >= safeBlockForDriver) { - return 0; - } - - const remainingBlocks = safeBlockForDriver - tx.blockNumber!; + const lastBlockNumber = await this.getEvmLatestBlockNumber(this.evmProviders[wChainId]); + const remainingBlocks = safeBlockForDriver - lastBlockNumber; // every tx is polled at most 10 times so rpc usage is controlled return (remainingBlocks * this.blockGenerationTimeSecond[wChainId]) / 10; } } + +// class SwapVerifier { +// private readonly swiftInterface = new ethers.Interface(SwiftAbi); +// constructor() {} + +// async parseEvmSwiftOrderHash(evmProvider: ethers.JsonRpcProvider, txHash: string): Promise { +// const txReceipt = await evmProvider.getTransactionReceipt(txHash); +// if (txReceipt?.status !== 1) { +// throw new Error(`Failed creation transaction ${txHash}`); +// } + +// for (let log of txReceipt?.logs || []) { +// if (log.topics.includes(ethers.id('OrderCreated(bytes32)'))) { +// const createLog = this.swiftInterface.decodeEventLog('OrderCreated(bytes32)', log.data, log.topics); +// const orderHash = createLog.key; +// return orderHash; +// } +// } + +// throw new Error('OrderCreated event not found'); +// } + +// async parseSolanaSwift() { + +// } + +// async parseAndCreateInitOrder(sig: string, trx: ParsedTransactionWithMeta, parsedData: Instruction, instruction: PartiallyDecodedInstruction) { +// if (parsedData.name !== 'initOrder') { +// throw new Error('parsedData.name must be initOrder'); +// } + +// let forwardedTokenAddress: string = null; +// let forwardedFromAmount: string = null; +// let forwardedFromSymbol: string = null; +// for (let ix of trx.transaction.message.instructions) { +// if (ix.programId.toString() === this.providersConfig.JUP_V6_PROGRAM_ID) { +// const parsedJupAmount = await this.jupParser.extractJupSwapOriginalInput(trx, sig); +// if (parsedJupAmount) { +// forwardedTokenAddress = parsedJupAmount.inMint; // todo check if available in our tokens +// forwardedFromSymbol = parsedJupAmount.inSymbol; +// let token = await getTokenDataGeneral( +// CHAIN_ID_SOLANA, +// forwardedTokenAddress, +// ); +// if (!token) { +// forwardedTokenAddress = null; +// } else { +// forwardedFromAmount = ethers.utils.formatUnits( +// parsedJupAmount.inAmount, +// token.decimals, +// ); +// } +// } +// } +// } + +// const { +// amountInMin, // BN +// nativeInput, // boolean +// feeSubmit, // BN +// addrDest, // bytes32. js array of number +// chainDest, // u8 js number +// tokenOut, // bytes32. js array of number +// amountOutMin, // BN +// gasDrop, // BN +// feeCancel, // BN +// feeRefund, // BN +// deadline, // BN +// addrRef, // bytes32. js array of number +// feeRateRef, // u8 js number +// feeRateMayan, // u8 js number +// auctionMode, // u8 js number +// keyRnd, // bytes32. js array of number +// } = (parsedData.data as any).params; + +// const trader = instruction.accounts[0].toString(); +// const stateAddr = instruction.accounts[2].toString(); +// const stateFromAcc = instruction.accounts[3]; +// const stateFromAccIdx = trx.transaction.message.accountKeys.findIndex(acc => acc.pubkey.equals(stateFromAcc)); +// const mintFrom = instruction.accounts[5].toString(); + +// let fromAmount: bigint = null; +// for (let log of this.eventParser.parseLogs(trx.meta.logMessages, false)) { +// if (log.name === 'OrderInitialized') { +// fromAmount = BigInt(log.data.amountIn as any); +// } +// } + +// if (!fromAmount) { +// const statePostBalance = trx.meta.postTokenBalances.find((tok) => tok.accountIndex === stateFromAccIdx); +// const statePreBalance = trx.meta.preTokenBalances.find((tok) => tok.accountIndex === stateFromAccIdx); + +// if (!statePostBalance) { +// throw new Error(`fromAmount not found for sig ${sig}`); +// } +// const postAmount64 = BigInt(statePostBalance.uiTokenAmount.amount); +// const preAmount64 = BigInt(statePreBalance?.uiTokenAmount?.amount || '0'); +// fromAmount = postAmount64 - preAmount64; +// } + +// const randomKey = '0x' + Buffer.from(keyRnd).toString('hex'); + +// const fromToken = nativeInput ? NativeTokens[CHAIN_ID_SOLANA] : (await getTokenDataGeneral(CHAIN_ID_SOLANA, mintFrom)); +// const toNativeToken = NativeTokens[chainDest]; +// const destTokenAddress = tryUint8ArrayToNative(Uint8Array.from(tokenOut), chainDest); +// const toToken = (await getTokenDataGeneral(chainDest, destTokenAddress)); +// const referrerAddress = tryUint8ArrayToNative(Uint8Array.from(addrRef), chainDest); +// const destAddress = tryUint8ArrayToNative(Uint8Array.from(addrDest), chainDest); +// if (!forwardedFromAmount) { +// forwardedTokenAddress = fromToken.contract; +// forwardedFromAmount = ethers.utils.formatUnits(fromAmount + BigInt(feeSubmit), fromToken.decimals); +// } + +// const orderHash = reconstructOrderHash( +// trader, +// CHAIN_ID_SOLANA, +// fromToken.contract, +// fromToken.decimals, +// chainDest, +// toToken.contract, +// toToken.decimals, +// BigInt(amountOutMin), +// BigInt(gasDrop), +// BigInt(feeCancel), +// BigInt(feeRefund), +// deadline, +// destAddress, +// referrerAddress, +// feeRateRef, +// feeRateMayan, +// auctionMode, +// randomKey, +// ); + +// const calculatedState = getSwiftStateAddrSrc(instruction.programId, orderHash); + +// if (calculatedState.toString() !== stateAddr) { +// throw new Error(`calculated state ${calculatedState.toString()} not equal to stateAddr ${stateAddr} for sig ${sig}`); +// } + +// const newswap = await this.swapService.createSwap({ +// id: uuidv4(), +// trader: trader, +// sourceTxBlockNo: trx.slot, +// sourceTxHash: sig, +// status: SWAP_STATUS.ORDER_CREATED, +// orderHash: '0x' + orderHash.toString('hex'), +// randomKey: randomKey, +// payloadId: null, +// statusUpdatedAt: new Date(trx.blockTime * 1000), +// deadline: new Date(Number(deadline) * 1000), +// sourceChain: CHAIN_ID_SOLANA.toString(), +// swapChain: chainDest.toString(), +// fromTokenAddress: fromToken.contract, +// fromTokenChain: fromToken.wChainId.toString(), +// fromTokenSymbol: fromToken.symbol, + +// auctionMode: auctionMode, + +// fromAmount: ethers.utils.formatUnits(fromAmount.toString(), fromToken.decimals), +// fromAmount64: fromAmount.toString(), +// forwardedFromAmount: forwardedFromAmount, +// forwardedTokenAddress: forwardedTokenAddress, +// forwardedTokenSymbol: forwardedFromSymbol, + +// toTokenChain: toToken.wChainId.toString(), +// toTokenAddress: toToken.contract, +// toTokenSymbol: toToken.symbol, +// destChain: chainDest.toString(), +// destAddress: destAddress, +// bridgeFee: 0, + +// submissionRelayerFee: ethers.utils.formatUnits( +// feeSubmit.toString(), +// Math.min(WORMHOLE_DECIMALS, fromToken.decimals), +// ), +// redeemRelayerFee: ethers.utils.formatUnits( +// feeCancel.toString(), +// Math.min(WORMHOLE_DECIMALS, fromToken.decimals), +// ), +// refundRelayerFee: ethers.utils.formatUnits( +// feeRefund.toString(), +// Math.min(WORMHOLE_DECIMALS, fromToken.decimals), +// ), +// auctionAddress: this.swiftConfig.auctionAddr, + +// mayanAddress: instruction.programId.toString(), +// posAddress: instruction.programId.toString(), +// referrerBps: feeRateRef, +// mayanBps: feeRateMayan, +// referrerAddress: referrerAddress, + +// stateAddr: stateAddr, +// auctionStateAddr: PublicKey.findProgramAddressSync( +// [Buffer.from('AUCTION'), orderHash], +// this.auction, +// )[0].toString(), + +// minAmountOut: ethers.utils.formatUnits( +// amountOutMin.toString(), +// Math.min(WORMHOLE_DECIMALS, toToken.decimals), +// ), +// minAmountOut64: amountOutMin.toString(), + +// gasDrop: ethers.utils.formatUnits( +// gasDrop.toString(), +// Math.min(WORMHOLE_DECIMALS, toNativeToken.decimals), +// ), +// gasDrop64: gasDrop.toString(), + +// service: SERVICE_TYPE.SWIFT_SWAP, +// savedAt: new Date(), +// initiatedAt: new Date(trx.blockTime * 1000), +// }); +// return newswap; +// } +// }