diff --git a/src/handler/index.ts b/src/handler/index.ts index b2ab15f..3fb511c 100644 --- a/src/handler/index.ts +++ b/src/handler/index.ts @@ -1,2 +1,2 @@ -export { listenEvents } from "./lock-listener"; -export { listenStakeEvents } from "./stake-listener"; +export { listenEvents } from "./lock-listener/lock-listener"; +export { listenStakeEvents } from "./stake-listener/stake-listener"; diff --git a/src/handler/lock-listener/index.ts b/src/handler/lock-listener/index.ts new file mode 100644 index 0000000..6f52f22 --- /dev/null +++ b/src/handler/lock-listener/index.ts @@ -0,0 +1,2 @@ +export * from "./lock-listener"; +export * from "./process-fail-safe"; diff --git a/src/handler/lock-listener.ts b/src/handler/lock-listener/lock-listener.ts similarity index 87% rename from src/handler/lock-listener.ts rename to src/handler/lock-listener/lock-listener.ts index 979a6aa..650e89b 100644 --- a/src/handler/lock-listener.ts +++ b/src/handler/lock-listener/lock-listener.ts @@ -4,17 +4,18 @@ import { Mutex } from "async-mutex"; import type { AxiosInstance } from "axios"; import axios from "axios"; import type { JsonRpcProvider } from "ethers"; -import type { TSupportedChains } from "../config"; -import type { BridgeStorage } from "../contractsTypes/evm"; -import { LockedEvent } from "../persistence/entities/locked"; -import { eventBuilder } from "./event-builder"; +import type { TSupportedChains } from "../../config"; +import type { BridgeStorage } from "../../contractsTypes/evm"; +import { LockedEvent } from "../../persistence/entities/locked"; +import { eventBuilder } from "../event-builder"; import type { LockEvent, LogInstance, THandler, TNftTransferDetailsObject, -} from "./types"; -import { fetchHttpOrIpfs, retry, useMutexAndRelease } from "./utils"; +} from "../types"; +import { fetchHttpOrIpfs, retry, useMutexAndRelease } from "../utils"; +import { processEventsFailSafe } from "./process-fail-safe"; export async function listenEvents( chains: Array, @@ -46,14 +47,14 @@ export async function listenEvents( const sourceChain = map.get(ev.sourceChain as TSupportedChains); if (!sourceChain) { log.warn( - `Unsupported src chain: ${sourceChain} for ${ev.transactionHash}`, + `Unsupported src chain: ${ev.sourceChain} for ${ev.transactionHash} on ${ev.listenerChain}`, ); return; } const destinationChain = map.get(ev.destinationChain as TSupportedChains); if (!destinationChain) { log.warn( - `Unsupported dest chain: ${destinationChain} for ${ev.transactionHash} ${destinationChain} ${ev.destinationChain}`, + `Unsupported dest chain: ${ev.destinationChain} for ${ev.transactionHash} on ${ev.listenerChain}`, ); return; } @@ -234,22 +235,3 @@ export async function listenEvents( serverLinkHandler === undefined ? poolEvents(chain) : pollEvents(chain); } } - -const processEventsFailSafe = async ( - chain: THandler, - ev: LockEvent, - log: LogInstance, - processEvent: (chain: THandler, ev: LockEvent) => Promise, -) => { - let success = false; - while (!success) { - try { - await processEvent(chain, ev); - success = true; - } catch (e) { - log.error("Error processing poll events", ev, e); - log.info("Awaiting 2s"); - await setTimeout(2 * 1000); - } - } -}; diff --git a/src/handler/lock-listener/process-fail-safe.ts b/src/handler/lock-listener/process-fail-safe.ts new file mode 100644 index 0000000..701f6c3 --- /dev/null +++ b/src/handler/lock-listener/process-fail-safe.ts @@ -0,0 +1,22 @@ +import { setTimeout } from "node:timers/promises"; +import type { LockEvent } from "../types"; +import type { LogInstance, THandler } from "../types"; + +export const processEventsFailSafe = async ( + chain: THandler, + ev: LockEvent, + log: LogInstance, + processEvent: (chain: THandler, ev: LockEvent) => Promise, +) => { + let success = false; + while (!success) { + try { + await processEvent(chain, ev); + success = true; + } catch (e) { + log.error("Error processing poll events", ev, e); + log.info("Awaiting 2s"); + await setTimeout(2 * 1000); + } + } +}; diff --git a/src/handler/stake-listener/index.ts b/src/handler/stake-listener/index.ts new file mode 100644 index 0000000..b30ae92 --- /dev/null +++ b/src/handler/stake-listener/index.ts @@ -0,0 +1,2 @@ +export * from "./stake-listener"; +export * from "./stake-tokens"; diff --git a/src/handler/stake-listener.ts b/src/handler/stake-listener/stake-listener.ts similarity index 90% rename from src/handler/stake-listener.ts rename to src/handler/stake-listener/stake-listener.ts index 6de087d..24bdeee 100644 --- a/src/handler/stake-listener.ts +++ b/src/handler/stake-listener/stake-listener.ts @@ -1,9 +1,9 @@ import type { EntityManager } from "@mikro-orm/sqlite"; -import type { TSupportedChainTypes } from "../config"; -import type { BridgeStorage } from "../contractsTypes/evm"; -import { eventBuilder } from "./event-builder"; -import type { LogInstance, THandler, TStakingHandler } from "./types"; -import { retry } from "./utils"; +import type { TSupportedChainTypes } from "../../config"; +import type { BridgeStorage } from "../../contractsTypes/evm"; +import { eventBuilder } from "../event-builder"; +import type { LogInstance, THandler, TStakingHandler } from "../types"; +import { retry } from "../utils"; export async function listenStakeEvents( chains: Array, diff --git a/src/handler/stake-listener/stake-tokens.ts b/src/handler/stake-listener/stake-tokens.ts new file mode 100644 index 0000000..7d14f06 --- /dev/null +++ b/src/handler/stake-listener/stake-tokens.ts @@ -0,0 +1,59 @@ +import { JsonRpcProvider, Wallet } from "ethers"; +import { + ERC20Staking__factory, + ERC20__factory, +} from "../../contractsTypes/evm"; +import type { IGeneratedWallets, IStakingConfig } from "../../types"; +import type { LogInstance, THandler } from "../types"; + +export async function stakeTokens( + conf: IStakingConfig, + secrets: IGeneratedWallets, + chains: THandler[], + logger: LogInstance, +) { + const others = chains.filter((e) => e.chainType !== "evm"); + const provider = new JsonRpcProvider(conf.rpcURL); + const signer = new Wallet(secrets.evmWallet.privateKey, provider); + const staker = ERC20Staking__factory.connect(conf.contractAddress, signer); + const token = ERC20__factory.connect(conf.coinAddress, signer); + const staked = await staker.stakingBalances(secrets.evmWallet.address); + if (staked > 0n) { + logger.info( + `Already staked ${staked} ${conf.coinSymbol} in contract ${conf.contractAddress}`, + ); + return; + } + const amtToStake = await staker.stakingAmount(); + logger.info("Awaiting completion of approve transaction."); + + const approve = await ( + await token.approve(conf.contractAddress, amtToStake * amtToStake) + ).wait(); + + logger.info("Approved to stake: ✅"); + if (!approve || approve.status !== 1) { + throw new Error("Failed to approve staking"); + } + + const data = [ + { + validatorAddress: secrets.evmWallet.address, + chainType: "evm", + }, + ...others.map((e) => { + return { + validatorAddress: e.publicKey, + chainType: e.chainType, + }; + }), + ]; + + logger.info("Awaiting completion of stake transaction."); + const staking = await (await staker.stakeERC20(data)).wait(); + logger.info("Stake complete: ✅"); + + if (!staking || staking.status !== 1) { + throw new Error("Failed to stake"); + } +} diff --git a/src/handler/types.ts b/src/handler/types.ts deleted file mode 100644 index f367857..0000000 --- a/src/handler/types.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { Logger } from "tslog"; -import type { TSupportedChainTypes, TSupportedChains } from "../config"; -import type { EventBuilder } from "./event-builder"; - -export type TNftData = { - name: string; - symbol: string; - metadata: string; - royalty: bigint; -}; - -export type TNftTransferDetailsObject = { - tokenId: string; - sourceChain: string; - destinationChain: string; - destinationUserAddress: string; - sourceNftContractAddress: string; - name: string; - symbol: string; - royalty: string; - royaltyReceiver: string; - metadata: string; - transactionHash: string; - tokenAmount: string; - nftType: string; - fee: string; - lockTxChain: string; - imgUri?: string; -}; - -export type LockEventIter = (event: LockEvent) => Promise; -export type StakeEventIter = (event: StakeEvent) => Promise; - -export interface THandler { - addSelfAsValidator(): Promise<"success" | "failure">; - listenForLockEvents(builder: EventBuilder, cb: LockEventIter): Promise; - pollForLockEvents(builder: EventBuilder, cb: LockEventIter): Promise; - signClaimData( - nfto: TNftTransferDetailsObject, - ): Promise<{ signer: string; signature: string }>; - signData(buf: string): Promise<{ signer: string; signature: string }>; - nftData( - tokenId: string, - contract: string, - logger: LogInstance, - ): Promise; - validateNftData( - data: TNftData, - ): { valid: false; reason: string } | { valid: true }; - chainIdent: TSupportedChains; - selfIsValidator(): Promise; - getBalance(): Promise; - initialFunds: bigint; - currency: string; - address: string; - chainType: TSupportedChainTypes; - publicKey: string; - decimals: bigint; -} - -export interface TStakingHandler { - listenForStakingEvents( - builder: EventBuilder, - cb: StakeEventIter, - ): Promise; -} - -export type LockEvent = { - listenerChain: string; - tokenId: string; - destinationChain: string; - destinationUserAddress: string; - sourceNftContractAddress: string; - tokenAmount: string; - nftType: string; - sourceChain: string; - transactionHash: string; - metaDataUri: string; -}; - -export type StakeEvent = { - validatorAddress: string; - caller: string; - chainType: TSupportedChainTypes; -}[]; - -export type LogInstance = Logger; diff --git a/src/handler/types/event-iterators.ts b/src/handler/types/event-iterators.ts new file mode 100644 index 0000000..04cf72c --- /dev/null +++ b/src/handler/types/event-iterators.ts @@ -0,0 +1,4 @@ +import type { LockEvent, StakeEvent } from "."; + +export type LockEventIter = (event: LockEvent) => Promise; +export type StakeEventIter = (event: StakeEvent) => Promise; diff --git a/src/handler/types/index.ts b/src/handler/types/index.ts new file mode 100644 index 0000000..5d2aa83 --- /dev/null +++ b/src/handler/types/index.ts @@ -0,0 +1,9 @@ +export * from "./lock-event"; +export * from "./stake-event"; +export * from "./lock-handler"; +export * from "./stake-handler"; +export * from "./event-iterators"; +export * from "./lock-handler"; +export * from "./nft-data"; +export * from "./nft-transfer-details-object"; +export * from "./logger"; diff --git a/src/handler/types/lock-event.ts b/src/handler/types/lock-event.ts new file mode 100644 index 0000000..157fd63 --- /dev/null +++ b/src/handler/types/lock-event.ts @@ -0,0 +1,12 @@ +export type LockEvent = { + listenerChain: string; + tokenId: string; + destinationChain: string; + destinationUserAddress: string; + sourceNftContractAddress: string; + tokenAmount: string; + nftType: string; + sourceChain: string; + transactionHash: string; + metaDataUri: string; +}; diff --git a/src/handler/types/lock-handler.ts b/src/handler/types/lock-handler.ts new file mode 100644 index 0000000..2001a57 --- /dev/null +++ b/src/handler/types/lock-handler.ts @@ -0,0 +1,31 @@ +import type { TNftTransferDetailsObject } from "."; +import type { LockEventIter, LogInstance, TNftData } from "."; +import type { TSupportedChainTypes, TSupportedChains } from "../../config"; +import type { EventBuilder } from "../event-builder"; + +export interface THandler { + addSelfAsValidator(): Promise<"success" | "failure">; + listenForLockEvents(builder: EventBuilder, cb: LockEventIter): Promise; + pollForLockEvents(builder: EventBuilder, cb: LockEventIter): Promise; + signClaimData( + nfto: TNftTransferDetailsObject, + ): Promise<{ signer: string; signature: string }>; + signData(buf: string): Promise<{ signer: string; signature: string }>; + nftData( + tokenId: string, + contract: string, + logger: LogInstance, + ): Promise; + validateNftData( + data: TNftData, + ): { valid: false; reason: string } | { valid: true }; + chainIdent: TSupportedChains; + selfIsValidator(): Promise; + getBalance(): Promise; + initialFunds: bigint; + currency: string; + address: string; + chainType: TSupportedChainTypes; + publicKey: string; + decimals: bigint; +} diff --git a/src/handler/types/logger.ts b/src/handler/types/logger.ts new file mode 100644 index 0000000..8040b53 --- /dev/null +++ b/src/handler/types/logger.ts @@ -0,0 +1,3 @@ +import type { Logger } from "tslog"; + +export type LogInstance = Logger; diff --git a/src/handler/types/nft-data.ts b/src/handler/types/nft-data.ts new file mode 100644 index 0000000..3b31206 --- /dev/null +++ b/src/handler/types/nft-data.ts @@ -0,0 +1,6 @@ +export type TNftData = { + name: string; + symbol: string; + metadata: string; + royalty: bigint; +}; diff --git a/src/handler/types/nft-transfer-details-object.ts b/src/handler/types/nft-transfer-details-object.ts new file mode 100644 index 0000000..7b044c7 --- /dev/null +++ b/src/handler/types/nft-transfer-details-object.ts @@ -0,0 +1,18 @@ +export type TNftTransferDetailsObject = { + tokenId: string; + sourceChain: string; + destinationChain: string; + destinationUserAddress: string; + sourceNftContractAddress: string; + name: string; + symbol: string; + royalty: string; + royaltyReceiver: string; + metadata: string; + transactionHash: string; + tokenAmount: string; + nftType: string; + fee: string; + lockTxChain: string; + imgUri?: string; +}; diff --git a/src/handler/types/stake-event.ts b/src/handler/types/stake-event.ts new file mode 100644 index 0000000..255cefd --- /dev/null +++ b/src/handler/types/stake-event.ts @@ -0,0 +1,7 @@ +import type { TSupportedChainTypes } from "../../config"; + +export type StakeEvent = { + validatorAddress: string; + caller: string; + chainType: TSupportedChainTypes; +}[]; diff --git a/src/handler/types/stake-handler.ts b/src/handler/types/stake-handler.ts new file mode 100644 index 0000000..ed0558d --- /dev/null +++ b/src/handler/types/stake-handler.ts @@ -0,0 +1,9 @@ +import type { EventBuilder } from "../event-builder"; +import type { StakeEventIter } from "./event-iterators"; + +export interface TStakingHandler { + listenForStakingEvents( + builder: EventBuilder, + cb: StakeEventIter, + ): Promise; +} diff --git a/src/handler/utils.ts b/src/handler/utils.ts deleted file mode 100644 index c844822..0000000 --- a/src/handler/utils.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { setTimeout } from "node:timers/promises"; -import type { AxiosInstance } from "axios"; -import { JsonRpcProvider, Wallet } from "ethers"; -import { ERC20Staking__factory, ERC20__factory } from "../contractsTypes/evm"; -import type { IGeneratedWallets, IStakingConfig } from "../types"; -import type { LogInstance, THandler } from "./types"; - -export const confirmationCountNeeded = (validatorCount: number) => { - const twoByThree = 0.666666667; - const paddedValidatorCount = 1; - return Math.floor(twoByThree * validatorCount) + paddedValidatorCount; -}; - -export const ProcessDelayMilliseconds = 5000; - -export function waitForMSWithMsg(ms: number, msg: string): Promise { - const secondsInMilliSeconds = 1000; - const numberOfDecimals = 2; - console.info( - `${msg}, retrying in ${(ms / secondsInMilliSeconds).toFixed( - numberOfDecimals, - )} seconds`, - ); - return setTimeout(ms); -} - -export async function checkOrAddSelfAsVal( - chains: THandler[], - log: LogInstance, -) { - for (const chain of chains) { - const selfIsValidator = await chain.selfIsValidator(); - if (!selfIsValidator) { - const added = await chain.addSelfAsValidator(); - if (added === "failure") { - throw new Error( - `Failed to add self as validator for chain ${chain.chainIdent}`, - ); - } - } else log.info(`Validator is already added to ${chain.chainIdent}`); - } -} - -export async function retry( - func: () => Promise, - ctx: string, - log: LogInstance, - retryCount?: number, -): Promise { - let count = retryCount; - while (true) { - try { - log.trace(`Context: ${ctx} - Retrying:`); - const res = await func(); - log.trace("RESULT", res); - return res; // Only returns once the function succeeds - } catch (err) { - log.error(`Context: ${ctx} - Retrying. Error:`, err); - // Use a Promise-based delay - if (count) { - count = count - 1; - if (count <= 0) { - throw new Error(`Failed ${ctx}`); - } - } - await setTimeout(5000); - } - } -} - -export async function stakeTokens( - conf: IStakingConfig, - secrets: IGeneratedWallets, - chains: THandler[], - logger: LogInstance, -) { - const others = chains.filter((e) => e.chainType !== "evm"); - const provider = new JsonRpcProvider(conf.rpcURL); - const signer = new Wallet(secrets.evmWallet.privateKey, provider); - const staker = ERC20Staking__factory.connect(conf.contractAddress, signer); - const token = ERC20__factory.connect(conf.coinAddress, signer); - const staked = await staker.stakingBalances(secrets.evmWallet.address); - if (staked > 0n) { - logger.info( - `Already staked ${staked} ${conf.coinSymbol} in contract ${conf.contractAddress}`, - ); - return; - } - const amtToStake = await staker.stakingAmount(); - logger.info("Awaiting completion of approve transaction."); - - const approve = await ( - await token.approve(conf.contractAddress, amtToStake * amtToStake) - ).wait(); - - logger.info("Approved to stake: ✅"); - if (!approve || approve.status !== 1) { - throw new Error("Failed to approve staking"); - } - - const data = [ - { - validatorAddress: secrets.evmWallet.address, - chainType: "evm", - }, - ...others.map((e) => { - return { - validatorAddress: e.publicKey, - chainType: e.chainType, - }; - }), - ]; - - logger.info("Awaiting completion of stake transaction."); - const staking = await (await staker.stakeERC20(data)).wait(); - logger.info("Stake complete: ✅"); - - if (!staking || staking.status !== 1) { - throw new Error("Failed to stake"); - } -} - -export async function fetchHttpOrIpfs(uri: string, http: AxiosInstance) { - const url = new URL(uri); - if (url.protocol === "http:" || url.protocol === "https:") { - const response = await http.get(uri); - return response.data; - } - if (url.protocol === "ipfs:") { - try { - return ( - await http.get(`https://ipfs.io/ipfs/${uri.replace("ipfs://", "")}`) - ).data; - } catch (ex) { - return ( - await http.get( - `https://xpnetwork.infura-ipfs.io/ipfs/${uri.replace("ipfs://", "")}`, - ) - ).data; - } - } - throw new Error("Unsupported protocol"); -} - -export async function useMutexAndRelease( - lock: () => Promise void]>, - func: (t: Lock) => Promise, -) { - const [resource, release] = await lock(); - try { - const res = await func(resource); - return res; - } finally { - release(); - } -} diff --git a/src/handler/utils/check-or-add-self-as-validator.ts b/src/handler/utils/check-or-add-self-as-validator.ts new file mode 100644 index 0000000..70694da --- /dev/null +++ b/src/handler/utils/check-or-add-self-as-validator.ts @@ -0,0 +1,18 @@ +import type { LogInstance, THandler } from "../types"; + +export async function checkOrAddSelfAsVal( + chains: THandler[], + log: LogInstance, +) { + for (const chain of chains) { + const selfIsValidator = await chain.selfIsValidator(); + if (!selfIsValidator) { + const added = await chain.addSelfAsValidator(); + if (added === "failure") { + throw new Error( + `Failed to add self as validator for chain ${chain.chainIdent}`, + ); + } + } else log.info(`Validator is already added to ${chain.chainIdent}`); + } +} diff --git a/src/handler/utils/fetch-http-or-ipfs.ts b/src/handler/utils/fetch-http-or-ipfs.ts new file mode 100644 index 0000000..5ee0a2f --- /dev/null +++ b/src/handler/utils/fetch-http-or-ipfs.ts @@ -0,0 +1,23 @@ +import type { AxiosInstance } from "axios"; + +export async function fetchHttpOrIpfs(uri: string, http: AxiosInstance) { + const url = new URL(uri); + if (url.protocol === "http:" || url.protocol === "https:") { + const response = await http.get(uri); + return response.data; + } + if (url.protocol === "ipfs:") { + try { + return ( + await http.get(`https://ipfs.io/ipfs/${uri.replace("ipfs://", "")}`) + ).data; + } catch (ex) { + return ( + await http.get( + `https://xpnetwork.infura-ipfs.io/ipfs/${uri.replace("ipfs://", "")}`, + ) + ).data; + } + } + throw new Error("Unsupported protocol"); +} diff --git a/src/handler/utils/index.ts b/src/handler/utils/index.ts new file mode 100644 index 0000000..0ea19ab --- /dev/null +++ b/src/handler/utils/index.ts @@ -0,0 +1,5 @@ +export * from "./fetch-http-or-ipfs"; +export * from "./retry"; +export * from "./check-or-add-self-as-validator"; +export * from "./useMutex"; +export * from "./wait"; diff --git a/src/handler/utils/retry.ts b/src/handler/utils/retry.ts new file mode 100644 index 0000000..bf10924 --- /dev/null +++ b/src/handler/utils/retry.ts @@ -0,0 +1,29 @@ +import { setTimeout } from "node:timers/promises"; +import type { LogInstance } from "../types"; + +export async function retry( + func: () => Promise, + ctx: string, + log: LogInstance, + retryCount?: number, +): Promise { + let count = retryCount; + while (true) { + try { + log.trace(`Context: ${ctx} - Retrying:`); + const res = await func(); + log.trace("RESULT", res); + return res; // Only returns once the function succeeds + } catch (err) { + log.error(`Context: ${ctx} - Retrying. Error:`, err); + // Use a Promise-based delay + if (count) { + count = count - 1; + if (count <= 0) { + throw new Error(`Failed ${ctx}`); + } + } + await setTimeout(5000); + } + } +} diff --git a/src/handler/utils/useMutex.ts b/src/handler/utils/useMutex.ts new file mode 100644 index 0000000..1cc680b --- /dev/null +++ b/src/handler/utils/useMutex.ts @@ -0,0 +1,12 @@ +export async function useMutexAndRelease( + lock: () => Promise void]>, + func: (t: Lock) => Promise, +) { + const [resource, release] = await lock(); + try { + const res = await func(resource); + return res; + } finally { + release(); + } +} diff --git a/src/handler/utils/wait.ts b/src/handler/utils/wait.ts new file mode 100644 index 0000000..8cdc7b9 --- /dev/null +++ b/src/handler/utils/wait.ts @@ -0,0 +1,20 @@ +import { setTimeout } from "node:timers/promises"; + +export const confirmationCountNeeded = (validatorCount: number) => { + const twoByThree = 0.666666667; + const paddedValidatorCount = 1; + return Math.floor(twoByThree * validatorCount) + paddedValidatorCount; +}; + +export const ProcessDelayMilliseconds = 5000; + +export function waitForMSWithMsg(ms: number, msg: string): Promise { + const secondsInMilliSeconds = 1000; + const numberOfDecimals = 2; + console.info( + `${msg}, retrying in ${(ms / secondsInMilliSeconds).toFixed( + numberOfDecimals, + )} seconds`, + ); + return setTimeout(ms); +} diff --git a/src/index.ts b/src/index.ts index e5a8414..6193245 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,8 @@ import { configDeps } from "./deps"; import "./environment"; import { configureValidator } from "./environment"; import { listenEvents, listenStakeEvents } from "./handler"; -import { checkOrAddSelfAsVal, retry, stakeTokens } from "./handler/utils"; +import { stakeTokens } from "./handler/stake-listener"; +import { checkOrAddSelfAsVal, retry } from "./handler/utils"; import { configureRouter } from "./http"; import type { IBridgeConfig, IGeneratedWallets } from "./types"; import { requireEnoughBalance, syncWallets } from "./utils";