From d7dae2413e594df21d1ee0a6960eaa5a514de03a Mon Sep 17 00:00:00 2001 From: Julian <52217955+julianmerlo95@users.noreply.github.com> Date: Fri, 22 Dec 2023 17:50:38 -0300 Subject: [PATCH] [Blockchain Watcher] (EVM) Support evm fail redeem (#919) * feature-823/support-evm-fail-redeem * feature-823/support-evm-fail-redeem * Ad handler fo transactions flow * Integrate test and handler * Run prettier * Create unit test for methodNameByAddressMapper and GetEvmTransactions class * Improve names * Add logger info * Improve log name and mapped methods * Run prettier * Implement strategy to process message * Run prettier * Run prettier * Resolve method test * Mapped timestamp value * Change string to number in chainId property * Improve names and mappers * Run prettier * Create interface for strategy * Resolve method GetEvmTransactions test * Resolve comment in PR * Resolve comment in PR * Rename redeem-failed * Improve rage value * Run prettier * Reduce mapper in one evmTransferFoundMapper * Mapped standar relay name * Improve readme * Change evmTransactionFoundMapper name * Change evm mapper name in object return * Mapped evm mapper test * Map EvmTransactionFound in asyncapi docs * Improve name in methods mapper * Implement batch request for getTransactionReceipt method * Add error manage in getTransactionReceipt method * Rename protocol MethodCompleteTransferWithRelay --------- Co-authored-by: julian merlo --- blockchain-watcher/README.md | 8 +- blockchain-watcher/config/mainnet.json | 1 + blockchain-watcher/docs/asyncapi.yaml | 144 +++++---- blockchain-watcher/package.json | 4 +- .../src/domain/actions/evm/GetEvmLogs.ts | 20 +- .../domain/actions/evm/GetEvmTransactions.ts | 87 +++++ .../actions/evm/HandleEvmTransactions.ts | 47 +++ .../evm/{PollEvmLogs.ts => PollEvm.ts} | 38 ++- .../evm/mappers/methodNameByAddressMapper.ts | 304 ++++++++++++++++++ .../src/domain/actions/index.ts | 3 +- .../src/domain/entities/events.ts | 32 ++ blockchain-watcher/src/domain/entities/evm.ts | 31 ++ .../src/domain/entities/jobs.ts | 3 +- blockchain-watcher/src/domain/repositories.ts | 20 +- .../mappers/evmStandardRelayDelivered.ts | 34 -- .../mappers/evmTransactionFoundMapper.ts | 42 +++ .../mappers/evmTransferRedeemedMapper.ts | 25 -- .../src/infrastructure/mappers/index.ts | 3 +- .../repositories/RepositoriesBuilder.ts | 1 + .../repositories/StaticJobRepository.ts | 32 +- .../evm/EvmJsonRPCBlockRepository.ts | 78 ++++- .../evm/MoonbeamEvmJsonRPCBlockRepository.ts | 2 +- .../actions/evm/GetEvmTransactions.test.ts | 177 ++++++++++ .../{PollEvmLogs.test.ts => PollEvm.test.ts} | 25 +- .../mappers/methodNameByAddressMapper.test.ts | 96 ++++++ .../mappers/evmStandardRelayDelivered.test.ts | 59 ---- .../mappers/evmTransactionFoundMapper.test.ts | 60 ++++ .../mappers/evmTransferRedeemed.test.ts | 53 --- .../repositories/StaticJobRepository.test.ts | 4 +- .../workers/ethereum-1.yaml | 28 +- .../blockchain-watcher/workers/ethereum.yaml | 28 +- 31 files changed, 1161 insertions(+), 328 deletions(-) create mode 100644 blockchain-watcher/src/domain/actions/evm/GetEvmTransactions.ts create mode 100644 blockchain-watcher/src/domain/actions/evm/HandleEvmTransactions.ts rename blockchain-watcher/src/domain/actions/evm/{PollEvmLogs.ts => PollEvm.ts} (83%) create mode 100644 blockchain-watcher/src/domain/actions/evm/mappers/methodNameByAddressMapper.ts delete mode 100644 blockchain-watcher/src/infrastructure/mappers/evmStandardRelayDelivered.ts create mode 100644 blockchain-watcher/src/infrastructure/mappers/evmTransactionFoundMapper.ts delete mode 100644 blockchain-watcher/src/infrastructure/mappers/evmTransferRedeemedMapper.ts create mode 100644 blockchain-watcher/test/domain/actions/evm/GetEvmTransactions.test.ts rename blockchain-watcher/test/domain/actions/evm/{PollEvmLogs.test.ts => PollEvm.test.ts} (88%) create mode 100644 blockchain-watcher/test/domain/actions/evm/mappers/methodNameByAddressMapper.test.ts delete mode 100644 blockchain-watcher/test/infrastructure/mappers/evmStandardRelayDelivered.test.ts create mode 100644 blockchain-watcher/test/infrastructure/mappers/evmTransactionFoundMapper.test.ts delete mode 100644 blockchain-watcher/test/infrastructure/mappers/evmTransferRedeemed.test.ts diff --git a/blockchain-watcher/README.md b/blockchain-watcher/README.md index 31f0074e..7bd67fcc 100644 --- a/blockchain-watcher/README.md +++ b/blockchain-watcher/README.md @@ -43,7 +43,7 @@ Example: "id": "poll-log-message-published-ethereum", "chain": "ethereum", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "fromBlock": "10012499", "blockBatchSize": 100, @@ -70,7 +70,7 @@ Example: { "action": "HandleEvmLogs", "target": "sns", - "mapper": "evmTransferRedeemedMapper", + "mapper": "evmTransactionFoundMapper", "config": { "abi": "event TransferRedeemed(uint16 indexed emitterChainId, bytes32 indexed emitterAddress, uint64 indexed sequence)", "filter": { @@ -82,7 +82,7 @@ Example: { "action": "HandleEvmLogs", "target": "sns", - "mapper": "evmStandardRelayDelivered", + "mapper": "evmTransactionFoundMapper", "config": { "abi": "event Delivery(address indexed recipientContract, uint16 indexed sourceChain, uint64 indexed sequence, bytes32 deliveryVaaHash, uint8 status, uint256 gasUsed, uint8 refundStatus, bytes additionalStatusInfo, bytes overridesInfo)", "filter": { @@ -127,5 +127,5 @@ Example: Currently, jobs are read and loaded based on a JSON file. Each job has a source, and one or more handlers. -Each handler has an action, a mapper and a target. For example, you can choose to use PollEvmLogs as an action and HandleEvmLogs as a handler. For this handler you need to set a mapper like evmLogMessagePublishedMapper. +Each handler has an action, a mapper and a target. For example, you can choose to use PollEvm (GetEvmLogs) as an action and HandleEvmLogs as a handler. For this handler you need to set a mapper like evmLogMessagePublishedMapper. The target can be sns, or a fake one if dryRun is enabled. diff --git a/blockchain-watcher/config/mainnet.json b/blockchain-watcher/config/mainnet.json index 83e314aa..9a231391 100644 --- a/blockchain-watcher/config/mainnet.json +++ b/blockchain-watcher/config/mainnet.json @@ -1,4 +1,5 @@ { + "environment": "mainnet", "chains": { "solana": { "network": "mainnet-beta", diff --git a/blockchain-watcher/docs/asyncapi.yaml b/blockchain-watcher/docs/asyncapi.yaml index b889cb36..2fa6c762 100644 --- a/blockchain-watcher/docs/asyncapi.yaml +++ b/blockchain-watcher/docs/asyncapi.yaml @@ -8,6 +8,15 @@ servers: staging-testnet: url: notification-chain-events-dev-testnet.fifo protocol: sns + staging-mainnet: + url: notification-chain-events-dev-mainnet.fifo + protocol: sns + prod-testnet: + url: notification-chain-events-prod-testnet.fifo + protocol: sns + prod-mainnet: + url: notification-chain-events-prod-mainnet.fifo + protocol: sns defaultContentType: application/json channels: LogMessagePublished: @@ -15,11 +24,11 @@ channels: subscribe: message: $ref: "#/components/messages/logMessagePublished" - TransferRedeemed: + EvmTransactionFound: description: Token bridge emitted event subscribe: message: - $ref: "#/components/messages/transferRedeemed" + $ref: "#/components/messages/evmTransactionFound" components: messages: logMessagePublished: @@ -28,50 +37,42 @@ components: contentType: application/json payload: $ref: "#/components/schemas/logMessagePublished" - transferRedeemed: - name: TransferRedeemed - title: TransferRedeemed + evmTransactionFound: + name: EvmTransactionFound + title: EvmTransactionFound contentType: application/json payload: - $ref: "#/components/schemas/transferRedeemed" + $ref: "#/components/schemas/evmTransactionFound" examples: - - name: TransferRedeemed in Solana from Ethereum + - name: EvmTransactionFound from Ethereum payload: - name: "transfer-redeemed" - address: wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb + name: "evm-transaction-found" + address: "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb" chainId: 1 - txHash: 3FySmshUgVCM2N158oNYbeTfZt2typEU32c9ZxdAXiXURFHuTmeJHhc7cSUtqHdwAsbVWWvEsEddWNAKzkjVPSg2 + txHash: "3FySmshUgVCM2N158oNYbeTfZt2typEU32c9ZxdAXiXURFHuTmeJHhc7cSUtqHdwAsbVWWvEsEddWNAKzkjVPSg2" blockHeight: 234015120 blockTime: 1701724272 attributes: - emitterChainId: 2 - emitterAddress: "0000000000000000000000003ee18b2214aff97000d974cf647e7c347e8fa585" - sequence: 144500 - standardRelayDelivered: - name: StandardRelayDelivered - title: StandardRelayDelivered - contentType: application/json - payload: - $ref: "#/components/schemas/standardRelayDelivered" - examples: - - name: StandardRelayDelivered from in Ethereum from Base - payload: - name: "standard-relay-delivered" - address: "0x27428dd2d3dd32a4d7f7c497eaaa23130d894911" - chainId: 2 - txHash: "0xcbdefc83080a8f60cbde7785eb2978548fd5c1f7d0ea2c024cce537845d339c7" - blockHeight: 18708316n - blockTime: 1699443287 - attributes: - recipientContract: "0xF80cf52922B512B22D46aA8916BD7767524305d9" - sourceChain: 30 - sequence: 2304 - deliveryVaaHash: "0xf29cac97156fa11c205eda95c0655e4a6e2a9c247245bab4d3d8257c41fc11d2" - status: 0 - gasUsed: 80521 - refundStatus: 0 - additionalStatusInfo: "0x" - overridesInfo: "0x" + blockHash: "0x1359819238ea89f49c20e42eb5603bf0541589d838d971984b60c7cdb391d9c2" + blockNumber: 0x11ec2bc + from: 0xfb070adcd21361a3946a0584dc84a7b89faa68e3 + gas: 0x14485 + gasPrice: xfc518561e + input: "0x9981509f000000000000" + maxFeePerGas: 0x1610f75b9a + maxPriorityFeePerGas: 0x5f5e100 + methodsByAddress: MethodCompleteTransfer + name: transfer-redeemed + nonce: 0x1 + r: 0xf5794b0970386d73b693b17f147fae0427db278e951e45465ac2c9835537e5a9 + s: 0x6dccc8cfee216bc43a9d66525fa94905da234ad32d6cc3220845bef78f25dd42 + status: 0x1 + timestamp: 1702663079 + to: 0x3ee18b2214aff97000d974cf647e7c347e8fa585 + transactionIndex: 0x6f + type: 0x2 + v: 0x1 + value: 0x5b09cd3e5e90000 schemas: base: type: object @@ -122,7 +123,7 @@ components: type: string consistencyLevel: type: number - transferRedeemed: + evmTransactionFound: allOf: - $ref: "#/components/schemas/base" type: object @@ -135,42 +136,45 @@ components: attributes: type: object properties: - emitterChainId: - type: number - emitterAddress: + name: type: string - sequence: - type: number - standardRelayDelivered: - allOf: - - $ref: "#/components/schemas/base" - type: object - properties: - data: - type: object - allOf: - - $ref: "#/components/schemas/chainEventBase" - properties: - attributes: - type: object - properties: - recipientContract: + from: type: string - sourceChain: - type: number - sequence: - type: number - deliveryVaaHash: + to: type: string status: - type: number - gasUsed: - type: number - refundStatus: - type: number - additionalStatusInfo: type: string - overridesInfo: + blockNumber: + type: number + input: + type: string + methodsByAddress: + type: string + timestamp: + type: number + blockHash: + type: string + gas: + type: string + gasPrice: + type: string + maxFeePerGas: + type: string + maxPriorityFeePerGas: + type: string + nonce: + type: string + r: + type: string + s: + type: string + transactionIndex: + type: string + type: + type: string + v: + type: string + value: type: string sentAt: type: string diff --git a/blockchain-watcher/package.json b/blockchain-watcher/package.json index c95d0540..7e91ebb2 100644 --- a/blockchain-watcher/package.json +++ b/blockchain-watcher/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "node lib/start.js", "start:ncc": "node lib/index.js", - "test": "jest", + "test": "jest --collectCoverage=false", "test:coverage": "jest --collectCoverage=true", "build": "tsc", "build:ncc": "ncc build src/start.ts -o lib", @@ -74,7 +74,7 @@ "coverageDirectory": "./coverage", "coverageThreshold": { "global": { - "lines": 73 + "lines": 74 } } } diff --git a/blockchain-watcher/src/domain/actions/evm/GetEvmLogs.ts b/blockchain-watcher/src/domain/actions/evm/GetEvmLogs.ts index 14e4a83b..4692785d 100644 --- a/blockchain-watcher/src/domain/actions/evm/GetEvmLogs.ts +++ b/blockchain-watcher/src/domain/actions/evm/GetEvmLogs.ts @@ -11,17 +11,18 @@ export class GetEvmLogs { this.logger = winston.child({ module: "GetEvmLogs" }); } - async execute(range: Range, opts: GetEvmLogsOpts): Promise { - if (range.fromBlock > range.toBlock) { - this.logger.info( - `[exec] Invalid range [fromBlock: ${range.fromBlock} - toBlock: ${range.toBlock}]` - ); + async execute(range: Range, opts: GetEvmOpts): Promise { + const fromBlock = range.fromBlock; + const toBlock = range.toBlock; + + if (fromBlock > toBlock) { + this.logger.info(`[exec] Invalid range [fromBlock: ${fromBlock} - toBlock: ${toBlock}]`); return []; } const logs = await this.blockRepo.getFilteredLogs(opts.chain, { - fromBlock: range.fromBlock, - toBlock: range.toBlock, + fromBlock, + toBlock, addresses: opts.addresses ?? [], // Works when sending multiple addresses, but not multiple topics. topics: opts.topics ?? [], }); @@ -42,8 +43,9 @@ type Range = { toBlock: bigint; }; -export interface GetEvmLogsOpts { +export type GetEvmOpts = { addresses?: string[]; topics?: string[]; chain: string; -} + environment: string; +}; diff --git a/blockchain-watcher/src/domain/actions/evm/GetEvmTransactions.ts b/blockchain-watcher/src/domain/actions/evm/GetEvmTransactions.ts new file mode 100644 index 00000000..116edafc --- /dev/null +++ b/blockchain-watcher/src/domain/actions/evm/GetEvmTransactions.ts @@ -0,0 +1,87 @@ +import { methodNameByAddressMapper } from "./mappers/methodNameByAddressMapper"; +import { EvmBlock, EvmTransaction } from "../../entities"; +import { EvmBlockRepository } from "../../repositories"; +import { GetEvmOpts } from "./GetEvmLogs"; +import winston from "winston"; + +export class GetEvmTransactions { + private readonly blockRepo: EvmBlockRepository; + protected readonly logger: winston.Logger; + + constructor(blockRepo: EvmBlockRepository) { + this.logger = winston.child({ module: "GetEvmTransactions" }); + this.blockRepo = blockRepo; + } + + async execute(range: Range, opts: GetEvmOpts): Promise { + const fromBlock = range.fromBlock; + const toBlock = range.toBlock; + + if (fromBlock > toBlock) { + this.logger.info(`[exec] Invalid range [fromBlock: ${fromBlock} - toBlock: ${toBlock}]`); + return []; + } + + let populateTransactions: EvmTransaction[] = []; + const environment = opts.environment; + const isTransactionsPresent = true; + const chain = opts.chain; + + for (let block = fromBlock; block <= toBlock; block++) { + const evmBlock = await this.blockRepo.getBlock(chain, block, isTransactionsPresent); + const transactions = evmBlock.transactions ?? []; + + // Only process transactions to the contract address + const transactionsFilter = transactions.filter( + (transaction) => + opts.addresses?.includes(String(transaction.to).toLowerCase()) || + opts.addresses?.includes(String(transaction.from).toLowerCase()) + ); + + if (transactionsFilter.length > 0) { + populateTransactions = await this.populateTransaction( + chain, + environment, + evmBlock, + transactionsFilter + ); + } + } + + this.logger.info( + `[${chain}][exec] Got ${ + populateTransactions?.length + } transactions to process for ${this.populateLog(opts, fromBlock, toBlock)}` + ); + return populateTransactions; + } + + private async populateTransaction( + chain: string, + environment: string, + evmBlock: EvmBlock, + transactionsFilter: EvmTransaction[] + ): Promise { + const hashNumbers = new Set(transactionsFilter.map((transaction) => transaction.hash)); + const receiptTransaction = await this.blockRepo.getTransactionReceipt(chain, hashNumbers); + + transactionsFilter.forEach((transaction) => { + transaction.chainId = Number(transaction.chainId); + transaction.timestamp = evmBlock.timestamp; + transaction.status = receiptTransaction[transaction.hash].status; + transaction.environment = environment; + transaction.chain = chain; + }); + + return transactionsFilter; + } + + private populateLog(opts: GetEvmOpts, fromBlock: bigint, toBlock: bigint): string { + return `[addresses:${opts.addresses}][topics:${opts.topics}][blocks:${fromBlock} - ${toBlock}]`; + } +} + +type Range = { + fromBlock: bigint; + toBlock: bigint; +}; diff --git a/blockchain-watcher/src/domain/actions/evm/HandleEvmTransactions.ts b/blockchain-watcher/src/domain/actions/evm/HandleEvmTransactions.ts new file mode 100644 index 00000000..b3d12bf2 --- /dev/null +++ b/blockchain-watcher/src/domain/actions/evm/HandleEvmTransactions.ts @@ -0,0 +1,47 @@ +import { HandleEvmLogsConfig } from "./HandleEvmLogs"; +import { EvmTransaction, TransactionFound } from "../../entities"; + +/** + * Handling means mapping and forward to a given target. + * As of today, we have mapped this event evmFailedRedeemed, evmStandardRelayDelivered and evmTransferRedeemed. + */ +export class HandleEvmTransactions { + cfg: HandleEvmLogsConfig; + mapper: (log: EvmTransaction) => T; + target: (parsed: T[]) => Promise; + + constructor( + cfg: HandleEvmLogsConfig, + mapper: (log: EvmTransaction) => T, + target: (parsed: T[]) => Promise + ) { + this.cfg = this.normalizeCfg(cfg); + this.mapper = mapper; + this.target = target; + } + + public async handle(transactions: EvmTransaction[]): Promise { + const mappedItems = transactions.map((transaction) => { + return this.mapper(transaction); + }) as TransactionFound[]; + + const filterItems = mappedItems.filter( + (transaction) => transaction.methodsByAddress || transaction.name + ) as T[]; + + await this.target(filterItems); + + // TODO: return a result specifying failures if any + return filterItems; + } + + private normalizeCfg(cfg: HandleEvmLogsConfig): HandleEvmLogsConfig { + return { + filter: { + addresses: cfg.filter.addresses.map((addr) => addr.toLowerCase()), + topics: cfg.filter.topics.map((topic) => topic.toLowerCase()), + }, + abi: cfg.abi, + }; + } +} diff --git a/blockchain-watcher/src/domain/actions/evm/PollEvmLogs.ts b/blockchain-watcher/src/domain/actions/evm/PollEvm.ts similarity index 83% rename from blockchain-watcher/src/domain/actions/evm/PollEvmLogs.ts rename to blockchain-watcher/src/domain/actions/evm/PollEvm.ts index 8de852c6..54bdaeaa 100644 --- a/blockchain-watcher/src/domain/actions/evm/PollEvmLogs.ts +++ b/blockchain-watcher/src/domain/actions/evm/PollEvm.ts @@ -1,40 +1,46 @@ -import { EvmLog } from "../../entities"; +import { EvmLog, EvmTransaction } from "../../entities"; import { RunPollingJob } from "../RunPollingJob"; import { GetEvmLogs } from "./GetEvmLogs"; import { EvmBlockRepository, MetadataRepository, StatRepository } from "../../repositories"; import winston from "winston"; +import { GetEvmTransactions } from "./GetEvmTransactions"; const ID = "watch-evm-logs"; /** - * PollEvmLogs is an action that watches for new blocks and extracts logs from them. + * PollEvm is an action that watches for new blocks and extracts logs from them. */ -export class PollEvmLogs extends RunPollingJob { +export class PollEvm extends RunPollingJob { protected readonly logger: winston.Logger; private readonly blockRepo: EvmBlockRepository; private readonly metadataRepo: MetadataRepository; private readonly statsRepository: StatRepository; - private readonly getEvmLogs: GetEvmLogs; - private cfg: PollEvmLogsConfig; + private readonly getEvm: GetEvmLogs; + private cfg: PollEvmLogsConfig; private latestBlockHeight?: bigint; private blockHeightCursor?: bigint; private lastRange?: { fromBlock: bigint; toBlock: bigint }; + private getEvmRecords: { [key: string]: any } = { + GetEvmLogs, + GetEvmTransactions, + }; constructor( blockRepo: EvmBlockRepository, metadataRepo: MetadataRepository, statsRepository: StatRepository, - cfg: PollEvmLogsConfig + cfg: PollEvmLogsConfig, + getEvm: string ) { super(cfg.interval ?? 1_000, cfg.id, statsRepository); this.blockRepo = blockRepo; this.metadataRepo = metadataRepo; this.statsRepository = statsRepository; this.cfg = cfg; - this.getEvmLogs = new GetEvmLogs(blockRepo); - this.logger = winston.child({ module: "PollEvmLogs", label: this.cfg.id }); + this.logger = winston.child({ module: "PollEvm", label: this.cfg.id }); + this.getEvm = new this.getEvmRecords[getEvm ?? "GetEvmLogs"](blockRepo); } protected async preHook(): Promise { @@ -48,14 +54,14 @@ export class PollEvmLogs extends RunPollingJob { const hasFinished = this.cfg.hasFinished(this.blockHeightCursor); if (hasFinished) { this.logger.info( - `[hasNext] PollEvmLogs: (${this.cfg.id}) Finished processing all blocks from ${this.cfg.fromBlock} to ${this.cfg.toBlock}` + `[hasNext] PollEvm: (${this.cfg.id}) Finished processing all blocks from ${this.cfg.fromBlock} to ${this.cfg.toBlock}` ); } return !hasFinished; } - protected async get(): Promise { + protected async get(): Promise { this.report(); this.latestBlockHeight = await this.blockRepo.getBlockHeight( @@ -65,15 +71,16 @@ export class PollEvmLogs extends RunPollingJob { const range = this.getBlockRange(this.latestBlockHeight); - const logs = await this.getEvmLogs.execute(range, { + const records = await this.getEvm.execute(range, { chain: this.cfg.chain, addresses: this.cfg.addresses, topics: this.cfg.topics, + environment: this.cfg.environment, }); this.lastRange = range; - return logs; + return records; } protected async persist(): Promise { @@ -149,6 +156,7 @@ export interface PollEvmLogsConfigProps { topics: string[]; id?: string; chain: string; + environment: string; } export class PollEvmLogsConfig { @@ -210,7 +218,11 @@ export class PollEvmLogsConfig { return this.props.chain; } + public get environment() { + return this.props.environment; + } + static fromBlock(chain: string, fromBlock: bigint) { - return new PollEvmLogsConfig({ chain, fromBlock, addresses: [], topics: [] }); + return new PollEvmLogsConfig({ chain, fromBlock, addresses: [], topics: [], environment: "" }); } } diff --git a/blockchain-watcher/src/domain/actions/evm/mappers/methodNameByAddressMapper.ts b/blockchain-watcher/src/domain/actions/evm/mappers/methodNameByAddressMapper.ts new file mode 100644 index 00000000..e47ccd37 --- /dev/null +++ b/blockchain-watcher/src/domain/actions/evm/mappers/methodNameByAddressMapper.ts @@ -0,0 +1,304 @@ +import { EvmTransaction } from "../../../entities"; + +const TESTNET_ENVIRONMENT = "testnet"; + +export const methodNameByAddressMapper = ( + chain: string, + environment: string, + transaction: EvmTransaction +): Protocol | undefined => { + const address = transaction.to; + const input = transaction.input; + + if (environment == TESTNET_ENVIRONMENT) { + return methodsByAddressTestnet(chain, address, input); + } else { + return methodsByAddressMainnet(chain, address, input); + } +}; + +const methodsByAddressTestnet = ( + chain: string, + address: string, + input: string +): Protocol | undefined => { + const testnet: MethodsByAddress = { + ethereum: [ + { + [String("0xF890982f9310df57d00f659cf4fd87e65adEd8d7").toLowerCase()]: ethBase, + }, + { + [String("0x9563a59C15842a6f322B10f69d1dD88b41f2E97B").toLowerCase()]: + completeTransferWithRelay, + }, + ], + polygon: [ + { + [String("0x377D55a7928c046E18eEbb61977e714d2a76472a").toLowerCase()]: ethBase, + }, + { + [String("0x9563a59C15842a6f322B10f69d1dD88b41f2E97B").toLowerCase()]: + completeTransferWithRelay, + }, + { + [String("0xc3D46e0266d95215589DE639cC4E93b79f88fc6C").toLowerCase()]: receiveTbtc, + }, + ], + bsc: [ + { + [String("0x9dcF9D205C9De35334D646BeE44b2D2859712A09").toLowerCase()]: ethBase, + }, + { + [String("0x9563a59C15842a6f322B10f69d1dD88b41f2E97B").toLowerCase()]: + completeTransferWithRelay, + }, + ], + fantom: [ + { + [String("0x599CEa2204B4FaECd584Ab1F2b6aCA137a0afbE8").toLowerCase()]: ethBase, + }, + { + [String("0x9563a59C15842a6f322B10f69d1dD88b41f2E97B").toLowerCase()]: + completeTransferWithRelay, + }, + ], + avalanche: [ + { + [String("0x61E44E506Ca5659E6c0bba9b678586fA2d729756").toLowerCase()]: ethBase, + }, + { + [String("0x9563a59C15842a6f322B10f69d1dD88b41f2E97B").toLowerCase()]: + completeTransferWithRelay, + }, + ], + oasis: [ + { + [String("0x88d8004A9BdbfD9D28090A02010C19897a29605c").toLowerCase()]: ethBase, + }, + ], + moonbean: [ + { + [String("0xbc976D4b9D57E57c3cA52e1Fd136C45FF7955A96").toLowerCase()]: ethBase, + }, + { + [String("0x9563a59C15842a6f322B10f69d1dD88b41f2E97B").toLowerCase()]: + completeTransferWithRelay, + }, + ], + celo: [ + { + [String("0x05ca6037eC51F8b712eD2E6Fa72219FEaE74E153").toLowerCase()]: ethBase, + }, + { + [String("0x9563a59C15842a6f322B10f69d1dD88b41f2E97B").toLowerCase()]: + completeTransferWithRelay, + }, + ], + arbitrum: [ + { + [String("0xe3e0511EEbD87F08FbaE4486419cb5dFB06e1343").toLowerCase()]: receiveTbtc, + }, + ], + optimism: [ + { + [String("0xc3D46e0266d95215589DE639cC4E93b79f88fc6C").toLowerCase()]: receiveTbtc, + }, + ], + base: [ + { + [String("0xA31aa3FDb7aF7Db93d18DDA4e19F811342EDF780").toLowerCase()]: base, + }, + ], + }; + + return findMethodName(testnet, chain, address, input); +}; + +const methodsByAddressMainnet = ( + chain: string, + address: string, + input: string +): Protocol | undefined => { + const mainnet: MethodsByAddress = { + ethereum: [ + { + [String("0x3ee18B2214AFF97000D974cf647E7C347E8fa585").toLowerCase()]: ethBase, + }, + { + [String("0xcafd2f0a35a4459fa40c0517e17e6fa2939441ca").toLowerCase()]: + completeTransferWithRelay, + }, + { + [String("0xd8E1465908103eD5fd28e381920575fb09beb264").toLowerCase()]: receiveMessageAndSwap, + }, + ], + polygon: [ + { + [String("0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE").toLowerCase()]: ethBase, + }, + { + [String("0xcafd2f0a35a4459fa40c0517e17e6fa2939441ca").toLowerCase()]: + completeTransferWithRelay, + }, + { + [String("0x09959798B95d00a3183d20FaC298E4594E599eab").toLowerCase()]: receiveTbtc, + }, + { + [String("0xf6C5FD2C8Ecba25420859f61Be0331e68316Ba01").toLowerCase()]: receiveMessageAndSwap, + }, + ], + bsc: [ + { + [String("0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7").toLowerCase()]: ethBase, + }, + { + [String("0xcafd2f0a35a4459fa40c0517e17e6fa2939441ca").toLowerCase()]: + completeTransferWithRelay, + }, + ], + fantom: [ + { + [String("0x7C9Fc5741288cDFdD83CeB07f3ea7e22618D79D2").toLowerCase()]: ethBase, + }, + { + [String("0xcafd2f0a35a4459fa40c0517e17e6fa2939441ca").toLowerCase()]: + completeTransferWithRelay, + }, + ], + avalanche: [ + { + [String("0x0e082F06FF657D94310cB8cE8B0D9a04541d8052").toLowerCase()]: ethBase, + }, + { + [String("0xcafd2f0a35a4459fa40c0517e17e6fa2939441ca").toLowerCase()]: + completeTransferWithRelay, + }, + ], + oasis: [ + { + [String("0x5848C791e09901b40A9Ef749f2a6735b418d7564").toLowerCase()]: ethBase, + }, + ], + moonbean: [ + { + [String("0xb1731c586ca89a23809861c6103f0b96b3f57d92").toLowerCase()]: ethBase, + }, + { + [String("0xcafd2f0a35a4459fa40c0517e17e6fa2939441ca").toLowerCase()]: + completeTransferWithRelay, + }, + ], + celo: [ + { + [String("0x796Dff6D74F3E27060B71255Fe517BFb23C93eed").toLowerCase()]: ethBase, + }, + { + [String("0xcafd2f0a35a4459fa40c0517e17e6fa2939441ca").toLowerCase()]: + completeTransferWithRelay, + }, + ], + arbitrum: [ + { + [String("0x1293a54e160D1cd7075487898d65266081A15458").toLowerCase()]: receiveTbtc, + }, + { + [String("0xf8497FE5B0C5373778BFa0a001d476A21e01f09b").toLowerCase()]: receiveMessageAndSwap, + }, + ], + optimism: [ + { + [String("0x1293a54e160D1cd7075487898d65266081A15458").toLowerCase()]: receiveTbtc, + }, + { + [String("0xcF205Fa51D33280D9B70321Ae6a3686FB2c178b2").toLowerCase()]: receiveMessageAndSwap, + }, + ], + base: [ + { + [String("0x8d2de8d2f73F1F4cAB472AC9A881C9b123C79627").toLowerCase()]: base, + }, + { + [String("0x9816d7C448f79CdD4aF18c4Ae1726A14299E8C75").toLowerCase()]: receiveMessageAndSwap, + }, + ], + }; + + return findMethodName(mainnet, chain, address, input); +}; + +const findMethodName = ( + environment: MethodsByAddress, + chain: string, + address: string, + input: string +): Protocol | undefined => { + const first10Characters = input.slice(0, 10); + let protocol: Protocol | undefined; + + environment[chain]?.find((addresses) => { + const protocols = addresses[address]; + const foundProtocol = protocols?.get(first10Characters); + protocol = foundProtocol; + return foundProtocol; + }); + + return protocol; +}; + +export enum MethodID { + // Method ids for wormhole token bridge contract + MethodIDCompleteTransfer = "0xc6878519", + MethodIDWrapAndTransfer = "0x9981509f", + MethodIDTransferTokens = "0x0f5287b0", + MethodIDAttestToken = "0xc48fa115", + MethodIDCompleteAndUnwrapETH = "0xff200cde", + MethodIDCreateWrapped = "0xe8059810", + MethodIDUpdateWrapped = "0xf768441f", + // Method id for wormhole connect wrapped contract. + MethodCompleteTransferWithRelay = "0x2f25e25f", + // Method id for wormhole tBTC gateway + MethodIDReceiveTbtc = "0x5d21a596", + // Method id for Portico contract + MethodIDReceiveMessageAndSwap = "0x3d528f35", +} + +const ethBase = new Map([ + [ + MethodID.MethodIDCompleteTransfer, + { method: "MethodCompleteTransfer", name: "transfer-redeemed" }, + ], + [ + MethodID.MethodIDCompleteAndUnwrapETH, + { method: "MethodCompleteAndUnwrapETH", name: "transfer-redeemed" }, + ], + [MethodID.MethodIDCreateWrapped, { method: "MethodCreateWrapped", name: "transfer-redeemed" }], + [MethodID.MethodIDUpdateWrapped, { method: "MethodUpdateWrapped", name: "transfer-redeemed" }], +]); + +const completeTransferWithRelay = new Map([ + [ + MethodID.MethodCompleteTransferWithRelay, + { method: "MethodCompleteTransferWithRelay", name: "standard-relay-delivered" }, + ], +]); + +const receiveMessageAndSwap = new Map([ + [MethodID.MethodIDReceiveMessageAndSwap, { method: "MethodReceiveMessageAndSwap", name: "" }], // TODO: When active this protocol set the name +]); + +const receiveTbtc = new Map([ + [MethodID.MethodIDReceiveTbtc, { method: "MethodReceiveTbtc", name: "" }], // TODO: When active this protocol set the name +]); + +const base = new Map([...ethBase, ...completeTransferWithRelay]); + +type MethodsByAddress = { + [chain: string]: { + [address: string]: Map; + }[]; +}; + +type Protocol = { + method: string; + name: string; +}; diff --git a/blockchain-watcher/src/domain/actions/index.ts b/blockchain-watcher/src/domain/actions/index.ts index 936a1db4..6089ca2c 100644 --- a/blockchain-watcher/src/domain/actions/index.ts +++ b/blockchain-watcher/src/domain/actions/index.ts @@ -1,6 +1,7 @@ export * from "./evm/HandleEvmLogs"; +export * from "./evm/HandleEvmTransactions"; export * from "./evm/GetEvmLogs"; -export * from "./evm/PollEvmLogs"; +export * from "./evm/PollEvm"; export * from "./solana/GetSolanaTransactions"; export * from "./solana/PollSolanaTransactions"; export * from "./RunPollingJob"; diff --git a/blockchain-watcher/src/domain/entities/events.ts b/blockchain-watcher/src/domain/entities/events.ts index 171affd3..37ca77cd 100644 --- a/blockchain-watcher/src/domain/entities/events.ts +++ b/blockchain-watcher/src/domain/entities/events.ts @@ -33,3 +33,35 @@ export type StandardRelayDelivered = { additionalStatusInfo: string; overridesInfo: string; }; + +export type TransactionFoundEvent = { + name: string; + address: string; + txHash: string; + blockHeight: bigint; + chainId: number; + attributes: T; +}; + +export type TransactionFound = { + name?: string; + from: string; + to: string; + status?: string; + blockNumber: bigint; + input: string; + methodsByAddress?: string; + timestamp: number; + blockHash: string; + gas: string; + gasPrice: string; + maxFeePerGas: string; + maxPriorityFeePerGas: string; + nonce: string; + r: string; + s: string; + transactionIndex: string; + type: string; + v: string; + value: string; +}; diff --git a/blockchain-watcher/src/domain/entities/evm.ts b/blockchain-watcher/src/domain/entities/evm.ts index 30c9783c..2172ae13 100644 --- a/blockchain-watcher/src/domain/entities/evm.ts +++ b/blockchain-watcher/src/domain/entities/evm.ts @@ -2,6 +2,7 @@ export type EvmBlock = { number: bigint; hash: string; timestamp: number; // epoch seconds + transactions?: EvmTransaction[]; }; export type EvmLog = { @@ -18,6 +19,31 @@ export type EvmLog = { chainId: number; }; +export type EvmTransaction = { + blockHash: string; + blockNumber: bigint; + chainId: number; + from: string; + gas: string; + gasPrice: string; + hash: string; + input: string; + maxFeePerGas: string; + maxPriorityFeePerGas: string; + nonce: string; + r: string; + s: string; + status?: string; + to: string; + transactionIndex: string; + type: string; + v: string; + value: string; + timestamp: number; + environment: string; + chain: string; +}; + export type EvmTag = "finalized" | "latest" | "safe"; export type EvmTopicFilter = { @@ -31,3 +57,8 @@ export type EvmLogFilter = { addresses: string[]; topics: string[]; }; + +export type ReceiptTransaction = { + status: string; + transactionHash: string; +}; diff --git a/blockchain-watcher/src/domain/entities/jobs.ts b/blockchain-watcher/src/domain/entities/jobs.ts index 54549a66..050d03cd 100644 --- a/blockchain-watcher/src/domain/entities/jobs.ts +++ b/blockchain-watcher/src/domain/entities/jobs.ts @@ -3,6 +3,7 @@ export class JobDefinition { chain: string; source: { action: string; + records: string; config: Record; }; handlers: { @@ -15,7 +16,7 @@ export class JobDefinition { constructor( id: string, chain: string, - source: { action: string; config: Record }, + source: { action: string; records: string; config: Record }, handlers: { action: string; target: string; mapper: string; config: Record }[] ) { this.id = id; diff --git a/blockchain-watcher/src/domain/repositories.ts b/blockchain-watcher/src/domain/repositories.ts index cf8e8059..a3376591 100644 --- a/blockchain-watcher/src/domain/repositories.ts +++ b/blockchain-watcher/src/domain/repositories.ts @@ -1,5 +1,14 @@ import { RunPollingJob } from "./actions/RunPollingJob"; -import { EvmBlock, EvmLog, EvmLogFilter, Handler, JobDefinition, solana } from "./entities"; +import { + EvmBlock, + EvmLog, + EvmLogFilter, + EvmTag, + Handler, + JobDefinition, + ReceiptTransaction, + solana, +} from "./entities"; import { ConfirmedSignatureInfo } from "./entities/solana"; import { Fallible, SolanaFailure } from "./errors"; @@ -7,6 +16,15 @@ export interface EvmBlockRepository { getBlockHeight(chain: string, finality: string): Promise; getBlocks(chain: string, blockNumbers: Set): Promise>; getFilteredLogs(chain: string, filter: EvmLogFilter): Promise; + getTransactionReceipt( + chain: string, + hashNumbers: Set + ): Promise>; + getBlock( + chain: string, + blockNumberOrTag: EvmTag | bigint, + isTransactionsPresent: boolean + ): Promise; } export interface SolanaSlotRepository { diff --git a/blockchain-watcher/src/infrastructure/mappers/evmStandardRelayDelivered.ts b/blockchain-watcher/src/infrastructure/mappers/evmStandardRelayDelivered.ts deleted file mode 100644 index 53b270d4..00000000 --- a/blockchain-watcher/src/infrastructure/mappers/evmStandardRelayDelivered.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { BigNumber } from "ethers"; -import { EvmLog, LogFoundEvent, StandardRelayDelivered } from "../../domain/entities"; - -/* - * Delivery (index_topic_1 address recipientContract, index_topic_2 uint16 sourceChain, index_topic_3 uint64 sequence, bytes32 deliveryVaaHash, uint8 status, uint256 gasUsed, uint8 refundStatus, bytes additionalStatusInfo, bytes overridesInfo) - */ -export const evmStandardRelayDelivered = ( - log: EvmLog, - args: ReadonlyArray -): LogFoundEvent => { - if (!log.blockTime) { - throw new Error(`Block time is missing for log ${log.logIndex} in tx ${log.transactionHash}`); - } - - return { - name: "standard-relay-delivered", - address: log.address, - chainId: log.chainId, - txHash: log.transactionHash, - blockHeight: log.blockNumber, - blockTime: log.blockTime, - attributes: { - recipientContract: args[0], - sourceChain: BigNumber.from(args[1]).toNumber(), - sequence: BigNumber.from(args[2]).toNumber(), - deliveryVaaHash: args[3], - status: BigNumber.from(args[4]).toNumber(), - gasUsed: BigNumber.from(args[5]).toNumber(), - refundStatus: BigNumber.from(args[6]).toNumber(), - additionalStatusInfo: args[7], - overridesInfo: args[8], - }, - }; -}; diff --git a/blockchain-watcher/src/infrastructure/mappers/evmTransactionFoundMapper.ts b/blockchain-watcher/src/infrastructure/mappers/evmTransactionFoundMapper.ts new file mode 100644 index 00000000..2904b5e1 --- /dev/null +++ b/blockchain-watcher/src/infrastructure/mappers/evmTransactionFoundMapper.ts @@ -0,0 +1,42 @@ +import { methodNameByAddressMapper } from "../../domain/actions/evm/mappers/methodNameByAddressMapper"; +import { EvmTransaction, TransactionFound, TransactionFoundEvent } from "../../domain/entities"; + +export const evmTransactionFoundMapper = ( + transaction: EvmTransaction +): TransactionFoundEvent => { + const protocol = methodNameByAddressMapper( + transaction.chain, + transaction.environment, + transaction + ); + + return { + name: "evm-transaction-found", + address: transaction.to, + chainId: transaction.chainId, + txHash: transaction.hash, + blockHeight: BigInt(transaction.blockNumber), + attributes: { + name: protocol?.name, + from: transaction.from, + to: transaction.to, + status: transaction.status, + blockNumber: transaction.blockNumber, + input: transaction.input, + methodsByAddress: protocol?.method, + timestamp: transaction.timestamp, + blockHash: transaction.blockHash, + gas: transaction.gas, + gasPrice: transaction.gasPrice, + maxFeePerGas: transaction.maxFeePerGas, + maxPriorityFeePerGas: transaction.maxPriorityFeePerGas, + nonce: transaction.nonce, + r: transaction.r, + s: transaction.s, + transactionIndex: transaction.transactionIndex, + type: transaction.type, + v: transaction.v, + value: transaction.value, + }, + }; +}; diff --git a/blockchain-watcher/src/infrastructure/mappers/evmTransferRedeemedMapper.ts b/blockchain-watcher/src/infrastructure/mappers/evmTransferRedeemedMapper.ts deleted file mode 100644 index 5a4d8e58..00000000 --- a/blockchain-watcher/src/infrastructure/mappers/evmTransferRedeemedMapper.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { BigNumber } from "ethers"; -import { EvmLog, LogFoundEvent, TransferRedeemed } from "../../domain/entities"; - -export const evmTransferRedeemedMapper = ( - log: EvmLog, - _: ReadonlyArray -): LogFoundEvent => { - if (!log.blockTime) { - throw new Error(`Block time is missing for log ${log.logIndex} in tx ${log.transactionHash}`); - } - - return { - name: "transfer-redeemed", - address: log.address, - chainId: log.chainId, - txHash: log.transactionHash, - blockHeight: log.blockNumber, - blockTime: log.blockTime, - attributes: { - emitterChainId: Number(log.topics[1]), - emitterAddress: log.topics[2], - sequence: BigNumber.from(log.topics[3]).toNumber(), - }, - }; -}; diff --git a/blockchain-watcher/src/infrastructure/mappers/index.ts b/blockchain-watcher/src/infrastructure/mappers/index.ts index 5dc1a4e5..a5fc061d 100644 --- a/blockchain-watcher/src/infrastructure/mappers/index.ts +++ b/blockchain-watcher/src/infrastructure/mappers/index.ts @@ -1,5 +1,4 @@ export * from "./evmLogMessagePublishedMapper"; -export * from "./evmTransferRedeemedMapper"; -export * from "./evmStandardRelayDelivered"; +export * from "./evmTransactionFoundMapper"; export * from "./solanaLogMessagePublishedMapper"; export * from "./solanaTransferRedeemedMapper"; diff --git a/blockchain-watcher/src/infrastructure/repositories/RepositoriesBuilder.ts b/blockchain-watcher/src/infrastructure/repositories/RepositoriesBuilder.ts index 15d1c55d..27729962 100644 --- a/blockchain-watcher/src/infrastructure/repositories/RepositoriesBuilder.ts +++ b/blockchain-watcher/src/infrastructure/repositories/RepositoriesBuilder.ts @@ -93,6 +93,7 @@ export class RepositoriesBuilder { this.repositories.set( "jobs", new StaticJobRepository( + this.cfg.environment, this.cfg.jobs.dir, this.cfg.dryRun, (chain: string) => this.getEvmBlockRepository(chain), diff --git a/blockchain-watcher/src/infrastructure/repositories/StaticJobRepository.ts b/blockchain-watcher/src/infrastructure/repositories/StaticJobRepository.ts index 04049659..aa288607 100644 --- a/blockchain-watcher/src/infrastructure/repositories/StaticJobRepository.ts +++ b/blockchain-watcher/src/infrastructure/repositories/StaticJobRepository.ts @@ -1,6 +1,6 @@ import { HandleEvmLogs, - PollEvmLogs, + PollEvm, PollEvmLogsConfig, PollEvmLogsConfigProps, PollSolanaTransactions, @@ -21,13 +21,14 @@ import { solanaLogMessagePublishedMapper, solanaTransferRedeemedMapper, evmLogMessagePublishedMapper, - evmStandardRelayDelivered, - evmTransferRedeemedMapper, + evmTransactionFoundMapper, } from "../mappers"; import log from "../log"; +import { HandleEvmTransactions } from "../../domain/actions/evm/HandleEvmTransactions"; export class StaticJobRepository implements JobRepository { private fileRepo: FileMetadataRepository; + private environment: string; private dryRun: boolean = false; private sources: Map RunPollingJob> = new Map(); private handlers: Map Promise> = @@ -41,6 +42,7 @@ export class StaticJobRepository implements JobRepository { private solanaSlotRepo: SolanaSlotRepository; constructor( + environment: string, path: string, dryRun: boolean, blockRepoProvider: (chain: string) => EvmBlockRepository, @@ -57,6 +59,7 @@ export class StaticJobRepository implements JobRepository { this.statsRepo = repos.statsRepo; this.snsRepo = repos.snsRepo; this.solanaSlotRepo = repos.solanaSlotRepo; + this.environment = environment; this.dryRun = dryRun; this.fill(); } @@ -98,28 +101,29 @@ export class StaticJobRepository implements JobRepository { private fill() { // Actions - const pollEvmLogs = (jobDef: JobDefinition) => - new PollEvmLogs( + const pollEvm = (jobDef: JobDefinition) => + new PollEvm( this.blockRepoProvider(jobDef.source.config.chain), this.metadataRepo, this.statsRepo, new PollEvmLogsConfig({ ...(jobDef.source.config as PollEvmLogsConfigProps), id: jobDef.id, - }) + environment: this.environment, + }), + jobDef.source.records ); const pollSolanaTransactions = (jobDef: JobDefinition) => new PollSolanaTransactions(this.metadataRepo, this.solanaSlotRepo, this.statsRepo, { ...(jobDef.source.config as PollSolanaTransactionsConfig), id: jobDef.id, }); - this.sources.set("PollEvmLogs", pollEvmLogs); + this.sources.set("PollEvm", pollEvm); this.sources.set("PollSolanaTransactions", pollSolanaTransactions); // Mappers this.mappers.set("evmLogMessagePublishedMapper", evmLogMessagePublishedMapper); - this.mappers.set("evmStandardRelayDelivered", evmStandardRelayDelivered); - this.mappers.set("evmTransferRedeemedMapper", evmTransferRedeemedMapper); + this.mappers.set("evmTransactionFoundMapper", evmTransactionFoundMapper); this.mappers.set("solanaLogMessagePublishedMapper", solanaLogMessagePublishedMapper); this.mappers.set("solanaTransferRedeemedMapper", solanaTransferRedeemedMapper); @@ -141,12 +145,22 @@ export class StaticJobRepository implements JobRepository { return instance.handle.bind(instance); }; + const handleEvmTransactions = async (config: any, target: string, mapper: any) => { + const instance = new HandleEvmTransactions>( + config, + mapper, + await this.targets.get(this.dryRun ? "dummy" : target)!() + ); + + return instance.handle.bind(instance); + }; const handleSolanaTx = async (config: any, target: string, mapper: any) => { const instance = new HandleSolanaTransactions(config, mapper, await this.getTarget(target)); return instance.handle.bind(instance); }; this.handlers.set("HandleEvmLogs", handleEvmLogs); + this.handlers.set("HandleEvmTransactions", handleEvmTransactions); this.handlers.set("HandleSolanaTransactions", handleSolanaTx); } diff --git a/blockchain-watcher/src/infrastructure/repositories/evm/EvmJsonRPCBlockRepository.ts b/blockchain-watcher/src/infrastructure/repositories/evm/EvmJsonRPCBlockRepository.ts index 506914af..1a47bfc3 100644 --- a/blockchain-watcher/src/infrastructure/repositories/evm/EvmJsonRPCBlockRepository.ts +++ b/blockchain-watcher/src/infrastructure/repositories/evm/EvmJsonRPCBlockRepository.ts @@ -1,4 +1,10 @@ -import { EvmBlock, EvmLogFilter, EvmLog, EvmTag } from "../../../domain/entities"; +import { + EvmBlock, + EvmLogFilter, + EvmLog, + EvmTag, + ReceiptTransaction, +} from "../../../domain/entities"; import { EvmBlockRepository } from "../../../domain/repositories"; import winston from "../../log"; import { HttpClient } from "../../rpc/http/HttpClient"; @@ -173,7 +179,11 @@ export class EvmJsonRPCBlockRepository implements EvmBlockRepository { /** * Loosely based on the wormhole-dashboard implementation (minus some specially crafted blocks when null result is obtained) */ - protected async getBlock(chain: string, blockNumberOrTag: EvmTag | bigint): Promise { + async getBlock( + chain: string, + blockNumberOrTag: EvmTag | bigint, + isTransactionsPresent: boolean = false + ): Promise { const blockNumberParam = typeof blockNumberOrTag === "bigint" ? `${HEXADECIMAL_PREFIX}${blockNumberOrTag.toString(16)}` @@ -187,7 +197,7 @@ export class EvmJsonRPCBlockRepository implements EvmBlockRepository { { jsonrpc: "2.0", method: "eth_getBlockByNumber", - params: [blockNumberParam, false], // this means we'll get a light block (no txs) + params: [blockNumberParam, isTransactionsPresent], // this means we'll get a light block (no txs) id: 1, }, { timeout: chainCfg.timeout, retries: chainCfg.retries } @@ -205,6 +215,7 @@ export class EvmJsonRPCBlockRepository implements EvmBlockRepository { number: BigInt(result.number), timestamp: Number(result.timestamp), hash: result.hash, + transactions: result.transactions, }; } throw new Error( @@ -212,6 +223,67 @@ export class EvmJsonRPCBlockRepository implements EvmBlockRepository { ); } + /** + * Get the transaction ReceiptTransaction. Hash param refers to transaction hash + */ + async getTransactionReceipt( + chain: string, + hashNumbers: Set + ): Promise> { + const chainCfg = this.getCurrentChain(chain); + let results: { result: ReceiptTransaction; error?: ErrorBlock }[]; + + const reqs: any[] = []; + for (let hash of hashNumbers) { + reqs.push({ + jsonrpc: "2.0", + id: 1, + method: "eth_getTransactionReceipt", + params: [hash], + }); + } + + try { + results = await this.httpClient.post(chainCfg.rpc.href, reqs, { + timeout: chainCfg.timeout, + retries: chainCfg.retries, + }); + } catch (e: HttpClientError | any) { + this.handleError(chain, e, "getTransactionReceipt", "eth_getTransactionReceipt"); + throw e; + } + + if (results && results.length) { + return results + .map((response) => { + if (response.result?.status && response.result?.transactionHash) { + return { + status: response.result.status, + transactionHash: response.result.transactionHash, + }; + } + + const msg = `[${chain}][getTransactionReceipt] Got error ${response?.error} for eth_getTransactionReceipt for ${hashNumbers} on ${chainCfg.rpc.hostname}`; + + this.logger.error(msg); + + throw new Error( + `Unable to parse result of eth_getTransactionReceipt[${chain}] for ${response?.result}: ${msg}` + ); + }) + .reduce( + (acc: Record, receiptTransaction: ReceiptTransaction) => { + acc[receiptTransaction.transactionHash] = receiptTransaction; + return acc; + }, + {} + ); + } + throw new Error( + `Unable to parse result of eth_getTransactionReceipt for ${hashNumbers} on ${chainCfg.rpc}` + ); + } + protected handleError(chain: string, e: any, method: string, apiMethod: string) { const chainCfg = this.getCurrentChain(chain); if (e instanceof HttpClientError) { diff --git a/blockchain-watcher/src/infrastructure/repositories/evm/MoonbeamEvmJsonRPCBlockRepository.ts b/blockchain-watcher/src/infrastructure/repositories/evm/MoonbeamEvmJsonRPCBlockRepository.ts index 28222ba1..61536da2 100644 --- a/blockchain-watcher/src/infrastructure/repositories/evm/MoonbeamEvmJsonRPCBlockRepository.ts +++ b/blockchain-watcher/src/infrastructure/repositories/evm/MoonbeamEvmJsonRPCBlockRepository.ts @@ -53,7 +53,7 @@ export class MoonbeamEvmJsonRPCBlockRepository extends EvmJsonRPCBlockRepository } if (attempts > MAX_ATTEMPTS) { - this.logger.error(`[getBlockHeight] The block ${blockNumber} never ended`); + this.logger.warn(`[getBlockHeight] The block ${blockNumber} never ended`); throw new Error(`The block ${blockNumber} never ended`); } diff --git a/blockchain-watcher/test/domain/actions/evm/GetEvmTransactions.test.ts b/blockchain-watcher/test/domain/actions/evm/GetEvmTransactions.test.ts new file mode 100644 index 00000000..691064b1 --- /dev/null +++ b/blockchain-watcher/test/domain/actions/evm/GetEvmTransactions.test.ts @@ -0,0 +1,177 @@ +import { afterAll, afterEach, describe, it, expect, jest } from "@jest/globals"; +import { GetEvmTransactions } from "../../../../src/domain/actions/evm/GetEvmTransactions"; +import { EvmBlockRepository } from "../../../../src/domain/repositories"; +import { EvmBlock, EvmLog, ReceiptTransaction } from "../../../../src/domain/entities/evm"; + +let getTransactionReceipt: jest.SpiedFunction; +let getBlockSpy: jest.SpiedFunction; + +let getEvmTransactions: GetEvmTransactions; +let evmBlockRepo: EvmBlockRepository; + +describe("GetEvmTransactions", () => { + afterAll(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should be return empty array, because formBlock is higher than toBlock", async () => { + // Given + const range = { + fromBlock: 10n, + toBlock: 1n, + }; + + const opts = { + addresses: [], + topics: [], + chain: "ethereum", + environment: "testnet", + }; + + givenPollEvmLogs(); + + // When + const result = getEvmTransactions.execute(range, opts); + + // Then + result.then((response) => { + expect(response).toEqual([]); + }); + }); + + it("should be return empty array, because do not match any contract address with transaction address", async () => { + // Given + const range = { + fromBlock: 1n, + toBlock: 1n, + }; + + const opts = { + addresses: ["0x1ee18b2214aff97000d974cf647e7c545e8fa585"], + topics: [], + chain: "ethereum", + environment: "mainnet", + }; + + givenEvmBlockRepository(range.fromBlock, range.toBlock); + givenPollEvmLogs(); + + // When + const result = getEvmTransactions.execute(range, opts); + + // Then + result.then((response) => { + expect(response).toEqual([]); + expect(getBlockSpy).toHaveReturnedTimes(1); + }); + }); + + it("should be return array with one transaction filter and populated", async () => { + // Given + const range = { + fromBlock: 1n, + toBlock: 1n, + }; + + const opts = { + addresses: ["0x3ee18b2214aff97000d974cf647e7c347e8fa585"], + topics: [], + chain: "ethereum", + environment: "mainnet", + }; + + givenEvmBlockRepository(range.fromBlock, range.toBlock); + givenPollEvmLogs(); + + // When + const result = getEvmTransactions.execute(range, opts); + + // Then + result.then((response) => { + expect(response.length).toEqual(1); + expect(response[0].chainId).toEqual(1); + expect(response[0].status).toEqual("0x1"); + expect(response[0].from).toEqual("0x3ee123456786797000d974cf647e7c347e8fa585"); + expect(response[0].to).toEqual("0x3ee18b2214aff97000d974cf647e7c347e8fa585"); + expect(getTransactionReceipt).toHaveReturnedTimes(1); + expect(getBlockSpy).toHaveReturnedTimes(1); + }); + }); +}); + +const givenEvmBlockRepository = (height?: bigint, blocksAhead?: bigint) => { + const logsResponse: EvmLog[] = []; + const blocksResponse: Record = {}; + const receiptResponse: Record = {}; + if (height) { + for (let index = 0n; index <= (blocksAhead ?? 1n); index++) { + logsResponse.push({ + blockNumber: height + index, + blockHash: `0x0${index}`, + blockTime: 0, + address: "", + removed: false, + data: "", + transactionHash: "", + transactionIndex: "", + topics: [], + logIndex: 0, + chainId: 2, + }); + blocksResponse[`0x0${index}`] = { + timestamp: 0, + hash: `huohugigiyyff6677rr657s7xr8copi`, + number: height + index, + transactions: [ + { + blockHash: "0xf5794b0970386d7951e45465ac2c9835537e5a9", + hash: "dasdasfpialsfijlasfsahuf", + blockNumber: 1n, + chainId: 1, + from: "0x3ee123456786797000d974cf647e7c347e8fa585", + gas: "0x14485", + gasPrice: "0xfc518561e", + input: "0xc687851912312444wadadswadwd", + maxFeePerGas: "0x1610f75b9a", + maxPriorityFeePerGas: "0x5f5e100", + nonce: "0x1", + r: "0xf5794b0970386d73b693b17f147fae0427db278e951e45465ac2c9835537e5a9", + s: "0x6dccc8cfee216bc43a9d66525fa94905da234ad32d6cc3220845bef78f25dd42", + status: "0x1", + timestamp: 12313123, + to: "0x3ee18b2214aff97000d974cf647e7c347e8fa585", + transactionIndex: "0x6f", + type: "0x2", + v: "0x1", + value: "0x5b09cd3e5e90000", + environment: "testnet", + chain: "ethereum", + }, + ], + }; + receiptResponse["dasdasfpialsfijlasfsahuf"] = { + status: "0x1", + transactionHash: "dasdasfpialsfijlasfsahuf", + }; + } + } + + evmBlockRepo = { + getBlocks: () => Promise.resolve(blocksResponse), + getBlockHeight: () => Promise.resolve(height ? height + (blocksAhead ?? 10n) : 10n), + getFilteredLogs: () => Promise.resolve(logsResponse), + getTransactionReceipt: () => Promise.resolve(receiptResponse), + getBlock: () => Promise.resolve(blocksResponse[`0x01`]), + }; + + getBlockSpy = jest.spyOn(evmBlockRepo, "getBlock"); + getTransactionReceipt = jest.spyOn(evmBlockRepo, "getTransactionReceipt"); +}; + +const givenPollEvmLogs = () => { + getEvmTransactions = new GetEvmTransactions(evmBlockRepo); +}; diff --git a/blockchain-watcher/test/domain/actions/evm/PollEvmLogs.test.ts b/blockchain-watcher/test/domain/actions/evm/PollEvm.test.ts similarity index 88% rename from blockchain-watcher/test/domain/actions/evm/PollEvmLogs.test.ts rename to blockchain-watcher/test/domain/actions/evm/PollEvm.test.ts index 8c96ac0f..33cf113a 100644 --- a/blockchain-watcher/test/domain/actions/evm/PollEvmLogs.test.ts +++ b/blockchain-watcher/test/domain/actions/evm/PollEvm.test.ts @@ -1,16 +1,12 @@ import { afterEach, describe, it, expect, jest } from "@jest/globals"; import { setTimeout } from "timers/promises"; -import { - PollEvmLogsMetadata, - PollEvmLogs, - PollEvmLogsConfig, -} from "../../../../src/domain/actions"; +import { PollEvmLogsMetadata, PollEvm, PollEvmLogsConfig } from "../../../../src/domain/actions"; import { EvmBlockRepository, MetadataRepository, StatRepository, } from "../../../../src/domain/repositories"; -import { EvmBlock, EvmLog } from "../../../../src/domain/entities"; +import { EvmBlock, EvmLog, ReceiptTransaction } from "../../../../src/domain/entities"; let cfg = PollEvmLogsConfig.fromBlock("acala", 0n); @@ -27,11 +23,11 @@ let handlers = { working: (logs: EvmLog[]) => Promise.resolve(), failing: (logs: EvmLog[]) => Promise.reject(), }; -let pollEvmLogs: PollEvmLogs; +let pollEvm: PollEvm; -describe("PollEvmLogs", () => { +describe("PollEvm", () => { afterEach(async () => { - await pollEvmLogs.stop(); + await pollEvm.stop(); }); it("should be able to read logs from latest block when no fromBlock is configured", async () => { @@ -109,6 +105,7 @@ describe("PollEvmLogs", () => { const givenEvmBlockRepository = (height?: bigint, blocksAhead?: bigint) => { const logsResponse: EvmLog[] = []; const blocksResponse: Record = {}; + const receiptResponse: Record = {}; if (height) { for (let index = 0n; index <= (blocksAhead ?? 1n); index++) { logsResponse.push({ @@ -129,6 +126,10 @@ const givenEvmBlockRepository = (height?: bigint, blocksAhead?: bigint) => { hash: `0x0${index}`, number: height + index, }; + receiptResponse[`0x0${index}`] = { + status: "0x1", + transactionHash: `0x0${index}`, + }; } } @@ -136,6 +137,8 @@ const givenEvmBlockRepository = (height?: bigint, blocksAhead?: bigint) => { getBlocks: () => Promise.resolve(blocksResponse), getBlockHeight: () => Promise.resolve(height ? height + (blocksAhead ?? 10n) : 10n), getFilteredLogs: () => Promise.resolve(logsResponse), + getTransactionReceipt: () => Promise.resolve(receiptResponse), + getBlock: () => Promise.resolve(blocksResponse[0]), }; getBlocksSpy = jest.spyOn(evmBlockRepo, "getBlocks"); @@ -161,11 +164,11 @@ const givenStatsRepository = () => { const givenPollEvmLogs = (from?: bigint) => { cfg.setFromBlock(from); - pollEvmLogs = new PollEvmLogs(evmBlockRepo, metadataRepo, statsRepo, cfg); + pollEvm = new PollEvm(evmBlockRepo, metadataRepo, statsRepo, cfg, "GetEvmLogs"); }; const whenPollEvmLogsStarts = async () => { - pollEvmLogs.run([handlers.working]); + pollEvm.run([handlers.working]); }; const thenWaitForAssertion = async (...assertions: (() => void)[]) => { diff --git a/blockchain-watcher/test/domain/actions/evm/mappers/methodNameByAddressMapper.test.ts b/blockchain-watcher/test/domain/actions/evm/mappers/methodNameByAddressMapper.test.ts new file mode 100644 index 00000000..8a592a67 --- /dev/null +++ b/blockchain-watcher/test/domain/actions/evm/mappers/methodNameByAddressMapper.test.ts @@ -0,0 +1,96 @@ +import { methodNameByAddressMapper } from "../../../../../src/domain/actions/evm/mappers/methodNameByAddressMapper"; +import { describe, it, expect } from "@jest/globals"; + +describe("methodNameByAddressMapper", () => { + it("should be throw error because cannot find method name in testnet environment", async () => { + // Given + const transaction = getTransactions( + "0xF890982f9310df57d00f659cf4fd87e65adEd8d7", + "0xc65465587851912312421412124124" + ); + const environment = "testnet"; + const chain = "ethereum"; + + // When + const result = methodNameByAddressMapper(chain, environment, transaction); + + // Then + expect(result).toBeUndefined(); + }); + + it("should be return a method name in testnet environment", async () => { + // Given + const transaction = getTransactions( + "0xF890982f9310df57d00f659cf4fd87e65adEd8d7", + "0xc687851912312421412124124" + ); + const environment = "testnet"; + const chain = "ethereum"; + + // When + const result = methodNameByAddressMapper(chain, environment, transaction); + + // Then + expect(result).toEqual({ method: "MethodCompleteTransfer", name: "transfer-redeemed" }); + }); + + it("should be throw error because cannot find method name in in mainnet environment", async () => { + // Given + const transaction = getTransactions( + "0x3ee18B2214AFF97000D974cf647E7C347E8fa585", + "0xc65465587851912312421412124124" + ); + const environment = "mainnet"; + const chain = "ethereum"; + + // When + const result = methodNameByAddressMapper(chain, environment, transaction); + + // Then + expect(result).toBeUndefined(); + }); + + it("should be return a method name in mainnet environment", async () => { + // Given + const transaction = getTransactions( + "0x3ee18B2214AFF97000D974cf647E7C347E8fa585", + "0xc687851912312421412124124" + ); + const environment = "mainnet"; + const chain = "ethereum"; + + // When + const result = methodNameByAddressMapper(chain, environment, transaction); + + // Then + expect(result).toEqual({ method: "MethodCompleteTransfer", name: "transfer-redeemed" }); + }); +}); + +const getTransactions = (to: string, input: string) => { + return { + blockHash: "0x1359819238ea89f49c20e42eb5603bf0541589d838d971984b60c7cdb391d9c2", + blockNumber: 0x11ec2bcn, + chainId: 1, + from: "0xfb070adcd21361a3946a0584dc84a7b89faa68e3", + gas: "0x14485", + gasPrice: "0xfc518561e", + hash: "0x612a35f6739f70a81dfc34448c68e99dbcfe8dafaf241edbaa204cf0e236494d", + input: input.toLowerCase(), + maxFeePerGas: "0x1610f75b9a", + maxPriorityFeePerGas: "0x5f5e100", + methodsByAddress: undefined, + nonce: "0x1", + r: "0xf5794b0970386d73b693b17f147fae0427db278e951e45465ac2c9835537e5a9", + s: "0x6dccc8cfee216bc43a9d66525fa94905da234ad32d6cc3220845bef78f25dd42", + status: "0x1", + timestamp: 1702663079, + to: to.toLowerCase(), + transactionIndex: "0x6f", + type: "0x2", + v: "0x1", + value: "0x5b09cd3e5e90000", + environment: "testnet", + chain: "ethereum", + }; +}; diff --git a/blockchain-watcher/test/infrastructure/mappers/evmStandardRelayDelivered.test.ts b/blockchain-watcher/test/infrastructure/mappers/evmStandardRelayDelivered.test.ts deleted file mode 100644 index 4473f1f3..00000000 --- a/blockchain-watcher/test/infrastructure/mappers/evmStandardRelayDelivered.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { describe, it, expect } from "@jest/globals"; -import { evmStandardRelayDelivered } from "../../../src/infrastructure/mappers/evmStandardRelayDelivered"; -import { HandleEvmLogs } from "../../../src/domain/actions"; - -const address = "0x27428dd2d3dd32a4d7f7c497eaaa23130d894911"; -const topic = "0xbccc00b713f54173962e7de6098f643d8ebf53d488d71f4b2a5171496d038f9e"; -const txHash = "0xcbdefc83080a8f60cbde7785eb2978548fd5c1f7d0ea2c024cce537845d339c7"; - -const handler = new HandleEvmLogs( - { - filter: { addresses: [address], topics: [topic] }, - abi: "event Delivery(address indexed recipientContract, uint16 indexed sourceChain, uint64 indexed sequence, bytes32 deliveryVaaHash, uint8 status, uint256 gasUsed, uint8 refundStatus, bytes additionalStatusInfo, bytes overridesInfo)", - }, - evmStandardRelayDelivered, - async () => {} -); - -describe("evmStandardRelayDelivered", () => { - it("should be able to map log to TransferRedeeemed", async () => { - const [result] = await handler.handle([ - { - chainId: 2, - address, - blockTime: 1699443287, - transactionHash: txHash, - topics: [ - "0xbccc00b713f54173962e7de6098f643d8ebf53d488d71f4b2a5171496d038f9e", - "0x000000000000000000000000f80cf52922b512b22d46aa8916bd7767524305d9", - "0x000000000000000000000000000000000000000000000000000000000000001e", - "0x0000000000000000000000000000000000000000000000000000000000000900", - ], - data: "0xf29cac97156fa11c205eda95c0655e4a6e2a9c247245bab4d3d8257c41fc11d200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000013a89000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - blockNumber: 18708316n, - transactionIndex: "0x3b", - blockHash: "0x8c55cbd97c96f8322bed4d1790c7ac4a84b1cff46c157bf86fc35eb5886be451", - logIndex: 5, - removed: false, - }, - ]); - - expect(result.name).toBe("standard-relay-delivered"); - expect(result.chainId).toBe(2); - expect(result.txHash).toBe(txHash); - expect(result.blockHeight).toBe(18708316n); - expect(result.blockTime).toBe(1699443287); - - expect(result.attributes.recipientContract.toLowerCase()).toBe( - "0xf80cf52922b512b22d46aa8916bd7767524305d9" - ); - expect(result.attributes.sourceChain).toBe(30); - expect(result.attributes.sequence).toBe(2304); - expect(result.attributes.deliveryVaaHash.toLowerCase()).toBe( - "0xf29cac97156fa11c205eda95c0655e4a6e2a9c247245bab4d3d8257c41fc11d2" - ); - expect(result.attributes.status).toBe(0); - expect(result.attributes.gasUsed).toBe(80521); - expect(result.attributes.refundStatus).toBe(0); - }); -}); diff --git a/blockchain-watcher/test/infrastructure/mappers/evmTransactionFoundMapper.test.ts b/blockchain-watcher/test/infrastructure/mappers/evmTransactionFoundMapper.test.ts new file mode 100644 index 00000000..99685ebf --- /dev/null +++ b/blockchain-watcher/test/infrastructure/mappers/evmTransactionFoundMapper.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect } from "@jest/globals"; +import { evmTransactionFoundMapper } from "../../../src/infrastructure/mappers/evmTransactionFoundMapper"; +import { HandleEvmTransactions } from "../../../src/domain/actions"; + +const address = "0xf890982f9310df57d00f659cf4fd87e65aded8d7"; +const topic = "0xbccc00b713f54173962e7de6098f643d8ebf53d488d71f4b2a5171496d038f9e"; +const txHash = "0x612a35f6739f70a81dfc34448c68e99dbcfe8dafaf241edbaa204cf0e236494d"; + +const handler = new HandleEvmTransactions( + { + filter: { addresses: [address], topics: [topic] }, + abi: "event Delivery(address indexed recipientContract, uint16 indexed sourceChain, uint64 indexed sequence, bytes32 deliveryVaaHash, uint8 status, uint256 gasUsed, uint8 refundStatus, bytes additionalStatusInfo, bytes overridesInfo)", + }, + evmTransactionFoundMapper, + async () => {} +); + +describe("evmTransactionFoundMapper", () => { + it("should be able to map log to evmTransactionFoundMapper", async () => { + // When + const [result] = await handler.handle([ + { + blockHash: "0x612a35f6739f70a81dfc34448c68e99dbcfe8dafaf241edbaa204cf0e236494d", + blockNumber: 0x11ec2bcn, + chainId: 1, + from: "0xfb070adcd21361a3946a0584dc84a7b89faa68e3", + gas: "0x14485", + gasPrice: "0xfc518561e", + hash: "0x612a35f6739f70a81dfc34448c68e99dbcfe8dafaf241edbaa204cf0e236494d", + input: + "0xc68785190000000000000000000000000000000000000000000000000000000000000001637651ef71f834be28b8fab1dce9c228c2fe1813831bbc3673cfd3abde6dbb3d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080420000", + maxFeePerGas: "0x1610f75b9a", + maxPriorityFeePerGas: "0x5f5e100", + nonce: "0x1", + r: "0xf5794b0970386d73b693b17f147fae0427db278e951e45465ac2c9835537e5a9", + s: "0x6dccc8cfee216bc43a9d66525fa94905da234ad32d6cc3220845bef78f25dd42", + status: "0x1", + timestamp: 1702663079, + to: "0xf890982f9310df57d00f659cf4fd87e65aded8d7", + transactionIndex: "0x6f", + type: "0x2", + v: "0x1", + value: "0x5b09cd3e5e90000", + environment: "testnet", + chain: "ethereum", + }, + ]); + + // Then + expect(result.name).toBe("evm-transaction-found"); + expect(result.chainId).toBe(1); + expect(result.txHash).toBe(txHash); + expect(result.blockHeight).toBe(18793148n); + expect(result.attributes.blockNumber).toBe(18793148n); + expect(result.attributes.from).toBe("0xfb070adcd21361a3946a0584dc84a7b89faa68e3"); + expect(result.attributes.to).toBe("0xf890982f9310df57d00f659cf4fd87e65aded8d7"); + expect(result.attributes.methodsByAddress).toBe("MethodCompleteTransfer"); + expect(result.attributes.name).toBe("transfer-redeemed"); + }); +}); diff --git a/blockchain-watcher/test/infrastructure/mappers/evmTransferRedeemed.test.ts b/blockchain-watcher/test/infrastructure/mappers/evmTransferRedeemed.test.ts deleted file mode 100644 index 0088f7e7..00000000 --- a/blockchain-watcher/test/infrastructure/mappers/evmTransferRedeemed.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { describe, it, expect } from "@jest/globals"; -import { evmTransferRedeemedMapper } from "../../../src/infrastructure/mappers/evmTransferRedeemedMapper"; -import { HandleEvmLogs } from "../../../src/domain/actions"; - -const address = "0x98f3c9e6e3face36baad05fe09d375ef1464288b"; -const topic = "0xcaf280c8cfeba144da67230d9b009c8f868a75bac9a528fa0474be1ba317c169"; -const txHash = "0xcbdefc83080a8f60cbde7785eb2978548fd5c1f7d0ea2c024cce537845d339c7"; - -const handler = new HandleEvmLogs( - { - filter: { addresses: [address], topics: [topic] }, - abi: "event TransferRedeemed(uint16 indexed emitterChainId, bytes32 indexed emitterAddress, uint64 indexed sequence)", - }, - evmTransferRedeemedMapper, - async () => {} -); - -describe("evmTransferRedeemed", () => { - it("should be able to map log to TransferRedeeemed", async () => { - const [result] = await handler.handle([ - { - chainId: 2, - address, - topics: [ - "0xcaf280c8cfeba144da67230d9b009c8f868a75bac9a528fa0474be1ba317c169", - "0x0000000000000000000000000000000000000000000000000000000000000001", - "0xec7372995d5cc8732397fb0ad35c0121e0eaa90d26f828a534cab54391b3a4f5", - "0x0000000000000000000000000000000000000000000000000000000000052a3e", - ], - data: "0x", - blockNumber: 18708192n, - blockTime: 1699443287, - transactionHash: txHash, - transactionIndex: "0x3e", - blockHash: "0x241fa85f3494c654d59859b46af586bd43f37ec434f5cf0018a53e46c42da393", - logIndex: 216, - removed: false, - }, - ]); - - expect(result.name).toBe("transfer-redeemed"); - expect(result.chainId).toBe(2); - expect(result.txHash).toBe(txHash); - expect(result.blockHeight).toBe(18708192n); - expect(result.blockTime).toBe(1699443287); - - expect(result.attributes.sequence).toBe(338494); - expect(result.attributes.emitterAddress.toLowerCase()).toBe( - "0xec7372995d5cc8732397fb0ad35c0121e0eaa90d26f828a534cab54391b3a4f5" - ); - expect(result.attributes.emitterChainId).toBe(1); - }); -}); diff --git a/blockchain-watcher/test/infrastructure/repositories/StaticJobRepository.test.ts b/blockchain-watcher/test/infrastructure/repositories/StaticJobRepository.test.ts index eaa2bd50..31d0e8ec 100644 --- a/blockchain-watcher/test/infrastructure/repositories/StaticJobRepository.test.ts +++ b/blockchain-watcher/test/infrastructure/repositories/StaticJobRepository.test.ts @@ -22,7 +22,7 @@ describe("StaticJobRepository", () => { if (fs.existsSync(dirPath)) { fs.rmSync(dirPath, { recursive: true, force: true }); } - repo = new StaticJobRepository(dirPath, false, () => blockRepo, { + repo = new StaticJobRepository("testnet", dirPath, false, () => blockRepo, { metadataRepo, statsRepo, snsRepo, @@ -49,7 +49,7 @@ const givenJobsPresent = () => { id: "poll-log-message-published-ethereum", chain: "ethereum", source: { - action: "PollEvmLogs", + action: "PollEvm", config: { fromBlock: 10012499n, blockBatchSize: 100, diff --git a/deploy/blockchain-watcher/workers/ethereum-1.yaml b/deploy/blockchain-watcher/workers/ethereum-1.yaml index ca84e48b..0ab14bf5 100644 --- a/deploy/blockchain-watcher/workers/ethereum-1.yaml +++ b/deploy/blockchain-watcher/workers/ethereum-1.yaml @@ -40,7 +40,7 @@ data: "id": "poll-log-message-published-optimism", "chain": "optimism", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "latest", @@ -68,7 +68,7 @@ data: "id": "poll-log-message-published-base", "chain": "base", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "finalized", @@ -96,7 +96,7 @@ data: "id": "poll-log-message-published-celo", "chain": "celo", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "latest", @@ -124,7 +124,7 @@ data: "id": "poll-log-message-published-oasis", "chain": "oasis", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "latest", @@ -152,7 +152,7 @@ data: "id": "poll-log-message-published-klaytn", "chain": "klaytn", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "latest", @@ -180,7 +180,7 @@ data: "id": "poll-log-message-published-arbitrum", "chain": "arbitrum", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "latest", @@ -208,7 +208,7 @@ data: "id": "poll-log-message-published-polygon", "chain": "polygon", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "latest", @@ -239,7 +239,7 @@ data: "id": "poll-log-message-published-optimism", "chain": "optimism", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "latest", @@ -267,7 +267,7 @@ data: "id": "poll-log-message-published-base", "chain": "base", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "finalized", @@ -295,7 +295,7 @@ data: "id": "poll-log-message-published-celo", "chain": "celo", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "latest", @@ -323,7 +323,7 @@ data: "id": "poll-log-message-published-oasis", "chain": "oasis", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "latest", @@ -351,7 +351,7 @@ data: "id": "poll-log-message-published-klaytn", "chain": "klaytn", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "latest", @@ -379,7 +379,7 @@ data: "id": "poll-log-message-published-arbitrum", "chain": "arbitrum", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "latest", @@ -407,7 +407,7 @@ data: "id": "poll-log-message-published-polygon", "chain": "polygon", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "latest", diff --git a/deploy/blockchain-watcher/workers/ethereum.yaml b/deploy/blockchain-watcher/workers/ethereum.yaml index f912f373..aca89a2c 100644 --- a/deploy/blockchain-watcher/workers/ethereum.yaml +++ b/deploy/blockchain-watcher/workers/ethereum.yaml @@ -40,7 +40,7 @@ data: "id": "poll-log-message-published-ethereum", "chain": "ethereum", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "fromBlock": "10012499", "blockBatchSize": 100, @@ -69,7 +69,7 @@ data: "id": "poll-log-message-published-karura", "chain": "karura", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "finalized", @@ -103,7 +103,7 @@ data: "id": "poll-log-message-published-fantom", "chain": "fantom", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "latest", @@ -137,7 +137,7 @@ data: "id": "poll-log-message-published-acala", "chain": "acala", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "finalized", @@ -171,7 +171,7 @@ data: "id": "poll-log-message-published-avalanche", "chain": "avalanche", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "finalized", @@ -206,7 +206,7 @@ data: "id": "poll-log-message-published-bsc", "chain": "bsc", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "latest", @@ -234,7 +234,7 @@ data: "id": "poll-log-message-published-moonbeam", "chain": "moonbeam", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "latest", @@ -265,7 +265,7 @@ data: "id": "poll-log-message-published-ethereum", "chain": "ethereum", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "latest", @@ -293,7 +293,7 @@ data: "id": "poll-log-message-published-karura", "chain": "karura", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "finalized", @@ -327,7 +327,7 @@ data: "id": "poll-log-message-published-fantom", "chain": "fantom", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "latest", @@ -361,7 +361,7 @@ data: "id": "poll-log-message-published-acala", "chain": "acala", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "finalized", @@ -395,7 +395,7 @@ data: "id": "poll-log-message-published-avalanche", "chain": "avalanche", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "finalized", @@ -430,7 +430,7 @@ data: "id": "poll-log-message-published-bsc", "chain": "bsc", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "latest", @@ -458,7 +458,7 @@ data: "id": "poll-log-message-published-moonbeam", "chain": "moonbeam", "source": { - "action": "PollEvmLogs", + "action": "PollEvm", "config": { "blockBatchSize": 100, "commitment": "latest",