[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 <julianmerlo@julians-MacBook-Pro.local>
This commit is contained in:
Julian 2023-12-22 17:50:38 -03:00 committed by GitHub
parent 076338f63c
commit d7dae2413e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1161 additions and 328 deletions

View File

@ -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.

View File

@ -1,4 +1,5 @@
{
"environment": "mainnet",
"chains": {
"solana": {
"network": "mainnet-beta",

View File

@ -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

View File

@ -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
}
}
}

View File

@ -11,17 +11,18 @@ export class GetEvmLogs {
this.logger = winston.child({ module: "GetEvmLogs" });
}
async execute(range: Range, opts: GetEvmLogsOpts): Promise<EvmLog[]> {
if (range.fromBlock > range.toBlock) {
this.logger.info(
`[exec] Invalid range [fromBlock: ${range.fromBlock} - toBlock: ${range.toBlock}]`
);
async execute(range: Range, opts: GetEvmOpts): Promise<EvmLog[]> {
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;
};

View File

@ -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<EvmTransaction[]> {
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<EvmTransaction[]> {
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;
};

View File

@ -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<T> {
cfg: HandleEvmLogsConfig;
mapper: (log: EvmTransaction) => T;
target: (parsed: T[]) => Promise<void>;
constructor(
cfg: HandleEvmLogsConfig,
mapper: (log: EvmTransaction) => T,
target: (parsed: T[]) => Promise<void>
) {
this.cfg = this.normalizeCfg(cfg);
this.mapper = mapper;
this.target = target;
}
public async handle(transactions: EvmTransaction[]): Promise<T[]> {
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,
};
}
}

View File

@ -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<PollEvmLogsMetadata>;
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<PollEvmLogsMetadata>,
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<void> {
@ -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<EvmLog[]> {
protected async get(): Promise<EvmLog[] | EvmTransaction[]> {
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<void> {
@ -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: "" });
}
}

View File

@ -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<string, Protocol>([
[
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<string, Protocol>([
[
MethodID.MethodCompleteTransferWithRelay,
{ method: "MethodCompleteTransferWithRelay", name: "standard-relay-delivered" },
],
]);
const receiveMessageAndSwap = new Map<string, Protocol>([
[MethodID.MethodIDReceiveMessageAndSwap, { method: "MethodReceiveMessageAndSwap", name: "" }], // TODO: When active this protocol set the name
]);
const receiveTbtc = new Map<string, Protocol>([
[MethodID.MethodIDReceiveTbtc, { method: "MethodReceiveTbtc", name: "" }], // TODO: When active this protocol set the name
]);
const base = new Map<string, Protocol>([...ethBase, ...completeTransferWithRelay]);
type MethodsByAddress = {
[chain: string]: {
[address: string]: Map<string, Protocol>;
}[];
};
type Protocol = {
method: string;
name: string;
};

View File

@ -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";

View File

@ -33,3 +33,35 @@ export type StandardRelayDelivered = {
additionalStatusInfo: string;
overridesInfo: string;
};
export type TransactionFoundEvent<T> = {
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;
};

View File

@ -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;
};

View File

@ -3,6 +3,7 @@ export class JobDefinition {
chain: string;
source: {
action: string;
records: string;
config: Record<string, any>;
};
handlers: {
@ -15,7 +16,7 @@ export class JobDefinition {
constructor(
id: string,
chain: string,
source: { action: string; config: Record<string, any> },
source: { action: string; records: string; config: Record<string, any> },
handlers: { action: string; target: string; mapper: string; config: Record<string, any> }[]
) {
this.id = id;

View File

@ -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<bigint>;
getBlocks(chain: string, blockNumbers: Set<bigint>): Promise<Record<string, EvmBlock>>;
getFilteredLogs(chain: string, filter: EvmLogFilter): Promise<EvmLog[]>;
getTransactionReceipt(
chain: string,
hashNumbers: Set<string>
): Promise<Record<string, ReceiptTransaction>>;
getBlock(
chain: string,
blockNumberOrTag: EvmTag | bigint,
isTransactionsPresent: boolean
): Promise<EvmBlock>;
}
export interface SolanaSlotRepository {

View File

@ -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<any>
): LogFoundEvent<StandardRelayDelivered> => {
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],
},
};
};

View File

@ -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<TransactionFound> => {
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,
},
};
};

View File

@ -1,25 +0,0 @@
import { BigNumber } from "ethers";
import { EvmLog, LogFoundEvent, TransferRedeemed } from "../../domain/entities";
export const evmTransferRedeemedMapper = (
log: EvmLog,
_: ReadonlyArray<any>
): LogFoundEvent<TransferRedeemed> => {
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(),
},
};
};

View File

@ -1,5 +1,4 @@
export * from "./evmLogMessagePublishedMapper";
export * from "./evmTransferRedeemedMapper";
export * from "./evmStandardRelayDelivered";
export * from "./evmTransactionFoundMapper";
export * from "./solanaLogMessagePublishedMapper";
export * from "./solanaTransferRedeemedMapper";

View File

@ -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),

View File

@ -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<string, (def: JobDefinition) => RunPollingJob> = new Map();
private handlers: Map<string, (cfg: any, target: string, mapper: any) => Promise<Handler>> =
@ -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<LogFoundEvent<any>>(
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);
}

View File

@ -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<EvmBlock> {
async getBlock(
chain: string,
blockNumberOrTag: EvmTag | bigint,
isTransactionsPresent: boolean = false
): Promise<EvmBlock> {
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<string>
): Promise<Record<string, ReceiptTransaction>> {
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<typeof results>(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<string, ReceiptTransaction>, 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) {

View File

@ -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`);
}

View File

@ -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<EvmBlockRepository["getTransactionReceipt"]>;
let getBlockSpy: jest.SpiedFunction<EvmBlockRepository["getBlock"]>;
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<string, EvmBlock> = {};
const receiptResponse: Record<string, ReceiptTransaction> = {};
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);
};

View File

@ -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<string, EvmBlock> = {};
const receiptResponse: Record<string, ReceiptTransaction> = {};
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)[]) => {

View File

@ -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",
};
};

View File

@ -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);
});
});

View File

@ -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");
});
});

View File

@ -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);
});
});

View File

@ -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,

View File

@ -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",

View File

@ -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",