[Blockchain Watcher] (WORMCHAIN) Mapped wormchain source events (#1238)

* Mapped source events

* Set chainId for wormchain

* Add test

* Mapped testnet rpc

* Change blockBatchSize for 10 times per execution

* Improve log

* Improve code

* Remove console.log

* Resolved issue mapping tx hash

* Mapped txs array

* Add new test with 2 txs mapped

* Adapt endpoints implementation

* Improve log

* Resolve comment in PR

* Invert params in mapper

* Resolve test

* Resolve comment in PR

---------

Co-authored-by: julian merlo <julianmerlo@julians-MacBook-Pro.local>
This commit is contained in:
Julian 2024-04-16 11:02:13 -03:00 committed by GitHub
parent c41ffccf39
commit 408a297b63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1875 additions and 95 deletions

View File

@ -3,7 +3,7 @@
"port": 9090,
"logLevel": "debug",
"dryRun": true,
"enabledPlatforms": ["solana", "evm", "sui", "aptos"],
"enabledPlatforms": ["solana", "evm", "sui", "aptos", "wormchain"],
"sns": {
"topicArn": "arn:aws:sns:us-east-1:000000000000:localstack-topic.fifo",
"region": "us-east-1",
@ -209,6 +209,17 @@
"chainId": 10006,
"rpcs": ["https://rpc.ankr.com/eth_holesky"],
"timeout": 10000
},
"wormchain": {
"name": "wormchain",
"network": "testnet",
"chainId": 3104,
"rpcs": [
"https://gateway-01.testnet.xlabs.xyz",
"https://gateway-02.testnet.xlabs.xyz",
"https://gateway-03.testnet.xlabs.xyz"
],
"timeout": 10000
}
}
}

View File

@ -118,6 +118,10 @@
"network": "mainnet",
"rpcs": ["https://fullnode.mainnet.aptoslabs.com/v1"]
},
"wormchain": {
"network": "mainnet",
"rpcs": ["https://tncnt-eu-wormchain-main-01.rpc.p2p.world"]
},
"scroll": {
"network": "mainnet",
"rpcs": [

View File

@ -1,7 +1,7 @@
import { EvmLog, EvmTopicFilter } from "../../entities";
import { HandleEvmLogsConfig } from "./types";
import { StatRepository } from "../../repositories";
import { ethers } from "ethers";
import { HandleEvmLogsConfig } from "./types";
import { EvmLog } from "../../entities";
/**
* Handling means mapping and forward to a given target.

View File

@ -5,5 +5,12 @@ export * from "./evm/PollEvm";
export * from "./evm/types";
export * from "./solana/GetSolanaTransactions";
export * from "./solana/PollSolanaTransactions";
export * from "./wormchain/HandleWormchainLogs";
export * from "./wormchain/GetWormchainLogs";
export * from "./wormchain/PollWormchain";
export * from "./aptos/GetAptosTransactions";
export * from "./aptos/GetAptosTransactionsByEvents";
export * from "./aptos/HandleAptosTransactions";
export * from "./aptos/PollAptos";
export * from "./RunPollingJob";
export * from "./StartJobs";

View File

@ -0,0 +1,54 @@
import { WormchainRepository } from "../../repositories";
import { WormchainBlockLogs } from "../../entities/wormchain";
import winston from "winston";
export class GetWormchainLogs {
private readonly blockRepo: WormchainRepository;
protected readonly logger: winston.Logger;
constructor(blockRepo: WormchainRepository) {
this.logger = winston.child({ module: "GetWormchainLogs" });
this.blockRepo = blockRepo;
}
async execute(
range: Range,
opts: { addresses: string[]; chainId: number }
): Promise<WormchainBlockLogs[]> {
const fromBlock = range.fromBlock;
const toBlock = range.toBlock;
const collectWormchainLogs: WormchainBlockLogs[] = [];
if (fromBlock > toBlock) {
this.logger.info(
`[wormchain][exec] Invalid range [fromBlock: ${fromBlock} - toBlock: ${toBlock}]`
);
return [];
}
for (let blockNumber = fromBlock; blockNumber <= toBlock; blockNumber++) {
const wormchainLogs = await this.blockRepo.getBlockLogs(opts.chainId, blockNumber);
if (wormchainLogs && wormchainLogs.transactions && wormchainLogs.transactions.length > 0) {
collectWormchainLogs.push(wormchainLogs);
}
}
this.logger.info(
`[wormchain][exec] Got ${
collectWormchainLogs?.length
} transactions to process for ${this.populateLog(opts, fromBlock, toBlock)}`
);
return collectWormchainLogs;
}
private populateLog(opts: { addresses: string[] }, fromBlock: bigint, toBlock: bigint): string {
return `[addresses:${opts.addresses}][blocks:${fromBlock} - ${toBlock}]`;
}
}
type Range = {
fromBlock: bigint;
toBlock: bigint;
};

View File

@ -0,0 +1,48 @@
import { TransactionFoundEvent } from "../../entities";
import { WormchainBlockLogs } from "../../entities/wormchain";
import { StatRepository } from "../../repositories";
export class HandleWormchainLogs {
constructor(
private readonly cfg: HandleWormchainLogsOptions,
private readonly mapper: (
addresses: string[],
tx: WormchainBlockLogs
) => TransactionFoundEvent[],
private readonly target: (parsed: TransactionFoundEvent[]) => Promise<void>,
private readonly statsRepo: StatRepository
) {}
public async handle(logs: WormchainBlockLogs[]): Promise<TransactionFoundEvent[]> {
const filterLogs: TransactionFoundEvent[] = [];
logs.forEach((log) => {
const logMapped = this.mapper(this.cfg.filter.addresses, log);
if (logMapped.length > 0) {
logMapped.forEach((log) => {
this.report();
filterLogs.push(log);
});
}
});
await this.target(filterLogs);
return filterLogs;
}
private report() {
const labels = {
commitment: "immediate",
chain: "wormchain",
job: this.cfg.id,
};
this.statsRepo.count(this.cfg.metricName, labels);
}
}
export interface HandleWormchainLogsOptions {
metricName: string;
filter: { addresses: string[] };
id: string;
}

View File

@ -0,0 +1,203 @@
import { MetadataRepository, StatRepository, WormchainRepository } from "../../repositories";
import { GetWormchainLogs } from "./GetWormchainLogs";
import { RunPollingJob } from "../RunPollingJob";
import winston from "winston";
const ID = "watch-wormchain-logs";
export class PollWormchain extends RunPollingJob {
protected readonly logger: winston.Logger;
private readonly metadataRepo: MetadataRepository<PollWormchainLogsMetadata>;
private readonly getWormchain: GetWormchainLogs;
private readonly blockRepo: WormchainRepository;
private readonly statsRepo: StatRepository;
private latestBlockHeight?: bigint;
private blockHeightCursor?: bigint;
private lastRange?: { fromBlock: bigint; toBlock: bigint };
private cfg: PollWormchainLogsConfig;
private getWormchainRecords: { [key: string]: any } = {
GetWormchainLogs,
};
constructor(
blockRepo: WormchainRepository,
metadataRepo: MetadataRepository<PollWormchainLogsMetadata>,
statsRepo: StatRepository,
cfg: PollWormchainLogsConfig,
getWormchain: string
) {
super(cfg.id, statsRepo, cfg.interval);
this.blockRepo = blockRepo;
this.metadataRepo = metadataRepo;
this.statsRepo = statsRepo;
this.cfg = cfg;
this.logger = winston.child({ module: "PollWormchain", label: this.cfg.id });
this.getWormchain = new this.getWormchainRecords[getWormchain ?? "GetWormchainLogs"](blockRepo);
}
protected async preHook(): Promise<void> {
const metadata = await this.metadataRepo.get(this.cfg.id);
if (metadata) {
this.blockHeightCursor = BigInt(metadata.lastBlock);
}
}
protected async hasNext(): Promise<boolean> {
const hasFinished = this.cfg.hasFinished(this.blockHeightCursor);
if (hasFinished) {
this.logger.info(
`[hasNext] PollWormchain: (${this.cfg.id}) Finished processing all blocks from ${this.cfg.fromBlock} to ${this.cfg.toBlock}`
);
}
return !hasFinished;
}
protected async get(): Promise<any[]> {
const latestBlockHeight = await this.blockRepo.getBlockHeight();
if (!latestBlockHeight) {
throw new Error(`Could not obtain latest block height: ${latestBlockHeight}`);
}
const range = this.getBlockRange(latestBlockHeight);
const records = await this.getWormchain.execute(range, {
addresses: this.cfg.addresses,
chainId: this.cfg.chainId,
});
this.lastRange = range;
return records;
}
private getBlockRange(latestBlockHeight: bigint): {
fromBlock: bigint;
toBlock: bigint;
} {
let fromBlock = this.blockHeightCursor
? this.blockHeightCursor + 1n
: this.cfg.fromBlock ?? latestBlockHeight;
// fromBlock is configured and is greater than current block height, then we allow to skip blocks
if (
this.blockHeightCursor &&
this.cfg.fromBlock &&
this.cfg.fromBlock > this.blockHeightCursor
) {
fromBlock = this.cfg.fromBlock;
}
let toBlock = BigInt(fromBlock) + BigInt(this.cfg.getBlockBatchSize());
// limit toBlock to obtained block height
if (toBlock > fromBlock && toBlock > latestBlockHeight) {
toBlock = latestBlockHeight;
}
// limit toBlock to configured toBlock
if (this.cfg.toBlock && toBlock > this.cfg.toBlock) {
toBlock = this.cfg.toBlock;
}
return { fromBlock: BigInt(fromBlock), toBlock: BigInt(toBlock) };
}
protected async persist(): Promise<void> {
this.blockHeightCursor = this.lastRange?.toBlock ?? this.blockHeightCursor;
if (this.blockHeightCursor) {
await this.metadataRepo.save(this.cfg.id, { lastBlock: this.blockHeightCursor });
}
}
protected report(): void {
const labels = {
job: this.cfg.id,
chain: this.cfg.chain ?? "",
commitment: this.cfg.getCommitment(),
};
this.statsRepo.count("job_execution", labels);
this.statsRepo.measure("polling_cursor", this.latestBlockHeight ?? 0n, {
...labels,
type: "max",
});
this.statsRepo.measure("polling_cursor", this.blockHeightCursor ?? 0n, {
...labels,
type: "current",
});
}
}
export type PollWormchainLogsMetadata = {
lastBlock: bigint;
};
export interface PollWormchainLogsConfigProps {
blockBatchSize?: number;
commitment?: string;
fromBlock?: bigint;
addresses: string[];
interval?: number;
toBlock?: bigint;
chainId: number;
chain: string;
id?: string;
}
export class PollWormchainLogsConfig {
private props: PollWormchainLogsConfigProps;
constructor(props: PollWormchainLogsConfigProps) {
if (props.fromBlock && props.toBlock && props.fromBlock > props.toBlock) {
throw new Error("fromBlock must be less than or equal to toBlock");
}
this.props = props;
}
public getBlockBatchSize() {
return this.props.blockBatchSize ?? 100;
}
public getCommitment() {
return this.props.commitment ?? "latest";
}
public hasFinished(currentFromBlock?: bigint): boolean {
return (
currentFromBlock != undefined &&
this.props.toBlock != undefined &&
currentFromBlock >= this.props.toBlock
);
}
public get fromBlock() {
return this.props.fromBlock ? BigInt(this.props.fromBlock) : undefined;
}
public setFromBlock(fromBlock: bigint | undefined) {
this.props.fromBlock = fromBlock;
}
public get toBlock() {
return this.props.toBlock;
}
public get interval() {
return this.props.interval;
}
public get addresses() {
return this.props.addresses.map((address) => address.toLowerCase());
}
public get id() {
return this.props.id ?? ID;
}
public get chain() {
return this.props.chain;
}
public get chainId() {
return this.props.chainId;
}
}

View File

@ -0,0 +1,14 @@
export type WormchainBlockLogs = {
blockHeight: bigint;
timestamp: number;
chainId: number;
transactions?: {
hash: string;
type: string;
attributes: {
index: boolean;
value: string;
key: string;
}[];
}[];
};

View File

@ -1,25 +1,26 @@
import {
Checkpoint,
SuiEventFilter,
TransactionFilter as SuiTransactionFilter,
} from "@mysten/sui.js/client";
import { AptosEvent, AptosTransaction } from "./entities/aptos";
import { SuiTransactionBlockReceipt } from "./entities/sui";
import { Fallible, SolanaFailure } from "./errors";
import { ConfirmedSignatureInfo } from "./entities/solana";
import { WormchainBlockLogs } from "./entities/wormchain";
import { TransactionFilter } from "./actions/aptos/PollAptos";
import { RunPollingJob } from "./actions/RunPollingJob";
import {
EvmBlock,
EvmLog,
EvmLogFilter,
EvmTag,
Handler,
JobDefinition,
Range,
TransactionFilter as SuiTransactionFilter,
SuiEventFilter,
Checkpoint,
} from "@mysten/sui.js/client";
import {
ReceiptTransaction,
JobDefinition,
EvmLogFilter,
EvmBlock,
Handler,
solana,
EvmLog,
EvmTag,
Range,
} from "./entities";
import { ConfirmedSignatureInfo } from "./entities/solana";
import { Fallible, SolanaFailure } from "./errors";
import { SuiTransactionBlockReceipt } from "./entities/sui";
import { TransactionFilter } from "./actions/aptos/PollAptos";
import { AptosEvent, AptosTransaction } from "./entities/aptos";
export interface EvmBlockRepository {
getBlockHeight(chain: string, finality: string): Promise<bigint>;
@ -80,6 +81,11 @@ export interface AptosRepository {
getTransactionsByVersion(records: AptosEvent[] | AptosTransaction[]): Promise<AptosTransaction[]>;
}
export interface WormchainRepository {
getBlockHeight(): Promise<bigint | undefined>;
getBlockLogs(chainId: number, blockNumber: bigint): Promise<WormchainBlockLogs>;
}
export interface MetadataRepository<Metadata> {
get(id: string): Promise<Metadata | undefined>;
save(id: string, metadata: Metadata): Promise<void>;

View File

@ -0,0 +1,109 @@
import { LogFoundEvent, LogMessagePublished } from "../../../domain/entities";
import { WormchainBlockLogs } from "../../../domain/entities/wormchain";
import winston from "winston";
let logger: winston.Logger = winston.child({ module: "wormchainLogMessagePublishedMapper" });
export const wormchainLogMessagePublishedMapper = (
addresses: string[],
log: WormchainBlockLogs
): LogFoundEvent<LogMessagePublished>[] | [] => {
const transactionAttributesMapped = transactionAttributes(addresses, log);
if (transactionAttributesMapped.length === 0) {
return [];
}
const logMessages: LogFoundEvent<LogMessagePublished>[] = [];
transactionAttributesMapped.forEach((tx) => {
logger.info(
`[wormchain] Source event info: [tx: ${tx.hash}][emitterChain: ${tx.chainId}][sender: ${tx.emitter}][sequence: ${tx.sequence}]`
);
logMessages.push({
name: "log-message-published",
address: tx.coreContract!,
chainId: tx.chainId,
txHash: tx.hash!,
blockHeight: log.blockHeight,
blockTime: log.timestamp,
attributes: {
sender: tx.emitter!,
sequence: tx.sequence!,
payload: tx.payload!,
nonce: tx.nonce!,
consistencyLevel: 0,
},
});
});
return logMessages;
};
function transactionAttributes(
addresses: string[],
log: WormchainBlockLogs
): TransactionAttributes[] {
const transactionAttributes: TransactionAttributes[] = [];
log.transactions?.forEach((tx) => {
let coreContract: string | undefined;
let sequence: number | undefined;
let payload: string | undefined;
let emitter: string | undefined;
let nonce: number | undefined;
let hash: string | undefined;
for (const attr of tx.attributes) {
const key = Buffer.from(attr.key, "base64").toString().toLowerCase();
const value = Buffer.from(attr.value, "base64").toString().toLowerCase();
switch (key) {
case "message.sequence":
sequence = Number(value);
break;
case "message.message":
payload = value;
break;
case "message.sender":
emitter = value;
break;
case "message.nonce":
nonce = Number(value);
break;
case "_contract_address":
case "contract_address":
if (addresses.includes(value.toLowerCase())) {
coreContract = value.toLowerCase();
}
break;
}
}
if (coreContract && sequence && payload && emitter && nonce) {
hash = tx.hash;
transactionAttributes.push({
coreContract,
sequence,
payload,
emitter,
nonce,
hash,
chainId: log.chainId,
});
}
});
return transactionAttributes;
}
type TransactionAttributes = {
coreContract: string | undefined;
sequence: number | undefined;
payload: string | undefined;
emitter: string | undefined;
chainId: number;
nonce: number | undefined;
hash: string | undefined;
};

View File

@ -1,10 +1,18 @@
import { AptosRepository, JobRepository, SuiRepository } from "../../domain/repositories";
import { RateLimitedWormchainJsonRPCBlockRepository } from "./wormchain/RateLimitedWormchainJsonRPCBlockRepository";
import { RateLimitedAptosJsonRPCBlockRepository } from "./aptos/RateLimitedAptosJsonRPCBlockRepository";
import { RateLimitedEvmJsonRPCBlockRepository } from "./evm/RateLimitedEvmJsonRPCBlockRepository";
import { RateLimitedSuiJsonRPCBlockRepository } from "./sui/RateLimitedSuiJsonRPCBlockRepository";
import { WormchainJsonRPCBlockRepository } from "./wormchain/WormchainJsonRPCBlockRepository";
import { AptosJsonRPCBlockRepository } from "./aptos/AptosJsonRPCBlockRepository";
import { SNSClient, SNSClientConfig } from "@aws-sdk/client-sns";
import { InstrumentedHttpProvider } from "../rpc/http/InstrumentedHttpProvider";
import { Config } from "../config";
import {
WormchainRepository,
AptosRepository,
JobRepository,
SuiRepository,
} from "../../domain/repositories";
import {
InstrumentedConnection,
InstrumentedSuiClient,
@ -27,8 +35,8 @@ import {
SnsEventRepository,
ProviderPoolMap,
} from ".";
import { AptosJsonRPCBlockRepository } from "./aptos/AptosJsonRPCBlockRepository";
const WORMCHAIN_CHAIN = "wormchain";
const SOLANA_CHAIN = "solana";
const APTOS_CHAIN = "aptos";
const EVM_CHAIN = "evm";
@ -141,7 +149,7 @@ export class RepositoriesBuilder {
}
if (chain === APTOS_CHAIN) {
const pools = this.createAptosProviderPools();
const pools = this.createDefaultProviderPools(APTOS_CHAIN);
const aptosRepository = new RateLimitedAptosJsonRPCBlockRepository(
new AptosJsonRPCBlockRepository(pools)
@ -149,6 +157,16 @@ export class RepositoriesBuilder {
this.repositories.set("aptos-repo", aptosRepository);
}
if (chain === WORMCHAIN_CHAIN) {
const pools = this.createDefaultProviderPools(WORMCHAIN_CHAIN);
const wormchainRepository = new RateLimitedWormchainJsonRPCBlockRepository(
new WormchainJsonRPCBlockRepository(pools)
);
this.repositories.set("wormchain-repo", wormchainRepository);
}
});
this.repositories.set(
@ -165,6 +183,7 @@ export class RepositoriesBuilder {
solanaSlotRepo: this.getSolanaSlotRepository(),
suiRepo: this.getSuiRepository(),
aptosRepo: this.getAptosRepository(),
wormchainRepo: this.getWormchainRepository(),
}
)
);
@ -204,6 +223,10 @@ export class RepositoriesBuilder {
return this.getRepo("aptos-repo");
}
public getWormchainRepository(): WormchainRepository {
return this.getRepo("wormchain-repo");
}
private getRepo(name: string): any {
const repo = this.repositories.get(name);
if (!repo) throw new Error(`No repository ${name}`);
@ -241,11 +264,11 @@ export class RepositoriesBuilder {
return pools;
}
private createAptosProviderPools() {
const cfg = this.cfg.chains[APTOS_CHAIN];
private createDefaultProviderPools(chain: string) {
const cfg = this.cfg.chains[chain];
const pools = providerPoolSupplier(
cfg.rpcs.map((url) => ({ url })),
(rpcCfg: RpcConfig) => this.createHttpClient(APTOS_CHAIN, rpcCfg.url),
(rpcCfg: RpcConfig) => this.createHttpClient(chain, rpcCfg.url),
POOL_STRATEGY
);
return pools;

View File

@ -1,47 +1,55 @@
import { FileMetadataRepository, SnsEventRepository } from "./index";
import { JobDefinition, Handler, LogFoundEvent } from "../../domain/entities";
import { aptosRedeemedTransactionFoundMapper } from "../mappers/aptos/aptosRedeemedTransactionFoundMapper";
import { wormchainLogMessagePublishedMapper } from "../mappers/wormchain/wormchainLogMessagePublishedMapper";
import { suiRedeemedTransactionFoundMapper } from "../mappers/sui/suiRedeemedTransactionFoundMapper";
import { aptosLogMessagePublishedMapper } from "../mappers/aptos/aptosLogMessagePublishedMapper";
import { suiLogMessagePublishedMapper } from "../mappers/sui/suiLogMessagePublishedMapper";
import { HandleSolanaTransactions } from "../../domain/actions/solana/HandleSolanaTransactions";
import { HandleAptosTransactions } from "../../domain/actions/aptos/HandleAptosTransactions";
import { HandleEvmTransactions } from "../../domain/actions/evm/HandleEvmTransactions";
import { HandleSuiTransactions } from "../../domain/actions/sui/HandleSuiTransactions";
import { HandleWormchainLogs } from "../../domain/actions/wormchain/HandleWormchainLogs";
import log from "../log";
import {
PollWormchainLogsConfigProps,
PollWormchainLogsConfig,
PollWormchain,
} from "../../domain/actions/wormchain/PollWormchain";
import {
SolanaSlotRepository,
EvmBlockRepository,
MetadataRepository,
AptosRepository,
StatRepository,
JobRepository,
SuiRepository,
WormchainRepository,
} from "../../domain/repositories";
import {
PollSolanaTransactionsConfig,
PollSolanaTransactions,
PollEvmLogsConfigProps,
PollEvmLogsConfig,
RunPollingJob,
HandleEvmLogs,
PollEvm,
PollEvmLogsConfig,
PollEvmLogsConfigProps,
PollSolanaTransactions,
PollSolanaTransactionsConfig,
RunPollingJob,
} from "../../domain/actions";
import { JobDefinition, Handler, LogFoundEvent } from "../../domain/entities";
import {
AptosRepository,
EvmBlockRepository,
JobRepository,
MetadataRepository,
SolanaSlotRepository,
StatRepository,
SuiRepository,
} from "../../domain/repositories";
import { FileMetadataRepository, SnsEventRepository } from "./index";
import { HandleSolanaTransactions } from "../../domain/actions/solana/HandleSolanaTransactions";
import {
evmRedeemedTransactionFoundMapper,
solanaLogMessagePublishedMapper,
solanaTransferRedeemedMapper,
evmLogMessagePublishedMapper,
evmRedeemedTransactionFoundMapper,
} from "../mappers";
import log from "../log";
import { HandleEvmTransactions } from "../../domain/actions/evm/HandleEvmTransactions";
import { suiRedeemedTransactionFoundMapper } from "../mappers/sui/suiRedeemedTransactionFoundMapper";
import { HandleSuiTransactions } from "../../domain/actions/sui/HandleSuiTransactions";
import { suiLogMessagePublishedMapper } from "../mappers/sui/suiLogMessagePublishedMapper";
import {
PollSuiTransactions,
PollSuiTransactionsConfig,
PollSuiTransactions,
} from "../../domain/actions/sui/PollSuiTransactions";
import {
PollAptos,
PollAptosTransactionsConfig,
PollAptosTransactionsConfigProps,
PollAptosTransactionsConfig,
PollAptos,
} from "../../domain/actions/aptos/PollAptos";
import { HandleAptosTransactions } from "../../domain/actions/aptos/HandleAptosTransactions";
import { aptosLogMessagePublishedMapper } from "../mappers/aptos/aptosLogMessagePublishedMapper";
import { aptosRedeemedTransactionFoundMapper } from "../mappers/aptos/aptosRedeemedTransactionFoundMapper";
export class StaticJobRepository implements JobRepository {
private fileRepo: FileMetadataRepository;
@ -59,6 +67,7 @@ export class StaticJobRepository implements JobRepository {
private solanaSlotRepo: SolanaSlotRepository;
private suiRepo: SuiRepository;
private aptosRepo: AptosRepository;
private wormchainRepo: WormchainRepository;
constructor(
environment: string,
@ -72,6 +81,7 @@ export class StaticJobRepository implements JobRepository {
solanaSlotRepo: SolanaSlotRepository;
suiRepo: SuiRepository;
aptosRepo: AptosRepository;
wormchainRepo: WormchainRepository;
}
) {
this.fileRepo = new FileMetadataRepository(path);
@ -82,6 +92,7 @@ export class StaticJobRepository implements JobRepository {
this.solanaSlotRepo = repos.solanaSlotRepo;
this.suiRepo = repos.suiRepo;
this.aptosRepo = repos.aptosRepo;
this.wormchainRepo = repos.wormchainRepo;
this.environment = environment;
this.dryRun = dryRun;
this.fill();
@ -92,7 +103,6 @@ export class StaticJobRepository implements JobRepository {
if (!persisted) {
return Promise.resolve([]);
}
return persisted;
}
@ -101,7 +111,6 @@ export class StaticJobRepository implements JobRepository {
if (!src) {
throw new Error(`Source ${jobDef.source.action} not found`);
}
return src(jobDef);
}
@ -126,12 +135,21 @@ export class StaticJobRepository implements JobRepository {
};
result.push((await maybeHandler(config, handler.target, mapper)).bind(maybeHandler));
}
return result;
}
/**
* Fill all resources that applications needs to work
* Resources are: actions, mappers, targets and handlers
*/
private fill() {
// Actions
this.loadActions();
this.loadMappers();
this.loadTargets();
this.loadHandlers();
}
private loadActions(): void {
const pollEvm = (jobDef: JobDefinition) =>
new PollEvm(
this.blockRepoProvider(jobDef.source.config.chain),
@ -156,7 +174,6 @@ export class StaticJobRepository implements JobRepository {
this.metadataRepo,
this.suiRepo
);
const pollAptos = (jobDef: JobDefinition) =>
new PollAptos(
new PollAptosTransactionsConfig({
@ -169,12 +186,26 @@ export class StaticJobRepository implements JobRepository {
this.aptosRepo,
jobDef.source.records
);
const pollWormchain = (jobDef: JobDefinition) =>
new PollWormchain(
this.wormchainRepo,
this.metadataRepo,
this.statsRepo,
new PollWormchainLogsConfig({
...(jobDef.source.config as PollWormchainLogsConfigProps),
id: jobDef.id,
}),
jobDef.source.records
);
this.sources.set("PollEvm", pollEvm);
this.sources.set("PollSolanaTransactions", pollSolanaTransactions);
this.sources.set("PollSuiTransactions", pollSuiTransactions);
this.sources.set("PollAptos", pollAptos);
this.sources.set("PollWormchain", pollWormchain);
}
// Mappers
private loadMappers(): void {
this.mappers.set("evmLogMessagePublishedMapper", evmLogMessagePublishedMapper);
this.mappers.set("evmRedeemedTransactionFoundMapper", evmRedeemedTransactionFoundMapper);
this.mappers.set("solanaLogMessagePublishedMapper", solanaLogMessagePublishedMapper);
@ -183,16 +214,20 @@ export class StaticJobRepository implements JobRepository {
this.mappers.set("suiRedeemedTransactionFoundMapper", suiRedeemedTransactionFoundMapper);
this.mappers.set("aptosLogMessagePublishedMapper", aptosLogMessagePublishedMapper);
this.mappers.set("aptosRedeemedTransactionFoundMapper", aptosRedeemedTransactionFoundMapper);
this.mappers.set("wormchainLogMessagePublishedMapper", wormchainLogMessagePublishedMapper);
}
// Targets
private loadTargets(): void {
const snsTarget = () => this.snsRepo.asTarget();
const dummyTarget = async () => async (events: any[]) => {
log.info(`[target dummy] Got ${events.length} events`);
};
this.targets.set("sns", snsTarget);
this.targets.set("dummy", dummyTarget);
}
// Handles
private loadHandlers(): void {
const handleEvmLogs = async (config: any, target: string, mapper: any) => {
const instance = new HandleEvmLogs<LogFoundEvent<any>>(
config,
@ -200,7 +235,6 @@ export class StaticJobRepository implements JobRepository {
await this.targets.get(this.dryRun ? "dummy" : target)!(),
this.statsRepo
);
return instance.handle.bind(instance);
};
const handleEvmTransactions = async (config: any, target: string, mapper: any) => {
@ -210,7 +244,6 @@ export class StaticJobRepository implements JobRepository {
await this.targets.get(this.dryRun ? "dummy" : target)!(),
this.statsRepo
);
return instance.handle.bind(instance);
};
const handleSolanaTx = async (config: any, target: string, mapper: any) => {
@ -220,7 +253,6 @@ export class StaticJobRepository implements JobRepository {
await this.getTarget(target),
this.statsRepo
);
return instance.handle.bind(instance);
};
const handleSuiTx = async (config: any, target: string, mapper: any) => {
@ -230,7 +262,6 @@ export class StaticJobRepository implements JobRepository {
await this.getTarget(target),
this.statsRepo
);
return instance.handle.bind(instance);
};
const handleAptosTx = async (config: any, target: string, mapper: any) => {
@ -240,7 +271,16 @@ export class StaticJobRepository implements JobRepository {
await this.getTarget(target),
this.statsRepo
);
return instance.handle.bind(instance);
};
const handleWormchainLogs = async (config: any, target: string, mapper: any) => {
const instance = new HandleWormchainLogs(
config,
mapper,
await this.getTarget(target),
this.statsRepo
);
return instance.handle.bind(instance);
};
@ -249,6 +289,7 @@ export class StaticJobRepository implements JobRepository {
this.handlers.set("HandleSolanaTransactions", handleSolanaTx);
this.handlers.set("HandleSuiTransactions", handleSuiTx);
this.handlers.set("HandleAptosTransactions", handleAptosTx);
this.handlers.set("HandleWormchainLogs", handleWormchainLogs);
}
private async getTarget(target: string): Promise<(items: any[]) => Promise<void>> {
@ -256,7 +297,6 @@ export class StaticJobRepository implements JobRepository {
if (!maybeTarget) {
throw new Error(`Target ${target} not found`);
}
return maybeTarget();
}
}

View File

@ -1,3 +1,6 @@
import { SHA256 } from "jscrypto/SHA256";
import { Base64 } from "jscrypto/Base64";
export function divideIntoBatches<T>(set: Set<T>, batchSize = 10): Set<T>[] {
const batches: Set<T>[] = [];
let batch: any[] = [];
@ -15,3 +18,7 @@ export function divideIntoBatches<T>(set: Set<T>, batchSize = 10): Set<T>[] {
}
return batches;
}
export function hexToHash(data: string): string {
return SHA256.hash(Base64.parse(data)).toString().toUpperCase();
}

View File

@ -0,0 +1,23 @@
import { RateLimitedRPCRepository } from "../RateLimitedRPCRepository";
import { WormchainRepository } from "../../../domain/repositories";
import { WormchainBlockLogs } from "../../../domain/entities/wormchain";
import { Options } from "../common/rateLimitedOptions";
import winston from "winston";
export class RateLimitedWormchainJsonRPCBlockRepository
extends RateLimitedRPCRepository<WormchainRepository>
implements WormchainRepository
{
constructor(delegate: WormchainRepository, opts: Options = { period: 10_000, limit: 1000 }) {
super(delegate, opts);
this.logger = winston.child({ module: "RateLimitedWormchainJsonRPCBlockRepository" });
}
getBlockHeight(): Promise<bigint | undefined> {
return this.breaker.fn(() => this.delegate.getBlockHeight()).execute();
}
getBlockLogs(chainId: number, blockNumber: bigint): Promise<WormchainBlockLogs> {
return this.breaker.fn(() => this.delegate.getBlockLogs(chainId, blockNumber)).execute();
}
}

View File

@ -0,0 +1,247 @@
import { divideIntoBatches, hexToHash } from "../common/utils";
import { InstrumentedHttpProvider } from "../../rpc/http/InstrumentedHttpProvider";
import { WormchainRepository } from "../../../domain/repositories";
import { WormchainBlockLogs } from "../../../domain/entities/wormchain";
import { ProviderPool } from "@xlabs/rpc-pool";
import winston from "winston";
let BLOCK_HEIGHT_ENDPOINT = "/abci_info";
let TRANSACTION_ENDPOINT = "/tx";
let BLOCK_ENDPOINT = "/block";
type ProviderPoolMap = ProviderPool<InstrumentedHttpProvider>;
export class WormchainJsonRPCBlockRepository implements WormchainRepository {
private readonly logger: winston.Logger;
protected pool: ProviderPoolMap;
constructor(pool: ProviderPool<InstrumentedHttpProvider>) {
this.logger = winston.child({ module: "WormchainJsonRPCBlockRepository" });
this.pool = pool;
}
async getBlockHeight(): Promise<bigint | undefined> {
try {
let results: ResultBlockHeight;
results = await this.pool.get().get<typeof results>(BLOCK_HEIGHT_ENDPOINT);
if (
results &&
results.result &&
results.result.response &&
results.result.response.last_block_height
) {
const blockHeight = results.result.response.last_block_height;
return BigInt(blockHeight);
}
return undefined;
} catch (e) {
this.handleError(`Error: ${e}`, "getBlockHeight");
throw e;
}
}
async getBlockLogs(chainId: number, blockNumber: bigint): Promise<WormchainBlockLogs> {
try {
const blockEndpoint = `${BLOCK_ENDPOINT}?height=${blockNumber}`;
let resultsBlock: ResultBlock;
resultsBlock = await this.pool.get().get<typeof resultsBlock>(blockEndpoint);
const txs = resultsBlock.result.block.data.txs;
if (!txs) {
return {
transactions: [],
blockHeight: BigInt(resultsBlock.result.block.header.height),
timestamp: Number(resultsBlock.result.block.header.time),
chainId,
};
}
const cosmosTransaction: CosmosTransaction[] = [];
const hashNumbers = new Set(txs.map((tx) => tx));
const batches = divideIntoBatches(hashNumbers, 10);
for (const batch of batches) {
for (let hashBatch of batch) {
const hash: string = hexToHash(hashBatch);
const txEndpoint = `${TRANSACTION_ENDPOINT}?hash=0x${hash}`;
const resultTransaction: ResultTransaction = await this.pool
.get()
.get<typeof resultTransaction>(txEndpoint);
if (
resultTransaction &&
resultTransaction.result.tx_result &&
resultTransaction.result.tx_result.events
) {
resultTransaction.result.tx_result.events.forEach((event) => {
if (event.type === "wasm") {
cosmosTransaction.push({
hash: `0x${hash}`.toLocaleLowerCase(),
type: event.type,
attributes: event.attributes,
});
}
});
}
}
}
const dateTime: Date = new Date(resultsBlock.result.block.header.time);
const timestamp: number = dateTime.getTime();
return {
transactions: cosmosTransaction || [],
blockHeight: BigInt(resultsBlock.result.block.header.height),
timestamp: timestamp,
chainId,
};
} catch (e) {
this.handleError(`Error: ${e}`, "getBlockHeight");
throw e;
}
}
private handleError(e: any, method: string) {
this.logger.error(`[wormchain] Error calling ${method}: ${e.message ?? e}`);
}
}
type ResultBlockHeight = { result: { response: { last_block_height: string } } };
type ResultBlock = {
result: {
block_id: {
hash: string;
parts: {
total: number;
hash: string;
};
};
block: {
header: {
version: { block: string };
chain_id: string;
height: string;
time: string; // eg. '2023-01-03T12:13:00.849094631Z'
last_block_id: { hash: string; parts: { total: number; hash: string } };
last_commit_hash: string;
data_hash: string;
validators_hash: string;
next_validators_hash: string;
consensus_hash: string;
app_hash: string;
last_results_hash: string;
evidence_hash: string;
proposer_address: string;
};
data: { txs: string[] | null };
evidence: { evidence: null };
last_commit: {
height: string;
round: number;
block_id: { hash: string; parts: { total: number; hash: string } };
signatures: string[];
};
};
};
};
type ResultTransaction = {
result: {
tx: {
body: {
messages: string[];
memo: string;
timeout_height: string;
extension_options: [];
non_critical_extension_options: [];
};
auth_info: {
signer_infos: string[];
fee: {
amount: [{ denom: string; amount: string }];
gas_limit: string;
payer: string;
granter: string;
};
};
signatures: string[];
};
tx_result: {
height: string;
txhash: string;
codespace: string;
code: 0;
data: string;
raw_log: string;
logs: [{ msg_index: number; log: string; events: EventsType }];
info: string;
gas_wanted: string;
gas_used: string;
tx: {
"@type": "/cosmos.tx.v1beta1.Tx";
body: {
messages: [
{
"@type": "/cosmos.staking.v1beta1.MsgBeginRedelegate";
delegator_address: string;
validator_src_address: string;
validator_dst_address: string;
amount: { denom: string; amount: string };
}
];
memo: "";
timeout_height: "0";
extension_options: [];
non_critical_extension_options: [];
};
auth_info: {
signer_infos: [
{
public_key: {
"@type": "/cosmos.crypto.secp256k1.PubKey";
key: string;
};
mode_info: { single: { mode: string } };
sequence: string;
}
];
fee: {
amount: [{ denom: string; amount: string }];
gas_limit: string;
payer: string;
granter: string;
};
};
signatures: string[];
};
timestamp: string; // eg. '2023-01-03T12:12:54Z'
events: EventsType[];
};
};
};
type EventsType = {
type: string;
attributes: [
{
key: string;
value: string;
index: boolean;
}
];
};
type CosmosTransaction = {
hash: string;
type: string;
attributes: {
key: string;
value: string;
index: boolean;
}[];
};

View File

@ -127,7 +127,7 @@ describe("GetAptosTransactions", () => {
givenPollAptosTx(cfg);
// When
await whenPollEvmLogsStarts();
await whenPollAptosLogsStarts();
// Then
await thenWaitForAssertion(
@ -279,7 +279,7 @@ describe("GetAptosTransactions", () => {
givenPollAptosTx(cfg);
// Whem
await whenPollEvmLogsStarts();
await whenPollAptosLogsStarts();
// Then
await thenWaitForAssertion(
@ -299,7 +299,7 @@ describe("GetAptosTransactions", () => {
givenPollAptosTx(cfg);
// Whem
await whenPollEvmLogsStarts();
await whenPollAptosLogsStarts();
// Then
await thenWaitForAssertion(
@ -318,7 +318,7 @@ describe("GetAptosTransactions", () => {
givenPollAptosTx(cfg);
// Whem
await whenPollEvmLogsStarts();
await whenPollAptosLogsStarts();
// Then
await thenWaitForAssertion(
@ -383,6 +383,6 @@ const givenPollAptosTx = (cfg: PollAptosTransactionsConfig) => {
pollAptos = new PollAptos(cfg, statsRepo, metadataRepo, aptosRepo, "GetAptosTransactions");
};
const whenPollEvmLogsStarts = async () => {
const whenPollAptosLogsStarts = async () => {
pollAptos.run([handlers.working]);
};

View File

@ -66,7 +66,7 @@ describe("GetAptosTransactionsByEvents", () => {
givenPollAptosTx(cfg);
// When
await whenPollEvmLogsStarts();
await whenPollAptosLogsStarts();
// Then
await thenWaitForAssertion(
@ -96,7 +96,7 @@ describe("GetAptosTransactionsByEvents", () => {
givenPollAptosTx(cfg);
// When
await whenPollEvmLogsStarts();
await whenPollAptosLogsStarts();
// Then
await thenWaitForAssertion(
@ -126,7 +126,7 @@ describe("GetAptosTransactionsByEvents", () => {
givenPollAptosTx(cfg);
// When
await whenPollEvmLogsStarts();
await whenPollAptosLogsStarts();
// Then
await thenWaitForAssertion(
@ -156,7 +156,7 @@ describe("GetAptosTransactionsByEvents", () => {
givenPollAptosTx(cfg);
// When
await whenPollEvmLogsStarts();
await whenPollAptosLogsStarts();
// Then
await thenWaitForAssertion(
@ -313,6 +313,6 @@ const givenPollAptosTx = (cfg: PollAptosTransactionsConfig) => {
);
};
const whenPollEvmLogsStarts = async () => {
const whenPollAptosLogsStarts = async () => {
pollAptos.run([handlers.working]);
};

View File

@ -0,0 +1,165 @@
import { afterEach, describe, it, expect, jest } from "@jest/globals";
import { thenWaitForAssertion } from "../../../wait-assertion";
import { AptosTransaction } from "../../../../src/domain/entities/aptos";
import {
PollWormchainLogsMetadata,
PollWormchainLogsConfig,
PollWormchain,
} from "../../../../src/domain/actions/wormchain/PollWormchain";
import {
WormchainRepository,
MetadataRepository,
StatRepository,
} from "../../../../src/domain/repositories";
let getBlockHeightSpy: jest.SpiedFunction<WormchainRepository["getBlockHeight"]>;
let getBlockLogsSpy: jest.SpiedFunction<WormchainRepository["getBlockLogs"]>;
let metadataSaveSpy: jest.SpiedFunction<MetadataRepository<PollWormchainLogsMetadata>["save"]>;
let handlerSpy: jest.SpiedFunction<(txs: AptosTransaction[]) => Promise<void>>;
let metadataRepo: MetadataRepository<PollWormchainLogsMetadata>;
let wormchainRepo: WormchainRepository;
let statsRepo: StatRepository;
let handlers = {
working: (txs: AptosTransaction[]) => Promise.resolve(),
failing: (txs: AptosTransaction[]) => Promise.reject(),
};
let pollWormchain: PollWormchain;
let props = {
blockBatchSize: 100,
from: 0n,
limit: 0n,
environment: "testnet",
commitment: "immediate",
addresses: ["wormhole1ufs3tlq4umljk0qfe8k5ya0x6hpavn897u2cnf9k0en9jr7qarqqaqfk2j"],
interval: 5000,
topics: [],
chainId: 3104,
filter: {
address: "wormhole1ufs3tlq4umljk0qfe8k5ya0x6hpavn897u2cnf9k0en9jr7qarqqaqfk2j",
},
chain: "wormchain",
id: "poll-log-message-published-wormchain",
};
let cfg = new PollWormchainLogsConfig(props);
describe("GetWormchainLogs", () => {
afterEach(async () => {
await pollWormchain.stop();
});
it("should be skip the transations blocks, because the transactions will be undefined", async () => {
// Given
givenAptosBlockRepository(7606614n);
givenMetadataRepository({ lastBlock: 7606613n });
givenStatsRepository();
givenPollWormchainTx(cfg);
// When
await whenPollWormchainLogsStarts();
// Then
await thenWaitForAssertion(() => expect(getBlockLogsSpy).toBeCalledWith(3104, 7606614n));
});
it("should be process the log because it contains wasm transactions", async () => {
// Given
const log = {
transactions: [
{
hash: "0xd84a9c85170c28b12a1436082e99c1ea2598cbf36f9e263bfc0b7fb79a972dfe",
type: "wasm",
attributes: [
{
key: "X2NvbnRyYWN0X2FkZHJlc3M=",
value:
"d29ybWhvbGUxNGhqMnRhdnE4ZnBlc2R3eHhjdTQ0cnR5M2hoOTB2aHVqcnZjbXN0bDR6cjN0eG1mdnc5c3JyZzQ2NQ==",
index: true,
},
{ key: "YWN0aW9u", value: "c3VibWl0X29ic2VydmF0aW9ucw==", index: true },
{
key: "b3duZXI=",
value: "d29ybWhvbGUxcWdqZmRmNWczMnN4d2pqanduNHZwOGZnZzJmdzBtOHh4aDdheGM=",
index: true,
},
],
},
{
hash: "0x9042d7f656f2292e8a4bfa9468ee8215fd6de9ff23b447e20f96f6a70559df68",
type: "wasm",
attributes: [
{
key: "X2NvbnRyYWN0X2FkZHJlc3M=",
value:
"d29ybWhvbGUxNGhqMnRhdnE4ZnBlc2R3eHhjdTQ0cnR5M2hoOTB2aHVqcnZjbXN0bDR6cjN0eG1mdnc5c3JyZzQ2NQ==",
index: true,
},
{ key: "YWN0aW9u", value: "c3VibWl0X29ic2VydmF0aW9ucw==", index: true },
{
key: "b3duZXI=",
value: "d29ybWhvbGUxNW5rbTdhdnB4eHNuY3I0Z2c4dTJxbDdnY2tsbTJrcmt6d2U3N20=",
index: true,
},
],
},
],
blockHeight: "7606615",
timestamp: 1711025902418,
};
givenAptosBlockRepository(7606615n, log);
givenMetadataRepository({ lastBlock: 7606614n });
givenStatsRepository();
givenPollWormchainTx(cfg);
// When
await whenPollWormchainLogsStarts();
// Then
await thenWaitForAssertion(() => expect(getBlockLogsSpy).toBeCalledWith(3104, 7606615n));
});
});
const givenAptosBlockRepository = (blockHeigh: bigint, log: any = {}) => {
wormchainRepo = {
getBlockHeight: () => Promise.resolve(blockHeigh),
getBlockLogs: () => Promise.resolve(log),
};
getBlockHeightSpy = jest.spyOn(wormchainRepo, "getBlockHeight");
getBlockLogsSpy = jest.spyOn(wormchainRepo, "getBlockLogs");
};
const givenMetadataRepository = (data?: PollWormchainLogsMetadata) => {
metadataRepo = {
get: () => Promise.resolve(data),
save: () => Promise.resolve(),
};
metadataSaveSpy = jest.spyOn(metadataRepo, "save");
};
const givenStatsRepository = () => {
statsRepo = {
count: () => {},
measure: () => {},
report: () => Promise.resolve(""),
};
};
const givenPollWormchainTx = (cfg: PollWormchainLogsConfig) => {
pollWormchain = new PollWormchain(
wormchainRepo,
metadataRepo,
statsRepo,
cfg,
"GetWormchainLogs"
);
};
const whenPollWormchainLogsStarts = async () => {
pollWormchain.run([handlers.working]);
};

View File

@ -0,0 +1,119 @@
import { afterEach, describe, it, expect, jest } from "@jest/globals";
import { WormchainBlockLogs } from "../../../../src/domain/entities/wormchain";
import { StatRepository } from "../../../../src/domain/repositories";
import { LogFoundEvent } from "../../../../src/domain/entities";
import {
HandleWormchainLogsOptions,
HandleWormchainLogs,
} from "../../../../src/domain/actions/wormchain/HandleWormchainLogs";
let targetRepoSpy: jest.SpiedFunction<(typeof targetRepo)["save"]>;
let statsRepo: StatRepository;
let handleWormchainLogs: HandleWormchainLogs;
let logs: WormchainBlockLogs[];
let cfg: HandleWormchainLogsOptions;
describe("HandleWormchainLogs", () => {
afterEach(async () => {});
it("should be able to map source events log", async () => {
// Given
givenConfig();
givenStatsRepository();
givenHandleEvmLogs();
// When
const result = await handleWormchainLogs.handle(logs);
// Then
expect(result).toHaveLength(1);
expect(result[0].name).toBe("log-message-published");
expect(result[0].chainId).toBe(3104);
expect(result[0].txHash).toBe(
"0x7f61bf387fdb700d32d2b40ccecfb70ae46a2f82775242d04202bb7a538667c6"
);
expect(result[0].address).toBe(
"wormhole1ufs3tlq4umljk0qfe8k5ya0x6hpavn897u2cnf9k0en9jr7qarqqaqfk2j"
);
});
});
const mapper = (addresses: string[], tx: WormchainBlockLogs) => {
return [
{
name: "log-message-published",
address: "wormhole1ufs3tlq4umljk0qfe8k5ya0x6hpavn897u2cnf9k0en9jr7qarqqaqfk2j",
chainId: 3104,
txHash: "0x7f61bf387fdb700d32d2b40ccecfb70ae46a2f82775242d04202bb7a538667c6",
blockHeight: 153549311n,
blockTime: 1709645685704036,
attributes: {
sender: "wormhole1ufs3tlq4umljk0qfe8k5ya0x6hpavn897u2cnf9k0en9jr7qarqqaqfk2j",
sequence: 203,
payload: "",
nonce: 75952,
consistencyLevel: 0,
protocol: "Token Bridge",
},
},
];
};
const targetRepo = {
save: async (events: LogFoundEvent<Record<string, string>>[]) => {
Promise.resolve();
},
failingSave: async (events: LogFoundEvent<Record<string, string>>[]) => {
Promise.reject();
},
};
const givenHandleEvmLogs = (targetFn: "save" | "failingSave" = "save") => {
targetRepoSpy = jest.spyOn(targetRepo, targetFn);
handleWormchainLogs = new HandleWormchainLogs(cfg, mapper, () => Promise.resolve(), statsRepo);
};
const givenConfig = () => {
cfg = {
id: "poll-log-message-published-wormchain",
metricName: "process_source_event",
filter: { addresses: ["wormhole1ufs3tlq4umljk0qfe8k5ya0x6hpavn897u2cnf9k0en9jr7qarqqaqfk2j"] },
};
};
const givenStatsRepository = () => {
statsRepo = {
count: () => {},
measure: () => {},
report: () => Promise.resolve(""),
};
};
logs = [
{
transactions: [
{
hash: "0x7f61bf387fdb700d32d2b40ccecfb70ae46a2f82775242d04202bb7a538667c6",
type: "wasm",
attributes: [
{
key: "X2NvbnRyYWN0X2FkZHJlc3M=",
value:
"d29ybWhvbGUxNGhqMnRhdnE4ZnBlc2R3eHhjdTQ0cnR5M2hoOTB2aHVqcnZjbXN0bDR6cjN0eG1mdnc5c3JyZzQ2NQ==",
index: true,
},
{ key: "YWN0aW9u", value: "c3VibWl0X29ic2VydmF0aW9ucw==", index: true },
{
key: "b3duZXI=",
value: "d29ybWhvbGUxOHl3NmY4OHA3Znc2bTk5eDlrbnJmejNwMHk2OTNoaDBhaDh5Mm0=",
index: true,
},
],
},
],
blockHeight: BigInt(7606614),
timestamp: 1711025896481,
chainId: 3104,
},
];

View File

@ -0,0 +1,154 @@
import { afterEach, describe, it, expect, jest } from "@jest/globals";
import { thenWaitForAssertion } from "../../../wait-assertion";
import { WormchainBlockLogs } from "../../../../src/domain/entities/wormchain";
import {
PollWormchainLogsMetadata,
PollWormchainLogsConfig,
PollWormchain,
} from "../../../../src/domain/actions";
import {
WormchainRepository,
MetadataRepository,
StatRepository,
} from "../../../../src/domain/repositories";
let cfg = new PollWormchainLogsConfig({
chain: "wormchain",
fromBlock: 7626734n,
addresses: [],
chainId: 3104,
});
let getBlockHeightSpy: jest.SpiedFunction<WormchainRepository["getBlockHeight"]>;
let getBlockLogsSpy: jest.SpiedFunction<WormchainRepository["getBlockLogs"]>;
let handlerSpy: jest.SpiedFunction<(logs: WormchainBlockLogs[]) => Promise<void>>;
let metadataSaveSpy: jest.SpiedFunction<MetadataRepository<PollWormchainLogsMetadata>["save"]>;
let metadataRepo: MetadataRepository<PollWormchainLogsMetadata>;
let wormchainBlockRepo: WormchainRepository;
let statsRepo: StatRepository;
let handlers = {
working: (logs: WormchainBlockLogs[]) => Promise.resolve(),
failing: (logs: WormchainBlockLogs[]) => Promise.reject(),
};
let pollWormchain: PollWormchain;
describe("PollWormchain", () => {
afterEach(async () => {
await pollWormchain.stop();
});
it("should be able to read logs from latest block when no fromBlock is configured", async () => {
const currentHeight = 10n;
const logs = {
transactions: [
{
hash: "0x47a54890a16ea9d924c32a1fa6fd1cf39176be532c8ba454d33f628d89be3388",
type: "wasm",
attributes: [
{
key: "X2NvbnRyYWN0X2FkZHJlc3M=",
value:
"d29ybWhvbGUxNGhqMnRhdnE4ZnBlc2R3eHhjdTQ0cnR5M2hoOTB2aHVqcnZjbXN0bDR6cjN0eG1mdnc5c3JyZzQ2NQ==",
index: true,
},
{ key: "YWN0aW9u", value: "c3VibWl0X29ic2VydmF0aW9ucw==", index: true },
{
key: "b3duZXI=",
value: "d29ybWhvbGUxOHl3NmY4OHA3Znc2bTk5eDlrbnJmejNwMHk2OTNoaDBhaDh5Mm0=",
index: true,
},
],
},
{
hash: "0x56e974e33c5c7403d23a5fe7fa414d9f1d6dd4f1b67601342100093c604b5d70",
type: "wasm",
attributes: [
{
key: "X2NvbnRyYWN0X2FkZHJlc3M=",
value:
"d29ybWhvbGUxNGhqMnRhdnE4ZnBlc2R3eHhjdTQ0cnR5M2hoOTB2aHVqcnZjbXN0bDR6cjN0eG1mdnc5c3JyZzQ2NQ==",
index: true,
},
{ key: "YWN0aW9u", value: "c3VibWl0X29ic2VydmF0aW9ucw==", index: true },
{
key: "b3duZXI=",
value: "d29ybWhvbGUxYWNxYTV2bDJudW5oc250djBldGpzNnllZTN2NnZjOGw5bTRxOGU=",
index: true,
},
],
},
],
blockHeight: "7626735",
timestamp: 1711143216257,
};
givenEvmBlockRepository(currentHeight, logs);
givenMetadataRepository();
givenStatsRepository();
givenPollWormchainLogs();
await whenPollWormchainLogsStarts();
await thenWaitForAssertion(
() => expect(getBlockHeightSpy).toHaveReturnedTimes(1),
() => expect(getBlockLogsSpy).toHaveBeenCalledWith(3104, currentHeight)
);
});
it("should be return an empty array because to block is more greater than from block", async () => {
const currentHeight = 10n;
givenEvmBlockRepository(currentHeight);
givenMetadataRepository({ lastBlock: 15n });
givenStatsRepository();
givenPollWormchainLogs();
await whenPollWormchainLogsStarts();
await thenWaitForAssertion(() => expect(getBlockHeightSpy).toHaveReturnedTimes(1));
});
});
const givenEvmBlockRepository = (height?: bigint, logs: any = []) => {
wormchainBlockRepo = {
getBlockHeight: () => Promise.resolve(height),
getBlockLogs: () => Promise.resolve(logs),
};
getBlockHeightSpy = jest.spyOn(wormchainBlockRepo, "getBlockHeight");
getBlockLogsSpy = jest.spyOn(wormchainBlockRepo, "getBlockLogs");
handlerSpy = jest.spyOn(handlers, "working");
};
const givenMetadataRepository = (data?: PollWormchainLogsMetadata) => {
metadataRepo = {
get: () => Promise.resolve(data),
save: () => Promise.resolve(),
};
metadataSaveSpy = jest.spyOn(metadataRepo, "save");
};
const givenStatsRepository = () => {
statsRepo = {
count: () => {},
measure: () => {},
report: () => Promise.resolve(""),
};
};
const givenPollWormchainLogs = (from?: bigint) => {
cfg.setFromBlock(from);
pollWormchain = new PollWormchain(
wormchainBlockRepo,
metadataRepo,
statsRepo,
cfg,
"GetWormchainLogs"
);
};
const whenPollWormchainLogsStarts = async () => {
pollWormchain.run([handlers.working]);
};

View File

@ -0,0 +1,474 @@
import { wormchainLogMessagePublishedMapper } from "../../../../src/infrastructure/mappers/wormchain/wormchainLogMessagePublishedMapper";
import { describe, it, expect } from "@jest/globals";
import { WormchainBlockLogs } from "../../../../src/domain/entities/wormchain";
const addresses = ["wormhole1ufs3tlq4umljk0qfe8k5ya0x6hpavn897u2cnf9k0en9jr7qarqqaqfk2j"];
describe("wormchainLogMessagePublishedMapper", () => {
it("should be able to map log to wormchainLogMessagePublishedMapper", async () => {
// When
const result = wormchainLogMessagePublishedMapper(addresses, logWithOneTx) as any;
if (result) {
// Then
expect(result[0].name).toBe("log-message-published");
expect(result[0].chainId).toBe(3104);
expect(result[0].txHash).toBe(
"0xa08b0ac6ee67e21d3dd89f48f60cc907fc867288f4439bcf72731b0884d8aff2"
);
expect(result[0].address).toBe(
"wormhole1ufs3tlq4umljk0qfe8k5ya0x6hpavn897u2cnf9k0en9jr7qarqqaqfk2j"
);
expect(result[0].attributes.consistencyLevel).toBe(0);
expect(result[0].attributes.nonce).toBe(7671);
expect(result[0].attributes.payload).toBe(
"0100000000000000000000000000000000000000000000000000000000555643a3f5edec8471c75624ebc4079a634326d96a689e6157d79abe8f5a6f94472853bc00018622b98735cb870ae0cb22bd4ea58cfb512bd4002247ccd0b250eb6d0c5032fc00010000000000000000000000000000000000000000000000000000000000000000"
);
expect(result[0].attributes.sender).toBe(
"aeb534c45c3049d380b9d9b966f9895f53abd4301bfaff407fa09dea8ae7a924"
);
expect(result[0].attributes.sequence).toBe(28603);
}
});
it("should be able to map two logs to wormchainLogMessagePublishedMapper", async () => {
// When
const result = wormchainLogMessagePublishedMapper(addresses, logWithTwoTxs) as any;
if (result) {
// Then
expect(result[0].name).toBe("log-message-published");
expect(result[0].chainId).toBe(3104);
expect(result[0].txHash).toBe(
"0xa08b0ac6ee67e21d3dd89f48f60cc907fc867288f4439bcf72731b0884d8aff2"
);
expect(result[0].address).toBe(
"wormhole1ufs3tlq4umljk0qfe8k5ya0x6hpavn897u2cnf9k0en9jr7qarqqaqfk2j"
);
expect(result[0].attributes.consistencyLevel).toBe(0);
expect(result[0].attributes.nonce).toBe(7671);
expect(result[0].attributes.payload).toBe(
"0100000000000000000000000000000000000000000000000000000000555643a3f5edec8471c75624ebc4079a634326d96a689e6157d79abe8f5a6f94472853bc00018622b98735cb870ae0cb22bd4ea58cfb512bd4002247ccd0b250eb6d0c5032fc00010000000000000000000000000000000000000000000000000000000000000000"
);
expect(result[0].attributes.sender).toBe(
"aeb534c45c3049d380b9d9b966f9895f53abd4301bfaff407fa09dea8ae7a924"
);
expect(result[0].attributes.sequence).toBe(28603);
expect(result[1].name).toBe("log-message-published");
expect(result[1].chainId).toBe(3104);
expect(result[1].txHash).toBe(
"0xaeffa26c67ab657257635aaba23b7b77817f0ad69d2a507020e72ccfd30d0f35"
);
expect(result[1].address).toBe(
"wormhole1ufs3tlq4umljk0qfe8k5ya0x6hpavn897u2cnf9k0en9jr7qarqqaqfk2j"
);
expect(result[1].attributes.consistencyLevel).toBe(0);
expect(result[1].attributes.nonce).toBe(970);
expect(result[1].attributes.payload).toBe(
"010000000000000000000000000000000000000000000000000000000011c31e80c6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d6100011dec05e3bdd71fdb04e3305f6a0b107c80ea92dd44df6f0f29d86cba7508e26e00010000000000000000000000000000000000000000000000000000000000000000"
);
expect(result[1].attributes.sender).toBe(
"aeb534c45c3049d380b9d9b966f9895f53abd4301bfaff407fa09dea8ae7a924"
);
expect(result[1].attributes.sequence).toBe(28928);
}
});
});
const logWithOneTx: WormchainBlockLogs = {
transactions: [
{
hash: "0x987e77d2d8cf8b9c0b3998dc62dc94fad9de47c4e3b50ad9bfd3083d7ab958ff",
type: "wasm",
attributes: [
{
key: "X2NvbnRyYWN0X2FkZHJlc3M=",
value:
"d29ybWhvbGUxNGhqMnRhdnE4ZnBlc2R3eHhjdTQ0cnR5M2hoOTB2aHVqcnZjbXN0bDR6cjN0eG1mdnc5c3JyZzQ2NQ==",
index: true,
},
{ key: "YWN0aW9u", value: "c3VibWl0X29ic2VydmF0aW9ucw==", index: true },
{
key: "b3duZXI=",
value: "d29ybWhvbGUxODc4a3h6M3VnZXN2YTRoNGtmeng2Y3F0ZHk5NmN3d2RqajBwaHc=",
index: true,
},
],
},
{
hash: "0xa08b0ac6ee67e21d3dd89f48f60cc907fc867288f4439bcf72731b0884d8aff2",
type: "wasm",
attributes: [
{
key: "X2NvbnRyYWN0X2FkZHJlc3M=",
value:
"d29ybWhvbGUxajYydGt5cWhqeWpscXN5MzB1bnVhcW5uZDhkdDV3cXFucndqemYwZms3bnc0dzdkeHBzcWhheWFuZw==",
index: true,
},
{ key: "YWN0aW9u", value: "aW5jcmVhc2VfYWxsb3dhbmNl", index: true },
{ key: "YW1vdW50", value: "MTQzMTcxNjc3MQ==", index: true },
{
key: "b3duZXI=",
value:
"d29ybWhvbGUxNGVqcWp5cTh1bTRwM3hmcWo3NHlsZDV3YXFsamY4OGZ6MjV5eG5tYTBjbmdzcHhlM2xlczAwZnBqeA==",
index: true,
},
{
key: "c3BlbmRlcg==",
value:
"d29ybWhvbGUxNDY2bmYzenV4cHlhOHE5ZW14dWtkN3ZmdGFmNmg0cHNyMGEwN3NybDV6dzc0emg4NHlqcTRseWptaA==",
index: true,
},
],
},
{
hash: "0xa08b0ac6ee67e21d3dd89f48f60cc907fc867288f4439bcf72731b0884d8aff2",
type: "wasm",
attributes: [
{
key: "X2NvbnRyYWN0X2FkZHJlc3M=",
value:
"d29ybWhvbGUxNDY2bmYzenV4cHlhOHE5ZW14dWtkN3ZmdGFmNmg0cHNyMGEwN3NybDV6dzc0emg4NHlqcTRseWptaA==",
index: true,
},
{ key: "dHJhbnNmZXIuYW1vdW50", value: "MTQzMTcxNjc3MQ==", index: true },
{ key: "dHJhbnNmZXIuYmxvY2tfdGltZQ==", value: "MTcxMTE0MzIyMg==", index: true },
{ key: "dHJhbnNmZXIubm9uY2U=", value: "NzY3MQ==", index: true },
{
key: "dHJhbnNmZXIucmVjaXBpZW50",
value:
"ODYyMmI5ODczNWNiODcwYWUwY2IyMmJkNGVhNThjZmI1MTJiZDQwMDIyNDdjY2QwYjI1MGViNmQwYzUwMzJmYw==",
index: true,
},
{ key: "dHJhbnNmZXIucmVjaXBpZW50X2NoYWlu", value: "MQ==", index: true },
{
key: "dHJhbnNmZXIuc2VuZGVy",
value:
"YWU2NDA5MTAwN2U2ZWExODk5MjA5N2FhNGZiNjhlZTgzZjI0OWNlOTEyYTg0MzRmN2Q3ZTI2ODgwNGQ5OGZmMw==",
index: true,
},
{
key: "dHJhbnNmZXIudG9rZW4=",
value:
"ZjVlZGVjODQ3MWM3NTYyNGViYzQwNzlhNjM0MzI2ZDk2YTY4OWU2MTU3ZDc5YWJlOGY1YTZmOTQ0NzI4NTNiYw==",
index: true,
},
{ key: "dHJhbnNmZXIudG9rZW5fY2hhaW4=", value: "MQ==", index: true },
],
},
{
hash: "0xa08b0ac6ee67e21d3dd89f48f60cc907fc867288f4439bcf72731b0884d8aff2",
type: "wasm",
attributes: [
{
key: "X2NvbnRyYWN0X2FkZHJlc3M=",
value:
"d29ybWhvbGUxajYydGt5cWhqeWpscXN5MzB1bnVhcW5uZDhkdDV3cXFucndqemYwZms3bnc0dzdkeHBzcWhheWFuZw==",
index: true,
},
{ key: "YWN0aW9u", value: "YnVybl9mcm9t", index: true },
{ key: "YW1vdW50", value: "MTQzMTcxNjc3MQ==", index: true },
{
key: "Ynk=",
value:
"d29ybWhvbGUxNDY2bmYzenV4cHlhOHE5ZW14dWtkN3ZmdGFmNmg0cHNyMGEwN3NybDV6dzc0emg4NHlqcTRseWptaA==",
index: true,
},
{
key: "ZnJvbQ==",
value:
"d29ybWhvbGUxNGVqcWp5cTh1bTRwM3hmcWo3NHlsZDV3YXFsamY4OGZ6MjV5eG5tYTBjbmdzcHhlM2xlczAwZnBqeA==",
index: true,
},
],
},
{
hash: "0xa08b0ac6ee67e21d3dd89f48f60cc907fc867288f4439bcf72731b0884d8aff2",
type: "wasm",
attributes: [
{
key: "X2NvbnRyYWN0X2FkZHJlc3M=",
value:
"d29ybWhvbGUxdWZzM3RscTR1bWxqazBxZmU4azV5YTB4NmhwYXZuODk3dTJjbmY5azBlbjlqcjdxYXJxcWFxZmsyag==",
index: true,
},
{ key: "bWVzc2FnZS5ibG9ja190aW1l", value: "MTcxMTE0MzIyMg==", index: true },
{ key: "bWVzc2FnZS5jaGFpbl9pZA==", value: "MzEwNA==", index: true },
{
key: "bWVzc2FnZS5tZXNzYWdl",
value:
"MDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDU1NTY0M2EzZjVlZGVjODQ3MWM3NTYyNGViYzQwNzlhNjM0MzI2ZDk2YTY4OWU2MTU3ZDc5YWJlOGY1YTZmOTQ0NzI4NTNiYzAwMDE4NjIyYjk4NzM1Y2I4NzBhZTBjYjIyYmQ0ZWE1OGNmYjUxMmJkNDAwMjI0N2NjZDBiMjUwZWI2ZDBjNTAzMmZjMDAwMTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA=",
index: true,
},
{ key: "bWVzc2FnZS5ub25jZQ==", value: "NzY3MQ==", index: true },
{
key: "bWVzc2FnZS5zZW5kZXI=",
value:
"YWViNTM0YzQ1YzMwNDlkMzgwYjlkOWI5NjZmOTg5NWY1M2FiZDQzMDFiZmFmZjQwN2ZhMDlkZWE4YWU3YTkyNA==",
index: true,
},
{ key: "bWVzc2FnZS5zZXF1ZW5jZQ==", value: "Mjg2MDM=", index: true },
],
},
],
blockHeight: 7626736n,
timestamp: 1711143222043,
chainId: 3104,
};
const logWithTwoTxs: WormchainBlockLogs = {
transactions: [
{
hash: "0x987e77d2d8cf8b9c0b3998dc62dc94fad9de47c4e3b50ad9bfd3083d7ab958ff",
type: "wasm",
attributes: [
{
key: "X2NvbnRyYWN0X2FkZHJlc3M=",
value:
"d29ybWhvbGUxNGhqMnRhdnE4ZnBlc2R3eHhjdTQ0cnR5M2hoOTB2aHVqcnZjbXN0bDR6cjN0eG1mdnc5c3JyZzQ2NQ==",
index: true,
},
{ key: "YWN0aW9u", value: "c3VibWl0X29ic2VydmF0aW9ucw==", index: true },
{
key: "b3duZXI=",
value: "d29ybWhvbGUxODc4a3h6M3VnZXN2YTRoNGtmeng2Y3F0ZHk5NmN3d2RqajBwaHc=",
index: true,
},
],
},
{
hash: "0xa08b0ac6ee67e21d3dd89f48f60cc907fc867288f4439bcf72731b0884d8aff2",
type: "wasm",
attributes: [
{
key: "X2NvbnRyYWN0X2FkZHJlc3M=",
value:
"d29ybWhvbGUxajYydGt5cWhqeWpscXN5MzB1bnVhcW5uZDhkdDV3cXFucndqemYwZms3bnc0dzdkeHBzcWhheWFuZw==",
index: true,
},
{ key: "YWN0aW9u", value: "aW5jcmVhc2VfYWxsb3dhbmNl", index: true },
{ key: "YW1vdW50", value: "MTQzMTcxNjc3MQ==", index: true },
{
key: "b3duZXI=",
value:
"d29ybWhvbGUxNGVqcWp5cTh1bTRwM3hmcWo3NHlsZDV3YXFsamY4OGZ6MjV5eG5tYTBjbmdzcHhlM2xlczAwZnBqeA==",
index: true,
},
{
key: "c3BlbmRlcg==",
value:
"d29ybWhvbGUxNDY2bmYzenV4cHlhOHE5ZW14dWtkN3ZmdGFmNmg0cHNyMGEwN3NybDV6dzc0emg4NHlqcTRseWptaA==",
index: true,
},
],
},
{
hash: "0xa08b0ac6ee67e21d3dd89f48f60cc907fc867288f4439bcf72731b0884d8aff2",
type: "wasm",
attributes: [
{
key: "X2NvbnRyYWN0X2FkZHJlc3M=",
value:
"d29ybWhvbGUxNDY2bmYzenV4cHlhOHE5ZW14dWtkN3ZmdGFmNmg0cHNyMGEwN3NybDV6dzc0emg4NHlqcTRseWptaA==",
index: true,
},
{ key: "dHJhbnNmZXIuYW1vdW50", value: "MTQzMTcxNjc3MQ==", index: true },
{ key: "dHJhbnNmZXIuYmxvY2tfdGltZQ==", value: "MTcxMTE0MzIyMg==", index: true },
{ key: "dHJhbnNmZXIubm9uY2U=", value: "NzY3MQ==", index: true },
{
key: "dHJhbnNmZXIucmVjaXBpZW50",
value:
"ODYyMmI5ODczNWNiODcwYWUwY2IyMmJkNGVhNThjZmI1MTJiZDQwMDIyNDdjY2QwYjI1MGViNmQwYzUwMzJmYw==",
index: true,
},
{ key: "dHJhbnNmZXIucmVjaXBpZW50X2NoYWlu", value: "MQ==", index: true },
{
key: "dHJhbnNmZXIuc2VuZGVy",
value:
"YWU2NDA5MTAwN2U2ZWExODk5MjA5N2FhNGZiNjhlZTgzZjI0OWNlOTEyYTg0MzRmN2Q3ZTI2ODgwNGQ5OGZmMw==",
index: true,
},
{
key: "dHJhbnNmZXIudG9rZW4=",
value:
"ZjVlZGVjODQ3MWM3NTYyNGViYzQwNzlhNjM0MzI2ZDk2YTY4OWU2MTU3ZDc5YWJlOGY1YTZmOTQ0NzI4NTNiYw==",
index: true,
},
{ key: "dHJhbnNmZXIudG9rZW5fY2hhaW4=", value: "MQ==", index: true },
],
},
{
hash: "0xa08b0ac6ee67e21d3dd89f48f60cc907fc867288f4439bcf72731b0884d8aff2",
type: "wasm",
attributes: [
{
key: "X2NvbnRyYWN0X2FkZHJlc3M=",
value:
"d29ybWhvbGUxajYydGt5cWhqeWpscXN5MzB1bnVhcW5uZDhkdDV3cXFucndqemYwZms3bnc0dzdkeHBzcWhheWFuZw==",
index: true,
},
{ key: "YWN0aW9u", value: "YnVybl9mcm9t", index: true },
{ key: "YW1vdW50", value: "MTQzMTcxNjc3MQ==", index: true },
{
key: "Ynk=",
value:
"d29ybWhvbGUxNDY2bmYzenV4cHlhOHE5ZW14dWtkN3ZmdGFmNmg0cHNyMGEwN3NybDV6dzc0emg4NHlqcTRseWptaA==",
index: true,
},
{
key: "ZnJvbQ==",
value:
"d29ybWhvbGUxNGVqcWp5cTh1bTRwM3hmcWo3NHlsZDV3YXFsamY4OGZ6MjV5eG5tYTBjbmdzcHhlM2xlczAwZnBqeA==",
index: true,
},
],
},
{
hash: "0xa08b0ac6ee67e21d3dd89f48f60cc907fc867288f4439bcf72731b0884d8aff2",
type: "wasm",
attributes: [
{
key: "X2NvbnRyYWN0X2FkZHJlc3M=",
value:
"d29ybWhvbGUxdWZzM3RscTR1bWxqazBxZmU4azV5YTB4NmhwYXZuODk3dTJjbmY5azBlbjlqcjdxYXJxcWFxZmsyag==",
index: true,
},
{ key: "bWVzc2FnZS5ibG9ja190aW1l", value: "MTcxMTE0MzIyMg==", index: true },
{ key: "bWVzc2FnZS5jaGFpbl9pZA==", value: "MzEwNA==", index: true },
{
key: "bWVzc2FnZS5tZXNzYWdl",
value:
"MDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDU1NTY0M2EzZjVlZGVjODQ3MWM3NTYyNGViYzQwNzlhNjM0MzI2ZDk2YTY4OWU2MTU3ZDc5YWJlOGY1YTZmOTQ0NzI4NTNiYzAwMDE4NjIyYjk4NzM1Y2I4NzBhZTBjYjIyYmQ0ZWE1OGNmYjUxMmJkNDAwMjI0N2NjZDBiMjUwZWI2ZDBjNTAzMmZjMDAwMTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA=",
index: true,
},
{ key: "bWVzc2FnZS5ub25jZQ==", value: "NzY3MQ==", index: true },
{
key: "bWVzc2FnZS5zZW5kZXI=",
value:
"YWViNTM0YzQ1YzMwNDlkMzgwYjlkOWI5NjZmOTg5NWY1M2FiZDQzMDFiZmFmZjQwN2ZhMDlkZWE4YWU3YTkyNA==",
index: true,
},
{ key: "bWVzc2FnZS5zZXF1ZW5jZQ==", value: "Mjg2MDM=", index: true },
],
},
{
hash: "0xaeffa26c67ab657257635aaba23b7b77817f0ad69d2a507020e72ccfd30d0f35",
type: "wasm",
attributes: [
{
key: "X2NvbnRyYWN0X2FkZHJlc3M=",
value:
"d29ybWhvbGUxN2ZyOGF3bnlzeXYzbnQ1amU0c3RyY3pkdXBzc2w4dTlqcWFtODkwamZ2NzJzaDMyeXlxcWh0ZzNyeQ==",
index: true,
},
{ key: "YWN0aW9u", value: "aW5jcmVhc2VfYWxsb3dhbmNl", index: true },
{ key: "YW1vdW50", value: "Mjk4MDAwMDAw", index: true },
{
key: "b3duZXI=",
value:
"d29ybWhvbGUxNGVqcWp5cTh1bTRwM3hmcWo3NHlsZDV3YXFsamY4OGZ6MjV5eG5tYTBjbmdzcHhlM2xlczAwZnBqeA==",
index: true,
},
{
key: "c3BlbmRlcg==",
value:
"d29ybWhvbGUxNDY2bmYzenV4cHlhOHE5ZW14dWtkN3ZmdGFmNmg0cHNyMGEwN3NybDV6dzc0emg4NHlqcTRseWptaA==",
index: true,
},
],
},
{
hash: "0xaeffa26c67ab657257635aaba23b7b77817f0ad69d2a507020e72ccfd30d0f35",
type: "wasm",
attributes: [
{
key: "X2NvbnRyYWN0X2FkZHJlc3M=",
value:
"d29ybWhvbGUxNDY2bmYzenV4cHlhOHE5ZW14dWtkN3ZmdGFmNmg0cHNyMGEwN3NybDV6dzc0emg4NHlqcTRseWptaA==",
index: true,
},
{ key: "dHJhbnNmZXIuYW1vdW50", value: "Mjk4MDAwMDAw", index: true },
{ key: "dHJhbnNmZXIuYmxvY2tfdGltZQ==", value: "MTcxMTQwNDMzMQ==", index: true },
{ key: "dHJhbnNmZXIubm9uY2U=", value: "OTcw", index: true },
{
key: "dHJhbnNmZXIucmVjaXBpZW50",
value:
"MWRlYzA1ZTNiZGQ3MWZkYjA0ZTMzMDVmNmEwYjEwN2M4MGVhOTJkZDQ0ZGY2ZjBmMjlkODZjYmE3NTA4ZTI2ZQ==",
index: true,
},
{ key: "dHJhbnNmZXIucmVjaXBpZW50X2NoYWlu", value: "MQ==", index: true },
{
key: "dHJhbnNmZXIuc2VuZGVy",
value:
"YWU2NDA5MTAwN2U2ZWExODk5MjA5N2FhNGZiNjhlZTgzZjI0OWNlOTEyYTg0MzRmN2Q3ZTI2ODgwNGQ5OGZmMw==",
index: true,
},
{
key: "dHJhbnNmZXIudG9rZW4=",
value:
"YzZmYTdhZjNiZWRiYWQzYTNkNjVmMzZhYWJjOTc0MzFiMWJiZTRjMmQyZjZlMGU0N2NhNjAyMDM0NTJmNWQ2MQ==",
index: true,
},
{ key: "dHJhbnNmZXIudG9rZW5fY2hhaW4=", value: "MQ==", index: true },
],
},
{
hash: "0xaeffa26c67ab657257635aaba23b7b77817f0ad69d2a507020e72ccfd30d0f35",
type: "wasm",
attributes: [
{
key: "X2NvbnRyYWN0X2FkZHJlc3M=",
value:
"d29ybWhvbGUxN2ZyOGF3bnlzeXYzbnQ1amU0c3RyY3pkdXBzc2w4dTlqcWFtODkwamZ2NzJzaDMyeXlxcWh0ZzNyeQ==",
index: true,
},
{ key: "YWN0aW9u", value: "YnVybl9mcm9t", index: true },
{ key: "YW1vdW50", value: "Mjk4MDAwMDAw", index: true },
{
key: "Ynk=",
value:
"d29ybWhvbGUxNDY2bmYzenV4cHlhOHE5ZW14dWtkN3ZmdGFmNmg0cHNyMGEwN3NybDV6dzc0emg4NHlqcTRseWptaA==",
index: true,
},
{
key: "ZnJvbQ==",
value:
"d29ybWhvbGUxNGVqcWp5cTh1bTRwM3hmcWo3NHlsZDV3YXFsamY4OGZ6MjV5eG5tYTBjbmdzcHhlM2xlczAwZnBqeA==",
index: true,
},
],
},
{
hash: "0xaeffa26c67ab657257635aaba23b7b77817f0ad69d2a507020e72ccfd30d0f35",
type: "wasm",
attributes: [
{
key: "X2NvbnRyYWN0X2FkZHJlc3M=",
value:
"d29ybWhvbGUxdWZzM3RscTR1bWxqazBxZmU4azV5YTB4NmhwYXZuODk3dTJjbmY5azBlbjlqcjdxYXJxcWFxZmsyag==",
index: true,
},
{ key: "bWVzc2FnZS5ibG9ja190aW1l", value: "MTcxMTQwNDMzMQ==", index: true },
{ key: "bWVzc2FnZS5jaGFpbl9pZA==", value: "MzEwNA==", index: true },
{
key: "bWVzc2FnZS5tZXNzYWdl",
value:
"MDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDExYzMxZTgwYzZmYTdhZjNiZWRiYWQzYTNkNjVmMzZhYWJjOTc0MzFiMWJiZTRjMmQyZjZlMGU0N2NhNjAyMDM0NTJmNWQ2MTAwMDExZGVjMDVlM2JkZDcxZmRiMDRlMzMwNWY2YTBiMTA3YzgwZWE5MmRkNDRkZjZmMGYyOWQ4NmNiYTc1MDhlMjZlMDAwMTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA=",
index: true,
},
{ key: "bWVzc2FnZS5ub25jZQ==", value: "OTcw", index: true },
{
key: "bWVzc2FnZS5zZW5kZXI=",
value:
"YWViNTM0YzQ1YzMwNDlkMzgwYjlkOWI5NjZmOTg5NWY1M2FiZDQzMDFiZmFmZjQwN2ZhMDlkZWE4YWU3YTkyNA==",
index: true,
},
{ key: "bWVzc2FnZS5zZXF1ZW5jZQ==", value: "Mjg5Mjg=", index: true },
],
},
],
blockHeight: 7626736n,
timestamp: 1711143222043,
chainId: 3104,
};

View File

@ -1,6 +1,8 @@
import { mockRpcPool } from "../../mocks/mockRpcPool";
mockRpcPool();
import { RateLimitedWormchainJsonRPCBlockRepository } from "../../../src/infrastructure/repositories/wormchain/RateLimitedWormchainJsonRPCBlockRepository";
import { RateLimitedAptosJsonRPCBlockRepository } from "../../../src/infrastructure/repositories/aptos/RateLimitedAptosJsonRPCBlockRepository";
import { RateLimitedEvmJsonRPCBlockRepository } from "../../../src/infrastructure/repositories/evm/RateLimitedEvmJsonRPCBlockRepository";
import { RateLimitedSuiJsonRPCBlockRepository } from "../../../src/infrastructure/repositories/sui/RateLimitedSuiJsonRPCBlockRepository";
import { describe, expect, it } from "@jest/globals";
@ -12,7 +14,6 @@ import {
RateLimitedSolanaSlotRepository,
SnsEventRepository,
} from "../../../src/infrastructure/repositories";
import { RateLimitedAptosJsonRPCBlockRepository } from "../../../src/infrastructure/repositories/aptos/RateLimitedAptosJsonRPCBlockRepository";
describe("RepositoriesBuilder", () => {
it("should be throw error because dose not have any chain", async () => {
@ -107,5 +108,8 @@ describe("RepositoriesBuilder", () => {
expect(repos.getSolanaSlotRepository()).toBeInstanceOf(RateLimitedSolanaSlotRepository);
expect(repos.getSuiRepository()).toBeInstanceOf(RateLimitedSuiJsonRPCBlockRepository);
expect(repos.getAptosRepository()).toBeInstanceOf(RateLimitedAptosJsonRPCBlockRepository);
expect(repos.getWormchainRepository()).toBeInstanceOf(
RateLimitedWormchainJsonRPCBlockRepository
);
});
});

View File

@ -11,6 +11,7 @@ import {
SolanaSlotRepository,
StatRepository,
SuiRepository,
WormchainRepository,
} from "../../../src/domain/repositories";
const dirPath = "./metadata-repo/jobs";
@ -21,6 +22,7 @@ const snsRepo = {} as any as SnsEventRepository;
const solanaSlotRepo = {} as any as SolanaSlotRepository;
const suiRepo = {} as any as SuiRepository;
const aptosRepo = {} as any as AptosRepository;
const wormchainRepo = {} as any as WormchainRepository;
let repo: StaticJobRepository;
@ -36,6 +38,7 @@ describe("StaticJobRepository", () => {
solanaSlotRepo,
suiRepo,
aptosRepo,
wormchainRepo,
});
});

View File

@ -157,6 +157,13 @@ export const configMock = (): Config => {
rpcs: ["http://localhost"],
timeout: 10000,
},
wormchain: {
name: "wormchain",
network: "testnet",
chainId: 3104,
rpcs: ["http://localhost"],
timeout: 10000,
},
};
const snsConfig: SnsConfig = {
@ -184,7 +191,7 @@ export const configMock = (): Config => {
dir: "./metadata-repo/jobs",
},
chains: chainsRecord,
enabledPlatforms: ["solana", "evm", "sui", "aptos"],
enabledPlatforms: ["solana", "evm", "sui", "aptos", "wormchain"],
};
return cfg;

View File

@ -223,20 +223,20 @@ data:
},
"handlers": [
{
"action": "HandleEvmLogs",
"target": "sns",
"mapper": "evmLogMessagePublishedMapper",
"config": {
"abi": "event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel)",
"filter": {
"addresses": [
"0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C"
],
"topics": [
"0x6eb224fb001ed210e379b335e35efe88672a8ce935d981a6896b27ffdf52a3b2"
]
},
"metricName": "process_source_event"
"action": "HandleEvmLogs",
"target": "sns",
"mapper": "evmLogMessagePublishedMapper",
"config": {
"abi": "event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel)",
"filter": {
"addresses": [
"0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C"
],
"topics": [
"0x6eb224fb001ed210e379b335e35efe88672a8ce935d981a6896b27ffdf52a3b2"
]
},
"metricName": "process_source_event"
}
}
]
@ -396,7 +396,7 @@ data:
"config": {
"abi": "event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel)",
"filter": {
"addresses": ["0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B"],
"addresses": ["0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B", "0x27428dd2d3dd32a4d7f7c497eaaa23130d894911", "0x3ee18b2214aff97000d974cf647e7c347e8fa585"],
"topics": ["0x6eb224fb001ed210e379b335e35efe88672a8ce935d981a6896b27ffdf52a3b2"]
},
"metricName": "process_source_event"

View File

@ -275,6 +275,35 @@ data:
}
}
]
},
{
"id": "poll-log-message-published-wormchain",
"chain": "wormchain",
"source": {
"action": "PollWormchain",
"config": {
"blockBatchSize": 100,
"commitment": "latest",
"interval": 5000,
"addresses": ["wormhole16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580wsazcjwp"],
"chain": "wormchain",
"chainId": 3104
}
},
"handlers": [
{
"action": "HandleWormchainLogs",
"target": "sns",
"mapper": "wormchainLogMessagePublishedMapper",
"config": {
"abi": "",
"filter": {
"addresses": ["wormhole16jzpxp0e8550c9aht6q9svcux30vtyyyyxv5w2l2djjra46580wsazcjwp"]
},
"metricName": "process_source_event"
}
}
]
}
]
mainnet-jobs.json: |-
@ -488,6 +517,35 @@ data:
}
}
]
},
{
"id": "poll-log-message-published-wormchain",
"chain": "wormchain",
"source": {
"action": "PollWormchain",
"config": {
"blockBatchSize": 100,
"commitment": "latest",
"interval": 5000,
"addresses": ["wormhole1ufs3tlq4umljk0qfe8k5ya0x6hpavn897u2cnf9k0en9jr7qarqqaqfk2j"],
"chain": "wormchain",
"chainId": 3104
}
},
"handlers": [
{
"action": "HandleWormchainLogs",
"target": "sns",
"mapper": "wormchainLogMessagePublishedMapper",
"config": {
"abi": "",
"filter": {
"addresses": ["wormhole1ufs3tlq4umljk0qfe8k5ya0x6hpavn897u2cnf9k0en9jr7qarqqaqfk2j"]
},
"metricName": "process_source_event"
}
}
]
}
]
---