From 6f7d457386d10fc90e25e43ec59b66e88efc0590 Mon Sep 17 00:00:00 2001 From: Julian <52217955+julianmerlo95@users.noreply.github.com> Date: Tue, 25 Jun 2024 16:56:15 -0300 Subject: [PATCH] [Blockchain Watcher] (ALGORAND) Map algorand events (source and target) (#1505) * Start implement algorand events * Create handler and improve repository * Create mapper for redeem events * Resolve some //TODO comments * Support source events * Improve application id name * Validate payload length * Validate logs property * Add all test for infrastructere and domain * Improve code * Remove any type * Remove throw for wormchain * Resolve comment in PR --------- Co-authored-by: julian merlo --- .../config/custom-environment-variables.json | 7 + blockchain-watcher/config/default.json | 9 +- blockchain-watcher/config/mainnet.json | 5 + blockchain-watcher/package-lock.json | 1 + blockchain-watcher/package.json | 1 + .../algorand/GetAlgorandTransactions.ts | 41 +++ .../algorand/HandleAlgorandTransactions.ts | 49 ++++ .../domain/actions/algorand/PollAlgorand.ts | 221 ++++++++++++++++ .../src/domain/actions/index.ts | 3 + .../actions/wormchain/GetWormchainLogs.ts | 3 + .../actions/wormchain/GetWormchainRedeems.ts | 4 + .../src/domain/entities/algorand.ts | 16 ++ blockchain-watcher/src/domain/repositories.ts | 14 +- .../algorandLogMessagePublishedMapper.ts | 54 ++++ .../algorandRedeemedTransactionFoundMapper.ts | 86 +++++++ .../mappers/contractsMapperConfig.json | 15 ++ .../repositories/RepositoriesBuilder.ts | 34 ++- .../repositories/StaticJobRepository.ts | 40 ++- .../AlgorandJsonRPCBlockRepository.ts | 114 +++++++++ ...teLimitedAlgorandJsonRPCBlockRepository.ts | 29 +++ .../src/infrastructure/repositories/index.ts | 1 + .../WormchainJsonRPCBlockRepository.ts | 12 +- .../algorand/GetAlgorandTransactions.test.ts | 113 +++++++++ .../HandleAlgorandTransactions.test.ts | 113 +++++++++ .../actions/algorand/PollAlgorand.test.ts | 110 ++++++++ .../algorandLogMessagePublishedMapper.test.ts | 51 ++++ ...randRedeemedTransactionFoundMapper.test.ts | 50 ++++ .../AlgorandJsonRPCBlockRepository.test.ts | 235 ++++++++++++++++++ .../repositories/RepositoriesBuilder.test.ts | 4 +- .../repositories/StaticJobRepository.test.ts | 3 + blockchain-watcher/test/mocks/configMock.ts | 9 +- .../workers/source-events-3.yaml | 66 +++++ .../workers/target-events-3.yaml | 74 ++++++ 33 files changed, 1574 insertions(+), 13 deletions(-) create mode 100644 blockchain-watcher/src/domain/actions/algorand/GetAlgorandTransactions.ts create mode 100644 blockchain-watcher/src/domain/actions/algorand/HandleAlgorandTransactions.ts create mode 100644 blockchain-watcher/src/domain/actions/algorand/PollAlgorand.ts create mode 100644 blockchain-watcher/src/domain/entities/algorand.ts create mode 100644 blockchain-watcher/src/infrastructure/mappers/algorand/algorandLogMessagePublishedMapper.ts create mode 100644 blockchain-watcher/src/infrastructure/mappers/algorand/algorandRedeemedTransactionFoundMapper.ts create mode 100644 blockchain-watcher/src/infrastructure/repositories/algorand/AlgorandJsonRPCBlockRepository.ts create mode 100644 blockchain-watcher/src/infrastructure/repositories/algorand/RateLimitedAlgorandJsonRPCBlockRepository.ts create mode 100644 blockchain-watcher/test/domain/actions/algorand/GetAlgorandTransactions.test.ts create mode 100644 blockchain-watcher/test/domain/actions/algorand/HandleAlgorandTransactions.test.ts create mode 100644 blockchain-watcher/test/domain/actions/algorand/PollAlgorand.test.ts create mode 100644 blockchain-watcher/test/infrastructure/mappers/algorand/algorandLogMessagePublishedMapper.test.ts create mode 100644 blockchain-watcher/test/infrastructure/mappers/algorand/algorandRedeemedTransactionFoundMapper.test.ts create mode 100644 blockchain-watcher/test/infrastructure/repositories/AlgorandJsonRPCBlockRepository.test.ts diff --git a/blockchain-watcher/config/custom-environment-variables.json b/blockchain-watcher/config/custom-environment-variables.json index bf83ffb4..83d75074 100644 --- a/blockchain-watcher/config/custom-environment-variables.json +++ b/blockchain-watcher/config/custom-environment-variables.json @@ -242,6 +242,13 @@ "__name": "SEI_RPCS", "__format": "json" } + }, + "algorand": { + "network": "ALGORAND_NETWORK", + "rpcs": { + "__name": "ALGORAND_RPCS", + "__format": "json" + } } } } diff --git a/blockchain-watcher/config/default.json b/blockchain-watcher/config/default.json index 42dd1235..aaf3e97f 100644 --- a/blockchain-watcher/config/default.json +++ b/blockchain-watcher/config/default.json @@ -3,7 +3,7 @@ "port": 9090, "logLevel": "debug", "dryRun": true, - "enabledPlatforms": ["solana", "evm", "sui", "aptos", "wormchain", "sei"], + "enabledPlatforms": ["solana", "evm", "sui", "aptos", "wormchain", "sei", "algorand"], "sns": { "topicArn": "arn:aws:sns:us-east-1:000000000000:localstack-topic.fifo", "region": "us-east-1", @@ -70,6 +70,13 @@ "rpcs": ["https://testnet.emerald.oasis.dev"], "timeout": 10000 }, + "algorand": { + "name": "algorand", + "network": "testnet", + "chainId": 8, + "rpcs": [["https://testnet-api.algonode.cloud"], ["https://testnet-idx.algonode.cloud"]], + "timeout": 10000 + }, "fantom": { "name": "fantom", "network": "testnet", diff --git a/blockchain-watcher/config/mainnet.json b/blockchain-watcher/config/mainnet.json index e7c00996..03398219 100644 --- a/blockchain-watcher/config/mainnet.json +++ b/blockchain-watcher/config/mainnet.json @@ -48,6 +48,11 @@ "chainId": 7, "rpcs": ["https://emerald.oasis.dev"] }, + "algorand": { + "network": "mainnet", + "chainId": 8, + "rpcs": [["https://mainnet-api.algonode.cloud"], ["https://mainnet-idx.algonode.cloud"]] + }, "fantom": { "network": "mainnet", "chainId": 10, diff --git a/blockchain-watcher/package-lock.json b/blockchain-watcher/package-lock.json index 8694a4d3..8a08f8da 100644 --- a/blockchain-watcher/package-lock.json +++ b/blockchain-watcher/package-lock.json @@ -14,6 +14,7 @@ "@cosmjs/proto-signing": "^0.32.3", "@mysten/sui.js": "^0.49.1", "@xlabs/rpc-pool": "^0.0.4", + "algosdk": "^2.8.0", "axios": "^1.6.0", "bs58": "^5.0.0", "config": "^3.3.9", diff --git a/blockchain-watcher/package.json b/blockchain-watcher/package.json index 45074bdf..16c5d70a 100644 --- a/blockchain-watcher/package.json +++ b/blockchain-watcher/package.json @@ -24,6 +24,7 @@ "@cosmjs/proto-signing": "^0.32.3", "@mysten/sui.js": "^0.49.1", "@xlabs/rpc-pool": "^0.0.4", + "algosdk": "^2.8.0", "axios": "^1.6.0", "bs58": "^5.0.0", "config": "^3.3.9", diff --git a/blockchain-watcher/src/domain/actions/algorand/GetAlgorandTransactions.ts b/blockchain-watcher/src/domain/actions/algorand/GetAlgorandTransactions.ts new file mode 100644 index 00000000..0b7c1533 --- /dev/null +++ b/blockchain-watcher/src/domain/actions/algorand/GetAlgorandTransactions.ts @@ -0,0 +1,41 @@ +import { AlgorandTransaction } from "../../entities/algorand"; +import { AlgorandRepository } from "../../repositories"; +import { GetAlgorandOpts } from "./PollAlgorand"; +import winston from "winston"; + +export class GetAlgorandTransactions { + private readonly blockRepo: AlgorandRepository; + protected readonly logger: winston.Logger; + + constructor(blockRepo: AlgorandRepository) { + this.logger = winston.child({ module: "GetAlgorandTransactions" }); + this.blockRepo = blockRepo; + } + + async execute(range: Range, opts: GetAlgorandOpts): Promise { + const { fromBlock, toBlock } = range; + const chain = opts.chain; + + if (fromBlock > toBlock) { + this.logger.info( + `[${chain}][exec] Invalid range [fromBlock: ${fromBlock} - toBlock: ${toBlock}]` + ); + return []; + } + this.logger.info( + `[${chain}][exec] Processing blocks [fromBlock: ${fromBlock} - toBlock: ${toBlock}]` + ); + + const txs = await this.blockRepo.getTransactions(opts.applicationIds[0], fromBlock, toBlock); + + this.logger.info( + `[${chain}][exec] Got ${txs?.length} transactions to process [fromBlock: ${fromBlock} - toBlock: ${toBlock}]` + ); + return txs; + } +} + +type Range = { + fromBlock: bigint; + toBlock: bigint; +}; diff --git a/blockchain-watcher/src/domain/actions/algorand/HandleAlgorandTransactions.ts b/blockchain-watcher/src/domain/actions/algorand/HandleAlgorandTransactions.ts new file mode 100644 index 00000000..dae6fff2 --- /dev/null +++ b/blockchain-watcher/src/domain/actions/algorand/HandleAlgorandTransactions.ts @@ -0,0 +1,49 @@ +import { TransactionFoundEvent } from "../../entities"; +import { AlgorandTransaction } from "../../entities/algorand"; +import { StatRepository } from "../../repositories"; + +export class HandleAlgorandTransactions { + constructor( + private readonly cfg: HandleAlgorandTransactionsOptions, + private readonly mapper: (tx: AlgorandTransaction, filter: Filter[]) => TransactionFoundEvent, + private readonly target: (parsed: TransactionFoundEvent[]) => Promise, + private readonly statsRepo: StatRepository + ) {} + + public async handle(txs: AlgorandTransaction[]): Promise { + const items: TransactionFoundEvent[] = []; + + for (const tx of txs) { + const txMapped = this.mapper(tx, this.cfg.filter); + if (txMapped) { + this.report(txMapped.attributes.protocol); + items.push(txMapped); + } + } + + await this.target(items); + + return items; + } + + private report(protocol: string) { + const labels = { + job: this.cfg.id, + chain: "algorand", + protocol: protocol ?? "unknown", + commitment: "latest", + }; + this.statsRepo.count(this.cfg.metricName, labels); + } +} + +export interface HandleAlgorandTransactionsOptions { + metricName: string; + filter: Filter[]; + id: string; +} + +type Filter = { + applicationIds: string; + applicationAddress: string; +}; diff --git a/blockchain-watcher/src/domain/actions/algorand/PollAlgorand.ts b/blockchain-watcher/src/domain/actions/algorand/PollAlgorand.ts new file mode 100644 index 00000000..0ff2607b --- /dev/null +++ b/blockchain-watcher/src/domain/actions/algorand/PollAlgorand.ts @@ -0,0 +1,221 @@ +import { AlgorandRepository, MetadataRepository, StatRepository } from "../../repositories"; +import { GetAlgorandTransactions } from "./GetAlgorandTransactions"; +import { AlgorandTransaction } from "../../entities/algorand"; +import { RunPollingJob } from "../RunPollingJob"; +import winston from "winston"; + +const ID = "watch-algorand-logs"; + +export class PollAlgorand extends RunPollingJob { + protected readonly logger: winston.Logger; + + private readonly blockRepo: AlgorandRepository; + private readonly metadataRepo: MetadataRepository; + private readonly statsRepo: StatRepository; + private readonly getAlgorand: GetAlgorandTransactions; + + private cfg: PollAlgorandConfig; + private latestBlockHeight?: bigint; + private blockHeightCursor?: bigint; + private lastRange?: { fromBlock: bigint; toBlock: bigint }; + + constructor( + blockRepo: AlgorandRepository, + metadataRepo: MetadataRepository, + statsRepo: StatRepository, + cfg: PollAlgorandConfig + ) { + super(cfg.id, statsRepo, cfg.interval); + this.blockRepo = blockRepo; + this.metadataRepo = metadataRepo; + this.statsRepo = statsRepo; + this.cfg = cfg; + this.logger = winston.child({ module: "PollAlgorand", label: this.cfg.id }); + this.getAlgorand = new GetAlgorandTransactions(blockRepo); + } + + protected async preHook(): Promise { + const metadata = await this.metadataRepo.get(this.cfg.id); + if (metadata) { + this.blockHeightCursor = BigInt(metadata.lastBlock); + } + } + + protected async hasNext(): Promise { + const hasFinished = this.cfg.hasFinished(this.blockHeightCursor); + if (hasFinished) { + this.logger.info( + `[hasNext] PollAlgorand: (${this.cfg.id}) Finished processing all blocks from ${this.cfg.fromBlock} to ${this.cfg.toBlock}` + ); + } + + return !hasFinished; + } + + protected async get(): Promise { + this.latestBlockHeight = await this.blockRepo.getBlockHeight(); + + const range = this.getBlockRange(this.latestBlockHeight!); + + const algorandTransactions = await this.getAlgorand.execute(range, { + applicationIds: this.cfg.applicationIds, + chainId: this.cfg.chainId, + chain: this.cfg.chain, + }); + + this.lastRange = range; + + return algorandTransactions; + } + + protected async persist(): Promise { + this.blockHeightCursor = this.lastRange?.toBlock ?? this.blockHeightCursor; + if (this.blockHeightCursor) { + await this.metadataRepo.save(this.cfg.id, { lastBlock: this.blockHeightCursor }); + } + } + + 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 report(): void { + const labels = { + job: this.cfg.id, + chain: this.cfg.chain ?? "", + commitment: this.cfg.getCommitment(), + }; + const latestBlockHeight = this.latestBlockHeight ?? 0n; + const blockHeightCursor = this.blockHeightCursor ?? 0n; + const diffCursor = BigInt(latestBlockHeight) - BigInt(blockHeightCursor); + + this.statsRepo.count("job_execution", labels); + + this.statsRepo.measure("polling_cursor", latestBlockHeight, { + ...labels, + type: "max", + }); + + this.statsRepo.measure("polling_cursor", blockHeightCursor, { + ...labels, + type: "current", + }); + + this.statsRepo.measure("polling_cursor", diffCursor, { + ...labels, + type: "diff", + }); + } +} + +export type PollAlgorandMetadata = { + lastBlock: bigint; +}; + +export interface PollAlgorandConfigProps { + blockBatchSize?: number; + applicationIds: string[]; + commitment?: string; + environment: string; + fromBlock?: bigint; + interval?: number; + toBlock?: bigint; + chainId: number; + chain: string; + id?: string; +} + +export class PollAlgorandConfig { + private props: PollAlgorandConfigProps; + + constructor(props: PollAlgorandConfigProps) { + 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 id() { + return this.props.id ?? ID; + } + + public get chain() { + return this.props.chain; + } + + public get environment() { + return this.props.environment; + } + + public get chainId() { + return this.props.chainId; + } + + public get applicationIds(): string[] { + return this.props.applicationIds; + } +} + +export type GetAlgorandOpts = { + applicationIds: string[]; + chainId: number; + chain: string; +}; diff --git a/blockchain-watcher/src/domain/actions/index.ts b/blockchain-watcher/src/domain/actions/index.ts index 17746377..e8fd8989 100644 --- a/blockchain-watcher/src/domain/actions/index.ts +++ b/blockchain-watcher/src/domain/actions/index.ts @@ -12,5 +12,8 @@ export * from "./aptos/GetAptosTransactions"; export * from "./aptos/GetAptosTransactionsByEvents"; export * from "./aptos/HandleAptosTransactions"; export * from "./aptos/PollAptos"; +export * from "./algorand/PollAlgorand"; +export * from "./algorand/HandleAlgorandTransactions"; +export * from "./algorand/GetAlgorandTransactions"; export * from "./RunPollingJob"; export * from "./StartJobs"; diff --git a/blockchain-watcher/src/domain/actions/wormchain/GetWormchainLogs.ts b/blockchain-watcher/src/domain/actions/wormchain/GetWormchainLogs.ts index dc87abd2..4bb31181 100644 --- a/blockchain-watcher/src/domain/actions/wormchain/GetWormchainLogs.ts +++ b/blockchain-watcher/src/domain/actions/wormchain/GetWormchainLogs.ts @@ -28,6 +28,9 @@ export class GetWormchainLogs { ); return []; } + this.logger.info( + `[wormchain][exec] Processing blocks [fromBlock: ${fromBlock} - toBlock: ${toBlock}]` + ); for (let blockNumber = fromBlock; blockNumber <= toBlock; blockNumber++) { const wormchainLogs = await this.blockRepo.getBlockLogs( diff --git a/blockchain-watcher/src/domain/actions/wormchain/GetWormchainRedeems.ts b/blockchain-watcher/src/domain/actions/wormchain/GetWormchainRedeems.ts index 31e2de53..1560345c 100644 --- a/blockchain-watcher/src/domain/actions/wormchain/GetWormchainRedeems.ts +++ b/blockchain-watcher/src/domain/actions/wormchain/GetWormchainRedeems.ts @@ -29,6 +29,10 @@ export class GetWormchainRedeems { return []; } + this.logger.info( + `[wormchain][exec] Processing blocks [fromBlock: ${fromBlock} - toBlock: ${toBlock}]` + ); + for (let blockNumber = fromBlock; blockNumber <= toBlock; blockNumber++) { const wormchainLogs = await this.blockRepo.getBlockLogs( opts.chainId, diff --git a/blockchain-watcher/src/domain/entities/algorand.ts b/blockchain-watcher/src/domain/entities/algorand.ts new file mode 100644 index 00000000..d62baa98 --- /dev/null +++ b/blockchain-watcher/src/domain/entities/algorand.ts @@ -0,0 +1,16 @@ +export interface AlgorandTransaction { + applicationId: string; + blockNumber: number; + timestamp: number; + innerTxs?: { + applicationId?: string; + payload?: string; + sender: string; + method?: string; + logs?: string[]; + }[]; + payload: string; + method: string; + sender: string; + hash: string; +} diff --git a/blockchain-watcher/src/domain/repositories.ts b/blockchain-watcher/src/domain/repositories.ts index 1a435f2f..2ed94722 100644 --- a/blockchain-watcher/src/domain/repositories.ts +++ b/blockchain-watcher/src/domain/repositories.ts @@ -1,9 +1,12 @@ +import { IbcTransaction, WormchainBlockLogs, CosmosRedeem } from "./entities/wormchain"; import { AptosEvent, AptosTransaction } from "./entities/aptos"; import { SuiTransactionBlockReceipt } from "./entities/sui"; import { Fallible, SolanaFailure } from "./errors"; import { ConfirmedSignatureInfo } from "./entities/solana"; +import { AlgorandTransaction } from "./entities/algorand"; import { TransactionFilter } from "./actions/aptos/PollAptos"; import { RunPollingJob } from "./actions/RunPollingJob"; +import { SeiRedeem } from "./entities/sei"; import { TransactionFilter as SuiTransactionFilter, SuiEventFilter, @@ -20,8 +23,6 @@ import { EvmTag, Range, } from "./entities"; -import { IbcTransaction, WormchainBlockLogs, CosmosRedeem } from "./entities/wormchain"; -import { SeiRedeem } from "./entities/sei"; export interface EvmBlockRepository { getBlockHeight(chain: string, finality: string): Promise; @@ -97,6 +98,15 @@ export interface SeiRepository { getBlockTimestamp(blockNumber: bigint): Promise; } +export interface AlgorandRepository { + getTransactions( + applicationId: string, + fromBlock: bigint, + toBlock: bigint + ): Promise; + getBlockHeight(): Promise; +} + export interface MetadataRepository { get(id: string): Promise; save(id: string, metadata: Metadata): Promise; diff --git a/blockchain-watcher/src/infrastructure/mappers/algorand/algorandLogMessagePublishedMapper.ts b/blockchain-watcher/src/infrastructure/mappers/algorand/algorandLogMessagePublishedMapper.ts new file mode 100644 index 00000000..cb45ed8b --- /dev/null +++ b/blockchain-watcher/src/infrastructure/mappers/algorand/algorandLogMessagePublishedMapper.ts @@ -0,0 +1,54 @@ +import { LogFoundEvent, LogMessagePublished } from "../../../domain/entities"; +import { AlgorandTransaction } from "../../../domain/entities/algorand"; +import winston from "winston"; +import algosdk from "algosdk"; + +const CHAIN_ID_ALGORAND = 8; + +let logger: winston.Logger = winston.child({ module: "algorandLogMessagePublishedMapper" }); + +export const algorandLogMessagePublishedMapper = ( + transaction: AlgorandTransaction, + filters: { + applicationIds: string; + applicationAddress: string; + }[] +): LogFoundEvent | undefined => { + const innetTx = transaction.innerTxs?.find((tx) => tx.applicationId == filters[0].applicationIds); + + if (!innetTx?.method || !Array.isArray(innetTx.logs) || innetTx.logs.length === 0) { + return undefined; + } + + const method = Buffer.from(innetTx.method, "base64").toString("utf8"); + + if (method !== "publishMessage") { + return undefined; + } + + // We use the sender address from innerTxs to build the emitterChain because the sender address + // from the transaction is the bridge address (token bridge) + const emitterChain = Buffer.from(algosdk.decodeAddress(innetTx.sender).publicKey).toString("hex"); + + const sequence = Number(`0x${Buffer.from(innetTx.logs[0], "base64").toString("hex")}`); + + logger.info( + `[algorand] Source event info: [tx: ${transaction.hash}][${CHAIN_ID_ALGORAND}/${emitterChain}/${sequence}]` + ); + + return { + name: "log-message-published", + address: transaction.sender, + chainId: CHAIN_ID_ALGORAND, + txHash: transaction.hash, + blockHeight: BigInt(transaction.blockNumber), + blockTime: transaction.timestamp, + attributes: { + sender: emitterChain, + sequence: sequence, + payload: transaction.payload, + nonce: 0, // https://developer.algorand.org/docs/get-details/ethereum_to_algorand/#nonces-validity-windows-and-leases + consistencyLevel: 0, + }, + }; +}; diff --git a/blockchain-watcher/src/infrastructure/mappers/algorand/algorandRedeemedTransactionFoundMapper.ts b/blockchain-watcher/src/infrastructure/mappers/algorand/algorandRedeemedTransactionFoundMapper.ts new file mode 100644 index 00000000..f297025a --- /dev/null +++ b/blockchain-watcher/src/infrastructure/mappers/algorand/algorandRedeemedTransactionFoundMapper.ts @@ -0,0 +1,86 @@ +import { TransactionFoundEvent } from "../../../domain/entities"; +import { AlgorandTransaction } from "../../../domain/entities/algorand"; +import { CHAIN_ID_ALGORAND } from "@certusone/wormhole-sdk"; +import { findProtocol } from "../contractsMapper"; +import { parseVaa } from "@certusone/wormhole-sdk"; +import winston from "winston"; + +let logger: winston.Logger = winston.child({ module: "algorandRedeemedTransactionFoundMapper" }); + +const ALGORAND_CHAIN = "algorand"; + +export const algorandRedeemedTransactionFoundMapper = ( + transaction: AlgorandTransaction, + filters: { + applicationIds: string; + applicationAddress: string; + }[] +): TransactionFoundEvent | undefined => { + const method = Buffer.from(transaction.method, "base64").toString("utf8"); + + const applicationId = String(transaction.applicationId); + const protocol = findProtocol(ALGORAND_CHAIN, applicationId, method, transaction.hash); + + if (!protocol || protocol.type === "unknown") { + return undefined; + } + + const vaaInformation = mappedVaaInformation(transaction.payload); + + if (!vaaInformation) { + logger.warn( + `[algorand] Cannot mapper vaa information: [hash: ${transaction.hash}][protocol: ${protocol.type}/${protocol.method}]` + ); + return undefined; + } + + const filter = filters.find((filter) => filter.applicationIds === applicationId); + + const { emitterChain, emitterAddress, sequence } = vaaInformation; + + logger.info( + `[${ALGORAND_CHAIN}] Redeemed transaction info: [hash: ${transaction.hash}][VAA: ${emitterChain}/${emitterAddress}/${sequence}]` + ); + + return { + name: "transfer-redeemed", + address: filter?.applicationAddress ?? applicationId, + blockHeight: BigInt(transaction.blockNumber), + blockTime: transaction.timestamp, + chainId: CHAIN_ID_ALGORAND, + txHash: transaction.hash, + attributes: { + from: transaction.sender, + emitterChain: emitterChain, + emitterAddress: emitterAddress, + sequence: Number(sequence), + status: TxStatus.Completed, + protocol: protocol.method, + }, + }; +}; + +const mappedVaaInformation = (payload: string): VaaInformation | undefined => { + const payloadToHex = Buffer.from(payload, "base64").toString("hex"); + const buffer = Buffer.from(payloadToHex, "hex"); + const vaa = parseVaa(buffer); + + return { + emitterChain: vaa.emitterChain, + emitterAddress: vaa.emitterAddress.toString("hex").toUpperCase(), + sequence: Number(vaa.sequence), + }; +}; + +type VaaInformation = { + emitterChain: number; + emitterAddress: string; + sequence: number; + formAddress?: string; + toAddress?: string; +}; + +enum TxStatus { + Completed = "completed", + Failed = "failed", +} diff --git a/blockchain-watcher/src/infrastructure/mappers/contractsMapperConfig.json b/blockchain-watcher/src/infrastructure/mappers/contractsMapperConfig.json index edb79093..73cf9386 100644 --- a/blockchain-watcher/src/infrastructure/mappers/contractsMapperConfig.json +++ b/blockchain-watcher/src/infrastructure/mappers/contractsMapperConfig.json @@ -1778,6 +1778,21 @@ ] } ] + }, + { + "chain": "algorand", + "protocols": [ + { + "addresses": ["842126029", "86525641"], + "type": "Token Bridge", + "methods": [ + { + "methodId": "completeTransfer", + "method": "Token Bridge" + } + ] + } + ] } ] } diff --git a/blockchain-watcher/src/infrastructure/repositories/RepositoriesBuilder.ts b/blockchain-watcher/src/infrastructure/repositories/RepositoriesBuilder.ts index 50a1f750..68066f31 100644 --- a/blockchain-watcher/src/infrastructure/repositories/RepositoriesBuilder.ts +++ b/blockchain-watcher/src/infrastructure/repositories/RepositoriesBuilder.ts @@ -1,9 +1,11 @@ import { RateLimitedWormchainJsonRPCBlockRepository } from "./wormchain/RateLimitedWormchainJsonRPCBlockRepository"; +import { RateLimitedAlgorandJsonRPCBlockRepository } from "./algorand/RateLimitedAlgorandJsonRPCBlockRepository"; import { RateLimitedAptosJsonRPCBlockRepository } from "./aptos/RateLimitedAptosJsonRPCBlockRepository"; import { RateLimitedEvmJsonRPCBlockRepository } from "./evm/RateLimitedEvmJsonRPCBlockRepository"; import { RateLimitedSeiJsonRPCBlockRepository } from "./sei/RateLimitedSeiJsonRPCBlockRepository"; import { RateLimitedSuiJsonRPCBlockRepository } from "./sui/RateLimitedSuiJsonRPCBlockRepository"; import { WormchainJsonRPCBlockRepository } from "./wormchain/WormchainJsonRPCBlockRepository"; +import { AlgorandJsonRPCBlockRepository } from "./algorand/AlgorandJsonRPCBlockRepository"; import { AptosJsonRPCBlockRepository } from "./aptos/AptosJsonRPCBlockRepository"; import { SNSClient, SNSClientConfig } from "@aws-sdk/client-sns"; import { SeiJsonRPCBlockRepository } from "./sei/SeiJsonRPCBlockRepository"; @@ -11,6 +13,7 @@ import { InstrumentedHttpProvider } from "../rpc/http/InstrumentedHttpProvider"; import { Config } from "../config"; import { WormchainRepository, + AlgorandRepository, AptosRepository, JobRepository, SuiRepository, @@ -41,6 +44,7 @@ import { } from "."; const WORMCHAIN_CHAIN = "wormchain"; +const ALGORAND_CHAIN = "algorand"; const SOLANA_CHAIN = "solana"; const APTOS_CHAIN = "aptos"; const EVM_CHAIN = "evm"; @@ -96,6 +100,7 @@ export class RepositoriesBuilder { this.cfg.enabledPlatforms.forEach((chain) => { this.buildWormchainRepository(chain); + this.buildAlgorandRepository(chain); this.buildSolanaRepository(chain); this.buildAptosRepository(chain); this.buildEvmRepository(chain); @@ -119,6 +124,7 @@ export class RepositoriesBuilder { aptosRepo: this.getAptosRepository(), wormchainRepo: this.getWormchainRepository(), seiRepo: this.getSeiRepository(), + algorandRepo: this.getAlgorandRepository(), } ) ); @@ -167,6 +173,10 @@ export class RepositoriesBuilder { return this.getRepo("sei-repo"); } + public getAlgorandRepository(): AlgorandRepository { + return this.getRepo("algorand-repo"); + } + public close(): void { this.snsClient?.destroy(); } @@ -287,6 +297,22 @@ export class RepositoriesBuilder { } } + private buildAlgorandRepository(chain: string): void { + if (chain == ALGORAND_CHAIN) { + const algoIndexerRpcs = this.cfg.chains[chain].rpcs[1] as unknown as string[]; + const algoRpcs = this.cfg.chains[chain].rpcs[0] as unknown as string[]; + + const algoIndexerPools = this.createDefaultProviderPools(chain, algoIndexerRpcs); + const algoV2Pools = this.createDefaultProviderPools(chain, algoRpcs); + + const seiRepository = new RateLimitedAlgorandJsonRPCBlockRepository( + new AlgorandJsonRPCBlockRepository(algoV2Pools, algoIndexerPools) + ); + + this.repositories.set("algorand-repo", seiRepository); + } + } + private getRepo(name: string): any { const repo = this.repositories.get(name); if (!repo) @@ -321,10 +347,14 @@ export class RepositoriesBuilder { return pools; } - private createDefaultProviderPools(chain: string) { + private createDefaultProviderPools(chain: string, rpcs?: string[]) { + if (!rpcs) { + rpcs = this.cfg.chains[chain].rpcs; + } + const cfg = this.cfg.chains[chain]; const pools = providerPoolSupplier( - cfg.rpcs.map((url) => ({ url })), + rpcs.map((url) => ({ url })), (rpcCfg: RpcConfig) => this.createHttpClient(chain, rpcCfg.url), POOL_STRATEGY ); diff --git a/blockchain-watcher/src/infrastructure/repositories/StaticJobRepository.ts b/blockchain-watcher/src/infrastructure/repositories/StaticJobRepository.ts index d5a695d1..e06d965b 100644 --- a/blockchain-watcher/src/infrastructure/repositories/StaticJobRepository.ts +++ b/blockchain-watcher/src/infrastructure/repositories/StaticJobRepository.ts @@ -1,13 +1,16 @@ import { PollSei, PollSeiConfig, PollSeiConfigProps } from "../../domain/actions/sei/PollSei"; import { FileMetadataRepository, SnsEventRepository } from "./index"; import { wormchainRedeemedTransactionFoundMapper } from "../mappers/wormchain/wormchainRedeemedTransactionFoundMapper"; +import { algorandRedeemedTransactionFoundMapper } from "../mappers/algorand/algorandRedeemedTransactionFoundMapper"; import { JobDefinition, Handler, LogFoundEvent } from "../../domain/entities"; import { aptosRedeemedTransactionFoundMapper } from "../mappers/aptos/aptosRedeemedTransactionFoundMapper"; import { wormchainLogMessagePublishedMapper } from "../mappers/wormchain/wormchainLogMessagePublishedMapper"; import { seiRedeemedTransactionFoundMapper } from "../mappers/sei/seiRedeemedTransactionFoundMapper"; +import { algorandLogMessagePublishedMapper } from "../mappers/algorand/algorandLogMessagePublishedMapper"; import { suiRedeemedTransactionFoundMapper } from "../mappers/sui/suiRedeemedTransactionFoundMapper"; import { aptosLogMessagePublishedMapper } from "../mappers/aptos/aptosLogMessagePublishedMapper"; import { suiLogMessagePublishedMapper } from "../mappers/sui/suiLogMessagePublishedMapper"; +import { HandleAlgorandTransactions } from "../../domain/actions/algorand/HandleAlgorandTransactions"; import { HandleSolanaTransactions } from "../../domain/actions/solana/HandleSolanaTransactions"; import { HandleAptosTransactions } from "../../domain/actions/aptos/HandleAptosTransactions"; import { HandleWormchainRedeems } from "../../domain/actions/wormchain/HandleWormchainRedeems"; @@ -24,6 +27,7 @@ import { import { SolanaSlotRepository, WormchainRepository, + AlgorandRepository, EvmBlockRepository, MetadataRepository, AptosRepository, @@ -56,6 +60,11 @@ import { PollAptosTransactionsConfig, PollAptos, } from "../../domain/actions/aptos/PollAptos"; +import { + PollAlgorandConfigProps, + PollAlgorandConfig, + PollAlgorand, +} from "../../domain/actions/algorand/PollAlgorand"; export class StaticJobRepository implements JobRepository { private fileRepo: FileMetadataRepository; @@ -75,6 +84,7 @@ export class StaticJobRepository implements JobRepository { private aptosRepo: AptosRepository; private wormchainRepo: WormchainRepository; private seiRepo: SeiRepository; + private algorandRepo: AlgorandRepository; constructor( environment: string, @@ -90,6 +100,7 @@ export class StaticJobRepository implements JobRepository { aptosRepo: AptosRepository; wormchainRepo: WormchainRepository; seiRepo: SeiRepository; + algorandRepo: AlgorandRepository; } ) { this.fileRepo = new FileMetadataRepository(path); @@ -102,6 +113,7 @@ export class StaticJobRepository implements JobRepository { this.aptosRepo = repos.aptosRepo; this.wormchainRepo = repos.wormchainRepo; this.seiRepo = repos.seiRepo; + this.algorandRepo = repos.algorandRepo; this.environment = environment; this.dryRun = dryRun; this.fill(); @@ -207,7 +219,6 @@ export class StaticJobRepository implements JobRepository { }), jobDef.source.records ); - const pollSei = (jobDef: JobDefinition) => new PollSei( this.seiRepo, @@ -218,6 +229,16 @@ export class StaticJobRepository implements JobRepository { id: jobDef.id, }) ); + const pollAlgorand = (jobDef: JobDefinition) => + new PollAlgorand( + this.algorandRepo, + this.metadataRepo, + this.statsRepo, + new PollAlgorandConfig({ + ...(jobDef.source.config as PollAlgorandConfigProps), + id: jobDef.id, + }) + ); this.sources.set("PollEvm", pollEvm); this.sources.set("PollSolanaTransactions", pollSolanaTransactions); @@ -225,6 +246,7 @@ export class StaticJobRepository implements JobRepository { this.sources.set("PollAptos", pollAptos); this.sources.set("PollWormchain", pollWormchain); this.sources.set("PollSei", pollSei); + this.sources.set("PollAlgorand", pollAlgorand); } private loadMappers(): void { @@ -238,6 +260,11 @@ export class StaticJobRepository implements JobRepository { this.mappers.set("aptosRedeemedTransactionFoundMapper", aptosRedeemedTransactionFoundMapper); this.mappers.set("wormchainLogMessagePublishedMapper", wormchainLogMessagePublishedMapper); this.mappers.set("seiRedeemedTransactionFoundMapper", seiRedeemedTransactionFoundMapper); + this.mappers.set( + "algorandRedeemedTransactionFoundMapper", + algorandRedeemedTransactionFoundMapper + ); + this.mappers.set("algorandLogMessagePublishedMapper", algorandLogMessagePublishedMapper); this.mappers.set( "wormchainRedeemedTransactionFoundMapper", wormchainRedeemedTransactionFoundMapper @@ -331,6 +358,16 @@ export class StaticJobRepository implements JobRepository { return instance.handle.bind(instance); }; + const handleAlgorandTransactions = async (config: any, target: string, mapper: any) => { + const instance = new HandleAlgorandTransactions( + config, + mapper, + await this.getTarget(target), + this.statsRepo + ); + return instance.handle.bind(instance); + }; + this.handlers.set("HandleEvmLogs", handleEvmLogs); this.handlers.set("HandleEvmTransactions", handleEvmTransactions); this.handlers.set("HandleSolanaTransactions", handleSolanaTx); @@ -339,6 +376,7 @@ export class StaticJobRepository implements JobRepository { this.handlers.set("HandleWormchainLogs", handleWormchainLogs); this.handlers.set("HandleWormchainRedeems", handleWormchainRedeems); this.handlers.set("HandleSeiRedeems", handleSeiRedeems); + this.handlers.set("HandleAlgorandTransactions", handleAlgorandTransactions); } private async getTarget(target: string): Promise<(items: any[]) => Promise> { diff --git a/blockchain-watcher/src/infrastructure/repositories/algorand/AlgorandJsonRPCBlockRepository.ts b/blockchain-watcher/src/infrastructure/repositories/algorand/AlgorandJsonRPCBlockRepository.ts new file mode 100644 index 00000000..b9fc77ae --- /dev/null +++ b/blockchain-watcher/src/infrastructure/repositories/algorand/AlgorandJsonRPCBlockRepository.ts @@ -0,0 +1,114 @@ +import { InstrumentedHttpProvider } from "../../rpc/http/InstrumentedHttpProvider"; +import { AlgorandTransaction } from "../../../domain/entities/algorand"; +import { AlgorandRepository } from "../../../domain/repositories"; +import { ProviderPool } from "@xlabs/rpc-pool"; +import winston from "winston"; + +type ProviderPoolMap = ProviderPool; + +let TRANSACTIONS_ENDPOINT = "/v2/transactions"; +let STATUS_ENDPOINT = "/v2/status"; + +export class AlgorandJsonRPCBlockRepository implements AlgorandRepository { + private readonly logger: winston.Logger; + protected algoV2Pools: ProviderPoolMap; + protected algoIndexerPools: ProviderPoolMap; + + constructor( + algoV2Pools: ProviderPool, + algoIndexerPools: ProviderPool + ) { + this.logger = winston.child({ module: "AlgorandJsonRPCBlockRepository" }); + this.algoV2Pools = algoV2Pools; + this.algoIndexerPools = algoIndexerPools; + } + + async getBlockHeight(): Promise { + let result: ResultStatus; + result = await this.algoV2Pools.get().get(STATUS_ENDPOINT); + return BigInt(result["last-round"]); + } + + async getTransactions( + applicationId: string, + fromBlock: bigint, + toBlock: bigint + ): Promise { + try { + let result: ResultTransactions; + result = await this.algoIndexerPools + .get() + .get( + `${TRANSACTIONS_ENDPOINT}?application-id=${Number( + applicationId + )}&min-round=${fromBlock}&max-round=${toBlock}` + ); + + if (!result.transactions || result.transactions.length === 0) { + return []; + } + + return result.transactions.map((tx) => { + return { + payload: tx["application-transaction"]?.["application-args"][1], + method: tx["application-transaction"]?.["application-args"][0], + applicationId: tx["application-transaction"]["application-id"], + blockNumber: tx["confirmed-round"], + timestamp: tx["round-time"], + innerTxs: tx["inner-txns"]?.map((innerTx) => { + // build inner transactions + return { + applicationId: innerTx["application-transaction"]?.["application-id"], + payload: innerTx["application-transaction"]?.["application-args"][1], + method: innerTx["application-transaction"]?.["application-args"][0], + sender: innerTx.sender, + logs: innerTx.logs, + }; + }), + sender: tx.sender, + hash: tx.id, + }; + }); + } catch (e) { + this.handleError( + `Application id: ${applicationId} and range params: ${fromBlock} - ${toBlock}, error: ${e}`, + "getTransactions" + ); + throw e; + } + } + + private handleError(e: any, method: string) { + this.logger.error(`[algorand] Error calling ${method}: ${e.message ?? e}`); + } +} + +type ResultStatus = { + "last-round": number; +}; + +type ResultTransactions = { + "current-round": number; + "next-token": string; + transactions: { + "tx-type": string; + "application-transaction": { + "application-id": string; + "application-args": string[]; + }; + id: string; + sender: string; + "confirmed-round": number; + "application-args": string[]; + "round-time": number; + logs: string[]; + "inner-txns": { + sender: string; + logs: string[]; + "application-transaction": { + "application-id": string; + "application-args": string[]; + }; + }[]; + }[]; +}; diff --git a/blockchain-watcher/src/infrastructure/repositories/algorand/RateLimitedAlgorandJsonRPCBlockRepository.ts b/blockchain-watcher/src/infrastructure/repositories/algorand/RateLimitedAlgorandJsonRPCBlockRepository.ts new file mode 100644 index 00000000..60c47983 --- /dev/null +++ b/blockchain-watcher/src/infrastructure/repositories/algorand/RateLimitedAlgorandJsonRPCBlockRepository.ts @@ -0,0 +1,29 @@ +import { RateLimitedRPCRepository } from "../RateLimitedRPCRepository"; +import { AlgorandTransaction } from "../../../domain/entities/algorand"; +import { AlgorandRepository } from "../../../domain/repositories"; +import { Options } from "../common/rateLimitedOptions"; +import winston from "winston"; + +export class RateLimitedAlgorandJsonRPCBlockRepository + extends RateLimitedRPCRepository + implements AlgorandRepository +{ + constructor(delegate: AlgorandRepository, opts: Options = { period: 10_000, limit: 1000 }) { + super(delegate, opts); + this.logger = winston.child({ module: "RateLimitedAlgorandJsonRPCBlockRepository" }); + } + + getTransactions( + applicationId: string, + fromBlock: bigint, + toBlock: bigint + ): Promise { + return this.breaker + .fn(() => this.delegate.getTransactions(applicationId, fromBlock, toBlock)) + .execute(); + } + + getBlockHeight(): Promise { + return this.breaker.fn(() => this.delegate.getBlockHeight()).execute(); + } +} diff --git a/blockchain-watcher/src/infrastructure/repositories/index.ts b/blockchain-watcher/src/infrastructure/repositories/index.ts index 4831c487..80650d07 100644 --- a/blockchain-watcher/src/infrastructure/repositories/index.ts +++ b/blockchain-watcher/src/infrastructure/repositories/index.ts @@ -20,3 +20,4 @@ export * from "./solana/Web3SolanaSlotRepository"; export * from "./solana/RateLimitedSolanaSlotRepository"; export * from "./sui/SuiJsonRPCBlockRepository"; export * from "./wormchain/WormchainJsonRPCBlockRepository"; +export * from "./algorand/AlgorandJsonRPCBlockRepository"; diff --git a/blockchain-watcher/src/infrastructure/repositories/wormchain/WormchainJsonRPCBlockRepository.ts b/blockchain-watcher/src/infrastructure/repositories/wormchain/WormchainJsonRPCBlockRepository.ts index 7c942378..84ab43b2 100644 --- a/blockchain-watcher/src/infrastructure/repositories/wormchain/WormchainJsonRPCBlockRepository.ts +++ b/blockchain-watcher/src/infrastructure/repositories/wormchain/WormchainJsonRPCBlockRepository.ts @@ -17,7 +17,7 @@ let TRANSACTION_ENDPOINT = "/tx"; let BLOCK_ENDPOINT = "/block"; const GROW_SLEEP_TIME = 350; -const MAX_ATTEMPTS = 10; +const MAX_ATTEMPTS = 20; type ProviderPoolMap = ProviderPool; @@ -201,10 +201,12 @@ export class WormchainJsonRPCBlockRepository implements WormchainRepository { } } - if (!resultTransactionSearch || attempts > MAX_ATTEMPTS) { - throw new Error( - `[getRedeems] The transaction \n${query}\n with chainId: ${ibcTransaction.targetChain} never ended` - ); + if ( + !resultTransactionSearch || + !resultTransactionSearch.result || + !resultTransactionSearch.result.txs + ) { + return []; } return resultTransactionSearch.result.txs.map((tx) => { diff --git a/blockchain-watcher/test/domain/actions/algorand/GetAlgorandTransactions.test.ts b/blockchain-watcher/test/domain/actions/algorand/GetAlgorandTransactions.test.ts new file mode 100644 index 00000000..f2fa5ecf --- /dev/null +++ b/blockchain-watcher/test/domain/actions/algorand/GetAlgorandTransactions.test.ts @@ -0,0 +1,113 @@ +import { afterEach, describe, it, expect, jest } from "@jest/globals"; +import { thenWaitForAssertion } from "../../../wait-assertion"; +import { AlgorandTransaction } from "../../../../src/domain/entities/algorand"; +import { + PollAlgorandMetadata, + PollAlgorandConfig, + PollAlgorand, +} from "../../../../src/domain/actions"; +import { + AlgorandRepository, + MetadataRepository, + StatRepository, +} from "../../../../src/domain/repositories"; + +let getBlockHeightSpy: jest.SpiedFunction; +let getTransactionsSpy: jest.SpiedFunction; +let metadataSaveSpy: jest.SpiedFunction["save"]>; + +let handlerSpy: jest.SpiedFunction<(txs: AlgorandTransaction[]) => Promise>; + +let metadataRepo: MetadataRepository; +let algorandRepo: AlgorandRepository; +let statsRepo: StatRepository; + +let handlers = { + working: (txs: AlgorandTransaction[]) => Promise.resolve(), + failing: (txs: AlgorandTransaction[]) => Promise.reject(), +}; + +let pollAlgorand: PollAlgorand; + +let cfg = new PollAlgorandConfig({ + chain: "algorand", + applicationIds: ["842125965"], + chainId: 8, + environment: "testnet", +}); + +describe("GetAlgorandTransactions", () => { + afterEach(async () => { + await pollAlgorand.stop(); + }); + + it("should be use from and batch size cfg, and process tx because is a wormhole redeem", async () => { + // Given + const txs = [ + { + payload: + "AQAAAAQNAMvOqeysTxjYZtgYEOHVj77EPk0eVW0t1/xm5erC78GEJKVo/TJWgLpH+rPrv39ujSllqUNBG+4LCj1vVEO6HUYAASWlSS/cAIzz6mm+ZqpputkD25jFwEKycUGqjj39nP8yQzvR4PEPC75tIIgDLoj0HSzybbH1N7hFykOVE31Tg1cBAliHbjlP/OhEnOspo9WryTrmRmlQNkVGZ+5TRHkwy5WtVVBj5W0BE0M/hnkVAlny37QN99DRTl6cIs0IM4Bpw+QBA3gXwF6aqPF8aVp3vieQNweFLukFFqGXSdxV6KYOmfojc8/P7wLrD+4P9I2Y1XNfi9IoyihI+anU9IFDPpUkmuQABMJXlTMcms2Yit3JBLVjbiImzbtzPIMCRMLXG082cJVtevhCFtxR94pkOF75THhZy3vZ7v2oCQJXp6fssKGP2jgABihy7P7j+ovzB0/i+emkEXZMAoJcrviPbx11A+hAS36ubl+pk1sO1K4d7FM4HjP/f0WosNoCBXqfyD3jCrlZO/kACBZCCH+WTb6bYLPJC+nzzjvHA73QDH4lDkA4KjiJq3Z8MXRqRn6vFR7vjszj9NJCRw9xD6xscpMW0sNXcMFDDqsACYzV/6FXG/XZ7Kd4FYfoQ2ZHLSeRxRF4Xrt5YvsyhxOTHdj0S2b7eb7B41CxDUKzhYvWGf3V4WhYfvRgMFou944BDC8szu1vORgXN/EWBAJwBRmjOZXpHNH7GRIFupubGSQPIDtuom2/DHo2F77vxKC7HXVMVHbV7C3oOgB8w2lXfSABD9wQtyp+4hZAHFNOclrkTNgO/BRFaxXbTwKzKMv1ax3+O21cf3eSlYfdlwTz2DJrts/M/72v18EZriEFGPnFtHUAEOa6k9hnDJZys1ZJ4cSsd77DIFSRTc6GxbaWBTYXfMOjTMSArzdUl5XQKFTRIrDMHRY+sM4dz5dAWc7IM1rYcIoAES8aaahKohCHoR/d//Z0ZT1P4+LcgrF4rABIxki6mDbSaz+86v+derrWYHErb8+s5OpH5qwGbrftiXsDlebiyN8AEg6Rp7M4ooj+qsdqgzt4DUmLgUU2eqBVF1nWuEt0lJgNWAQ2ZPgqp2R+NcsxXEys/lIpSaYgxT3pGLbFWRdpLWgAZnqa+gADj5AABgAAAAAAAAAAAAAAAA4ILwb/ZX2UMQy4zosNmgRUHYBSAAAAAAADXAIBAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACw7dfAAAAAAAAAAAAAAAAuX7574c0xxkE2AAvi2vGbdnEim4ABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABK8EJrAAgAAAAAAAAAAAAAAABTZwZsNNSHRYgR77UGp6Kv2tuOi3dvcm1ob2xlRGVwb3NpdAAAAAAAAAAAAAAAAAurcc/Wfp8DdXO0rccmziZuKxssAAAAAAAAAAAAAAAASvBCawAXAAAAAAAAAAAAAAAAC6txz9Z+nwN1c7StxybOJm4rGyw=", + applicationId: "842126029", + blockNumber: 40085294, + timestamp: 1719311110, + innerTxs: [ + { + sender: "PG56DVKH6F3RXJASLIR4AXIXDYTFK2DQWIEOLDY22HEX5IPRE47HNFINKY", + }, + ], + sender: "C3EXCPEEMYTIJ2EYUMEMLBDHIJ7J2KAHGKFHWD4GQX5MP7PYZO7O2C6YZE", + hash: "SERG7537SOJADJO5LC2J5SC6DD2VONL76B64YB5PDID2T3FONK5Q", + }, + ]; + + givenAlgorandRepository(402222n, txs); + givenMetadataRepository(); + givenStatsRepository(); + givenPollAlgorandTxs(); + + // Whem + await whenPollAlgorandStarts(); + + // Then + await thenWaitForAssertion( + () => expect(getBlockHeightSpy).toHaveReturnedTimes(1), + () => expect(getTransactionsSpy).toBeCalledWith("842125965", 402222n, 402222n) + ); + }); +}); + +const givenAlgorandRepository = (height?: bigint, txs: any = []) => { + algorandRepo = { + getBlockHeight: () => Promise.resolve(height), + getTransactions: () => Promise.resolve(txs), + }; + + getBlockHeightSpy = jest.spyOn(algorandRepo, "getBlockHeight"); + getTransactionsSpy = jest.spyOn(algorandRepo, "getTransactions"); + handlerSpy = jest.spyOn(handlers, "working"); +}; + +const givenMetadataRepository = (data?: PollAlgorandMetadata) => { + metadataRepo = { + get: () => Promise.resolve(data), + save: () => Promise.resolve(), + }; + metadataSaveSpy = jest.spyOn(metadataRepo, "save"); +}; + +const givenStatsRepository = () => { + statsRepo = { + count: () => {}, + measure: () => {}, + report: () => Promise.resolve(""), + }; +}; + +const givenPollAlgorandTxs = (from?: bigint) => { + cfg.setFromBlock(from); + pollAlgorand = new PollAlgorand(algorandRepo, metadataRepo, statsRepo, cfg); +}; + +const whenPollAlgorandStarts = async () => { + pollAlgorand.run([handlers.working]); +}; diff --git a/blockchain-watcher/test/domain/actions/algorand/HandleAlgorandTransactions.test.ts b/blockchain-watcher/test/domain/actions/algorand/HandleAlgorandTransactions.test.ts new file mode 100644 index 00000000..605735a3 --- /dev/null +++ b/blockchain-watcher/test/domain/actions/algorand/HandleAlgorandTransactions.test.ts @@ -0,0 +1,113 @@ +import { afterEach, describe, it, expect, jest } from "@jest/globals"; +import { AlgorandTransaction } from "../../../../src/domain/entities/algorand"; +import { StatRepository } from "../../../../src/domain/repositories"; +import { LogFoundEvent } from "../../../../src/domain/entities"; +import { + HandleAlgorandTransactionsOptions, + HandleAlgorandTransactions, +} from "../../../../src/domain/actions/algorand/HandleAlgorandTransactions"; + +let targetRepoSpy: jest.SpiedFunction<(typeof targetRepo)["save"]>; +let statsRepo: StatRepository; + +let handleAlgorandTransactions: HandleAlgorandTransactions; +let txs: AlgorandTransaction[]; +let cfg: HandleAlgorandTransactionsOptions; + +describe("HandleAlgorandTransactions", () => { + afterEach(async () => {}); + + it("should be able to map source events tx", async () => { + // Given + givenConfig(); + givenStatsRepository(); + givenHandleEvmLogs(); + + // When + const result = await handleAlgorandTransactions.handle(txs); + + // Then + expect(result).toHaveLength(1); + expect(result[0].name).toBe("log-message-published"); + expect(result[0].chainId).toBe(8); + expect(result[0].txHash).toBe("SQA7S37MCLGHQRMFZHRNUNUFJ6PJKRZN5RO52NMEWJU5B365SINQ"); + expect(result[0].address).toBe("MG3DIJNS3JTVKUAQGFV5BQTDAK26OUM3SRXSLIFWVUS67V54VPKDUJQTOQ"); + }); +}); + +const mapper = (tx: AlgorandTransaction) => { + return { + name: "log-message-published", + address: "MG3DIJNS3JTVKUAQGFV5BQTDAK26OUM3SRXSLIFWVUS67V54VPKDUJQTOQ", + chainId: 8, + txHash: "SQA7S37MCLGHQRMFZHRNUNUFJ6PJKRZN5RO52NMEWJU5B365SINQ", + blockHeight: 40085318n, + blockTime: 1719311180, + attributes: { + sender: "67e93fa6c8ac5c819990aa7340c0c16b508abb1178be9b30d024b8ac25193d45", + sequence: 10576, + payload: "AAAAADcXNho=", + nonce: 0, + consistencyLevel: 0, + protocol: "Token Bridge", + }, + }; +}; + +const targetRepo = { + save: async (events: LogFoundEvent>[]) => { + Promise.resolve(); + }, + failingSave: async (events: LogFoundEvent>[]) => { + Promise.reject(); + }, +}; + +const givenHandleEvmLogs = (targetFn: "save" | "failingSave" = "save") => { + targetRepoSpy = jest.spyOn(targetRepo, targetFn); + handleAlgorandTransactions = new HandleAlgorandTransactions( + cfg, + mapper, + () => Promise.resolve(), + statsRepo + ); +}; + +const givenConfig = () => { + cfg = { + id: "poll-log-message-published-algorand", + metricName: "process_source_event", + filter: [ + { + applicationIds: "842125965", + applicationAddress: "J476J725L4JTOI2YU6DAI4E23LYUECLZR7RCYZ3LK6QFHX4M54ZI53SGXQ", + }, + ], + }; +}; + +const givenStatsRepository = () => { + statsRepo = { + count: () => {}, + measure: () => {}, + report: () => Promise.resolve(""), + }; +}; + +txs = [ + { + payload: "AAAAADcXNho=", + applicationId: "842126029", + blockNumber: 40085318, + timestamp: 1719311180, + method: "Y29tcGxldGVUcmFuc2Zlcg==", + innerTxs: [ + { + logs: ["AAAAAAAAKVA="], + sender: "M7UT7JWIVROIDGMQVJZUBQGBNNIIVOYRPC7JWMGQES4KYJIZHVCRZEGFRQ", + }, + ], + sender: "MG3DIJNS3JTVKUAQGFV5BQTDAK26OUM3SRXSLIFWVUS67V54VPKDUJQTOQ", + hash: "SQA7S37MCLGHQRMFZHRNUNUFJ6PJKRZN5RO52NMEWJU5B365SINQ", + }, +]; diff --git a/blockchain-watcher/test/domain/actions/algorand/PollAlgorand.test.ts b/blockchain-watcher/test/domain/actions/algorand/PollAlgorand.test.ts new file mode 100644 index 00000000..7180deba --- /dev/null +++ b/blockchain-watcher/test/domain/actions/algorand/PollAlgorand.test.ts @@ -0,0 +1,110 @@ +import { afterEach, describe, it, expect, jest } from "@jest/globals"; +import { thenWaitForAssertion } from "../../../wait-assertion"; +import { AlgorandTransaction } from "../../../../src/domain/entities/algorand"; +import { + PollAlgorandMetadata, + PollAlgorandConfig, + PollAlgorand, +} from "../../../../src/domain/actions"; +import { + AlgorandRepository, + MetadataRepository, + StatRepository, +} from "../../../../src/domain/repositories"; + +let cfg = new PollAlgorandConfig({ + chain: "algorand", + applicationIds: ["842125965"], + chainId: 8, + environment: "testnet", +}); + +let getBlockHeightSpy: jest.SpiedFunction; +let getTransactionsSpy: jest.SpiedFunction; +let handlerSpy: jest.SpiedFunction<(txs: AlgorandTransaction[]) => Promise>; +let metadataSaveSpy: jest.SpiedFunction["save"]>; + +let metadataRepo: MetadataRepository; +let algorandRepo: AlgorandRepository; +let statsRepo: StatRepository; + +let handlers = { + working: (txs: AlgorandTransaction[]) => Promise.resolve(), + failing: (txs: AlgorandTransaction[]) => Promise.reject(), +}; + +let pollAlgorand: PollAlgorand; + +describe("PollAlgorand", () => { + afterEach(async () => { + await pollAlgorand.stop(); + }); + + it("should be able to read logs from latest block when no fromBlock is configured", async () => { + const currentHeight = 10n; + + const txs = [ + { + payload: + "AQAAAAQNAMvOqeysTxjYZtgYEOHVj77EPk0eVW0t1/xm5erC78GEJKVo/TJWgLpH+rPrv39ujSllqUNBG+4LCj1vVEO6HUYAASWlSS/cAIzz6mm+ZqpputkD25jFwEKycUGqjj39nP8yQzvR4PEPC75tIIgDLoj0HSzybbH1N7hFykOVE31Tg1cBAliHbjlP/OhEnOspo9WryTrmRmlQNkVGZ+5TRHkwy5WtVVBj5W0BE0M/hnkVAlny37QN99DRTl6cIs0IM4Bpw+QBA3gXwF6aqPF8aVp3vieQNweFLukFFqGXSdxV6KYOmfojc8/P7wLrD+4P9I2Y1XNfi9IoyihI+anU9IFDPpUkmuQABMJXlTMcms2Yit3JBLVjbiImzbtzPIMCRMLXG082cJVtevhCFtxR94pkOF75THhZy3vZ7v2oCQJXp6fssKGP2jgABihy7P7j+ovzB0/i+emkEXZMAoJcrviPbx11A+hAS36ubl+pk1sO1K4d7FM4HjP/f0WosNoCBXqfyD3jCrlZO/kACBZCCH+WTb6bYLPJC+nzzjvHA73QDH4lDkA4KjiJq3Z8MXRqRn6vFR7vjszj9NJCRw9xD6xscpMW0sNXcMFDDqsACYzV/6FXG/XZ7Kd4FYfoQ2ZHLSeRxRF4Xrt5YvsyhxOTHdj0S2b7eb7B41CxDUKzhYvWGf3V4WhYfvRgMFou944BDC8szu1vORgXN/EWBAJwBRmjOZXpHNH7GRIFupubGSQPIDtuom2/DHo2F77vxKC7HXVMVHbV7C3oOgB8w2lXfSABD9wQtyp+4hZAHFNOclrkTNgO/BRFaxXbTwKzKMv1ax3+O21cf3eSlYfdlwTz2DJrts/M/72v18EZriEFGPnFtHUAEOa6k9hnDJZys1ZJ4cSsd77DIFSRTc6GxbaWBTYXfMOjTMSArzdUl5XQKFTRIrDMHRY+sM4dz5dAWc7IM1rYcIoAES8aaahKohCHoR/d//Z0ZT1P4+LcgrF4rABIxki6mDbSaz+86v+derrWYHErb8+s5OpH5qwGbrftiXsDlebiyN8AEg6Rp7M4ooj+qsdqgzt4DUmLgUU2eqBVF1nWuEt0lJgNWAQ2ZPgqp2R+NcsxXEys/lIpSaYgxT3pGLbFWRdpLWgAZnqa+gADj5AABgAAAAAAAAAAAAAAAA4ILwb/ZX2UMQy4zosNmgRUHYBSAAAAAAADXAIBAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACw7dfAAAAAAAAAAAAAAAAuX7574c0xxkE2AAvi2vGbdnEim4ABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABK8EJrAAgAAAAAAAAAAAAAAABTZwZsNNSHRYgR77UGp6Kv2tuOi3dvcm1ob2xlRGVwb3NpdAAAAAAAAAAAAAAAAAurcc/Wfp8DdXO0rccmziZuKxssAAAAAAAAAAAAAAAASvBCawAXAAAAAAAAAAAAAAAAC6txz9Z+nwN1c7StxybOJm4rGyw=", + applicationId: "842126029", + blockNumber: 40085294, + timestamp: 1719311110, + innerTxs: [ + { + sender: "PG56DVKH6F3RXJASLIR4AXIXDYTFK2DQWIEOLDY22HEX5IPRE47HNFINKY", + }, + ], + sender: "C3EXCPEEMYTIJ2EYUMEMLBDHIJ7J2KAHGKFHWD4GQX5MP7PYZO7O2C6YZE", + hash: "SERG7537SOJADJO5LC2J5SC6DD2VONL76B64YB5PDID2T3FONK5Q", + }, + ]; + givenAlgorandRepository(currentHeight, txs); + givenMetadataRepository(); + givenStatsRepository(); + givenPollAlgorandTxs(); + + await whenpollAlgorandLogsStarts(); + + await thenWaitForAssertion( + () => expect(getBlockHeightSpy).toHaveReturnedTimes(1), + () => expect(getTransactionsSpy).toHaveBeenCalledWith("842125965", 10n, 10n) + ); + }); +}); + +const givenAlgorandRepository = (height?: bigint, txs: any = []) => { + algorandRepo = { + getBlockHeight: () => Promise.resolve(height), + getTransactions: () => Promise.resolve(txs), + }; + + getBlockHeightSpy = jest.spyOn(algorandRepo, "getBlockHeight"); + getTransactionsSpy = jest.spyOn(algorandRepo, "getTransactions"); + handlerSpy = jest.spyOn(handlers, "working"); +}; + +const givenMetadataRepository = (data?: PollAlgorandMetadata) => { + metadataRepo = { + get: () => Promise.resolve(data), + save: () => Promise.resolve(), + }; + metadataSaveSpy = jest.spyOn(metadataRepo, "save"); +}; + +const givenStatsRepository = () => { + statsRepo = { + count: () => {}, + measure: () => {}, + report: () => Promise.resolve(""), + }; +}; + +const givenPollAlgorandTxs = (from?: bigint) => { + cfg.setFromBlock(from); + pollAlgorand = new PollAlgorand(algorandRepo, metadataRepo, statsRepo, cfg); +}; + +const whenpollAlgorandLogsStarts = async () => { + pollAlgorand.run([handlers.working]); +}; diff --git a/blockchain-watcher/test/infrastructure/mappers/algorand/algorandLogMessagePublishedMapper.test.ts b/blockchain-watcher/test/infrastructure/mappers/algorand/algorandLogMessagePublishedMapper.test.ts new file mode 100644 index 00000000..c47e2e50 --- /dev/null +++ b/blockchain-watcher/test/infrastructure/mappers/algorand/algorandLogMessagePublishedMapper.test.ts @@ -0,0 +1,51 @@ +import { algorandLogMessagePublishedMapper } from "../../../../src/infrastructure/mappers/algorand/algorandLogMessagePublishedMapper"; +import { describe, it, expect } from "@jest/globals"; + +describe("algorandLogMessagePublishedMapper", () => { + // 8/67e93fa6c8ac5c819990aa7340c0c16b508abb1178be9b30d024b8ac25193d45/10578 + it("should be able to map log to algorandLogMessagePublishedMapper", async () => { + // When + const result = algorandLogMessagePublishedMapper(tx, filters); + + if (result) { + // Then + expect(result.name).toBe("log-message-published"); + expect(result.chainId).toBe(8); + expect(result.txHash).toBe("WNXBBFRO2ZAWHPAC5RQOU2U3K7ZV5LWIY6LIAVYSRO2QGUDJOE6A"); + expect(result.address).toBe("BM26KC3NHYQ7BCDWVMP2OM6AWEZZ6ZGYQWKAQFC7XECOUBLP44VOYNBQTA"); + expect(result.attributes.consistencyLevel).toBe(0); + expect(result.attributes.nonce).toBe(0); + expect(result.attributes.payload).toBe("AAAAADwK+tc="); + expect(result.attributes.sender).toBe( + "67e93fa6c8ac5c819990aa7340c0c16b508abb1178be9b30d024b8ac25193d45" + ); + expect(result.attributes.sequence).toBe(10578); + } + }); +}); + +const tx = { + payload: "AAAAADwK+tc=", + method: "c2VuZFRyYW5zZmVy", + applicationId: "842126029", + blockNumber: 40095152, + timestamp: 1719339547, + innerTxs: [ + { + applicationId: "842125965", + payload: + "AwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF5FHqAAAAAAAAAAAAAAAAuX7574c0xxkE2AAvi2vGbdnEim4ABgAAAAAAAAAAAAAAAFNnBmw01IdFiBHvtQanoq/a246LAAYLNeULbT4h8Ih2qx+nM8CxM59k2IWUCBRfuQTqBW/nKmNjdHBXaXRoZHJhdwAAAAAAAAAAAAAAAB2P8AKWlWUx/XZJgCHETuO3hZY1ABcAAAAASvBCaw==", + method: "cHVibGlzaE1lc3NhZ2U=", + sender: "M7UT7JWIVROIDGMQVJZUBQGBNNIIVOYRPC7JWMGQES4KYJIZHVCRZEGFRQ", + logs: ["AAAAAAAAKVI="], + }, + ], + sender: "BM26KC3NHYQ7BCDWVMP2OM6AWEZZ6ZGYQWKAQFC7XECOUBLP44VOYNBQTA", + hash: "WNXBBFRO2ZAWHPAC5RQOU2U3K7ZV5LWIY6LIAVYSRO2QGUDJOE6A", +}; +const filters = [ + { + applicationIds: "842125965", + applicationAddress: "J476J725L4JTOI2YU6DAI4E23LYUECLZR7RCYZ3LK6QFHX4M54ZI53SGXQ", + }, +]; diff --git a/blockchain-watcher/test/infrastructure/mappers/algorand/algorandRedeemedTransactionFoundMapper.test.ts b/blockchain-watcher/test/infrastructure/mappers/algorand/algorandRedeemedTransactionFoundMapper.test.ts new file mode 100644 index 00000000..fb15d124 --- /dev/null +++ b/blockchain-watcher/test/infrastructure/mappers/algorand/algorandRedeemedTransactionFoundMapper.test.ts @@ -0,0 +1,50 @@ +import { algorandRedeemedTransactionFoundMapper } from "../../../../src/infrastructure/mappers/algorand/algorandRedeemedTransactionFoundMapper"; +import { describe, it, expect } from "@jest/globals"; + +describe("algorandRedeemedTransactionFoundMapper", () => { + // 6/0000000000000000000000000e082f06ff657d94310cb8ce8b0d9a04541d8052/220162 + it("should be able to map log to algorandRedeemedTransactionFoundMapper", async () => { + // When + const result = algorandRedeemedTransactionFoundMapper(tx, filters); + + if (result) { + // Then + expect(result.name).toBe("transfer-redeemed"); + expect(result.chainId).toBe(8); + expect(result.txHash).toBe("SERG7537SOJADJO5LC2J5SC6DD2VONL76B64YB5PDID2T3FONK5Q"); + expect(result.address).toBe("M7UT7JWIVROIDGMQVJZUBQGBNNIIVOYRPC7JWMGQES4KYJIZHVCRZEGFRQ"); + expect(result.attributes.from).toBe( + "C3EXCPEEMYTIJ2EYUMEMLBDHIJ7J2KAHGKFHWD4GQX5MP7PYZO7O2C6YZE" + ); + expect(result.attributes.emitterChain).toBe(6); + expect(result.attributes.emitterAddress).toBe( + "0000000000000000000000000E082F06FF657D94310CB8CE8B0D9A04541D8052" + ); + expect(result.attributes.sequence).toBe(220162); + expect(result.attributes.status).toBe("completed"); + expect(result.attributes.protocol).toBe("Token Bridge"); + } + }); +}); + +const tx = { + payload: + "AQAAAAQNAMvOqeysTxjYZtgYEOHVj77EPk0eVW0t1/xm5erC78GEJKVo/TJWgLpH+rPrv39ujSllqUNBG+4LCj1vVEO6HUYAASWlSS/cAIzz6mm+ZqpputkD25jFwEKycUGqjj39nP8yQzvR4PEPC75tIIgDLoj0HSzybbH1N7hFykOVE31Tg1cBAliHbjlP/OhEnOspo9WryTrmRmlQNkVGZ+5TRHkwy5WtVVBj5W0BE0M/hnkVAlny37QN99DRTl6cIs0IM4Bpw+QBA3gXwF6aqPF8aVp3vieQNweFLukFFqGXSdxV6KYOmfojc8/P7wLrD+4P9I2Y1XNfi9IoyihI+anU9IFDPpUkmuQABMJXlTMcms2Yit3JBLVjbiImzbtzPIMCRMLXG082cJVtevhCFtxR94pkOF75THhZy3vZ7v2oCQJXp6fssKGP2jgABihy7P7j+ovzB0/i+emkEXZMAoJcrviPbx11A+hAS36ubl+pk1sO1K4d7FM4HjP/f0WosNoCBXqfyD3jCrlZO/kACBZCCH+WTb6bYLPJC+nzzjvHA73QDH4lDkA4KjiJq3Z8MXRqRn6vFR7vjszj9NJCRw9xD6xscpMW0sNXcMFDDqsACYzV/6FXG/XZ7Kd4FYfoQ2ZHLSeRxRF4Xrt5YvsyhxOTHdj0S2b7eb7B41CxDUKzhYvWGf3V4WhYfvRgMFou944BDC8szu1vORgXN/EWBAJwBRmjOZXpHNH7GRIFupubGSQPIDtuom2/DHo2F77vxKC7HXVMVHbV7C3oOgB8w2lXfSABD9wQtyp+4hZAHFNOclrkTNgO/BRFaxXbTwKzKMv1ax3+O21cf3eSlYfdlwTz2DJrts/M/72v18EZriEFGPnFtHUAEOa6k9hnDJZys1ZJ4cSsd77DIFSRTc6GxbaWBTYXfMOjTMSArzdUl5XQKFTRIrDMHRY+sM4dz5dAWc7IM1rYcIoAES8aaahKohCHoR/d//Z0ZT1P4+LcgrF4rABIxki6mDbSaz+86v+derrWYHErb8+s5OpH5qwGbrftiXsDlebiyN8AEg6Rp7M4ooj+qsdqgzt4DUmLgUU2eqBVF1nWuEt0lJgNWAQ2ZPgqp2R+NcsxXEys/lIpSaYgxT3pGLbFWRdpLWgAZnqa+gADj5AABgAAAAAAAAAAAAAAAA4ILwb/ZX2UMQy4zosNmgRUHYBSAAAAAAADXAIBAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACw7dfAAAAAAAAAAAAAAAAuX7574c0xxkE2AAvi2vGbdnEim4ABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABK8EJrAAgAAAAAAAAAAAAAAABTZwZsNNSHRYgR77UGp6Kv2tuOi3dvcm1ob2xlRGVwb3NpdAAAAAAAAAAAAAAAAAurcc/Wfp8DdXO0rccmziZuKxssAAAAAAAAAAAAAAAASvBCawAXAAAAAAAAAAAAAAAAC6txz9Z+nwN1c7StxybOJm4rGyw=", + applicationId: "842126029", + blockNumber: 40085294, + timestamp: 1719311110, + method: "Y29tcGxldGVUcmFuc2Zlcg==", + innerTxs: [ + { + sender: "PG56DVKH6F3RXJASLIR4AXIXDYTFK2DQWIEOLDY22HEX5IPRE47HNFINKY", + }, + ], + sender: "C3EXCPEEMYTIJ2EYUMEMLBDHIJ7J2KAHGKFHWD4GQX5MP7PYZO7O2C6YZE", + hash: "SERG7537SOJADJO5LC2J5SC6DD2VONL76B64YB5PDID2T3FONK5Q", +}; +const filters = [ + { + applicationIds: "842126029", + applicationAddress: "M7UT7JWIVROIDGMQVJZUBQGBNNIIVOYRPC7JWMGQES4KYJIZHVCRZEGFRQ", + }, +]; diff --git a/blockchain-watcher/test/infrastructure/repositories/AlgorandJsonRPCBlockRepository.test.ts b/blockchain-watcher/test/infrastructure/repositories/AlgorandJsonRPCBlockRepository.test.ts new file mode 100644 index 00000000..56851a78 --- /dev/null +++ b/blockchain-watcher/test/infrastructure/repositories/AlgorandJsonRPCBlockRepository.test.ts @@ -0,0 +1,235 @@ +import { mockRpcPool } from "../../mocks/mockRpcPool"; +mockRpcPool(); + +import { describe, it, expect, afterEach, afterAll } from "@jest/globals"; +import { AlgorandJsonRPCBlockRepository } from "../../../src/infrastructure/repositories"; +import { InstrumentedHttpProvider } from "../../../src/infrastructure/rpc/http/InstrumentedHttpProvider"; +import nock from "nock"; + +let repo: AlgorandJsonRPCBlockRepository; +const rpc = "http://localhost"; + +describe("AlgorandJsonRPCBlockRepository", () => { + afterAll(() => { + nock.restore(); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + it("should be able to get block height", async () => { + const expectedHeight = 40087333n; + givenARepo(); + givenBlockHeightIs(); + + const result = await repo.getBlockHeight(); + + expect(result).toBe(expectedHeight); + }); + + it("should be able to get the transactions", async () => { + givenARepo(); + givenTransactions(); + const applicationId = "842125965"; + const result = await repo.getTransactions(applicationId, 40085294n, 40085299n); + + expect(result).toBeTruthy(); + expect(result[0].applicationId).toBe(842125965); + expect(result[0].blockNumber).toBe(40085294); + expect(result[0].hash).toBe("Y2PTYVGAJYDALKNN4KVIJ4HBJNY5ZZO3BMYT7AR3KBBYQVXHP3JA"); + expect(result[0].payload).toBe( + "AMvOqeysTxjYZtgYEOHVj77EPk0eVW0t1/xm5erC78GEJKVo/TJWgLpH+rPrv39ujSllqUNBG+4LCj1vVEO6HUYAASWlSS/cAIzz6mm+ZqpputkD25jFwEKycUGqjj39nP8yQzvR4PEPC75tIIgDLoj0HSzybbH1N7hFykOVE31Tg1cBAliHbjlP/OhEnOspo9WryTrmRmlQNkVGZ+5TRHkwy5WtVVBj5W0BE0M/hnkVAlny37QN99DRTl6cIs0IM4Bpw+QBA3gXwF6aqPF8aVp3vieQNweFLukFFqGXSdxV6KYOmfojc8/P7wLrD+4P9I2Y1XNfi9IoyihI+anU9IFDPpUkmuQABMJXlTMcms2Yit3JBLVjbiImzbtzPIMCRMLXG082cJVtevhCFtxR94pkOF75THhZy3vZ7v2oCQJXp6fssKGP2jgABihy7P7j+ovzB0/i+emkEXZMAoJcrviPbx11A+hAS36ubl+pk1sO1K4d7FM4HjP/f0WosNoCBXqfyD3jCrlZO/kA" + ); + expect(result[0].sender).toBe("EZATROXX2HISIRZDRGXW4LRQ46Z6IUJYYIHU3PJGP7P5IQDPKVX42N767A"); + expect(result[0].timestamp).toBe(1719311110); + }); +}); + +const givenARepo = () => { + repo = new AlgorandJsonRPCBlockRepository( + { get: () => new InstrumentedHttpProvider({ url: rpc, chain: "algorand" }) } as any, + { get: () => new InstrumentedHttpProvider({ url: rpc, chain: "algorand" }) } as any + ); +}; + +const givenBlockHeightIs = () => { + nock(rpc).post("/v2/status").reply(200, { "last-round": 40087333 }); +}; + +const givenTransactions = () => { + nock(rpc) + .post("/v2/transactions?application-id=842125965&min-round=40085294&max-round=40085299") + .reply(200, { + "current-round": 40087557, + "next-token": "LqdjAgAAAAA6AAAA", + transactions: [ + { + "application-transaction": { + accounts: [ + "22DBCQI25XZ52JB5QPBQ72BCMHIYGCEJ3ODIRVOD5MRQTIV6IQUHAT7T5A", + "XJC32PG73M4VIWAAZQZX6LRHPDAX2DMGVGQJJZSWJUZ5LECEVFMGJENX2M", + ], + "application-args": [ + "dmVyaWZ5U2lncw==", + "AMvOqeysTxjYZtgYEOHVj77EPk0eVW0t1/xm5erC78GEJKVo/TJWgLpH+rPrv39ujSllqUNBG+4LCj1vVEO6HUYAASWlSS/cAIzz6mm+ZqpputkD25jFwEKycUGqjj39nP8yQzvR4PEPC75tIIgDLoj0HSzybbH1N7hFykOVE31Tg1cBAliHbjlP/OhEnOspo9WryTrmRmlQNkVGZ+5TRHkwy5WtVVBj5W0BE0M/hnkVAlny37QN99DRTl6cIs0IM4Bpw+QBA3gXwF6aqPF8aVp3vieQNweFLukFFqGXSdxV6KYOmfojc8/P7wLrD+4P9I2Y1XNfi9IoyihI+anU9IFDPpUkmuQABMJXlTMcms2Yit3JBLVjbiImzbtzPIMCRMLXG082cJVtevhCFtxR94pkOF75THhZy3vZ7v2oCQJXp6fssKGP2jgABihy7P7j+ovzB0/i+emkEXZMAoJcrviPbx11A+hAS36ubl+pk1sO1K4d7FM4HjP/f0WosNoCBXqfyD3jCrlZO/kA", + "WJO1p2w/c5ZFZIiFvczAbNcKPNP/bLlSWJvehiwl70OSEy+51KQhVxFN6EYBk73zovz4H4agl2X0di/REHoAhrMtegl3kmogUTHYcx05y+uMgrL9gvrtJxHVmvDySZ0W5yb2slTOW000j7dLlY6JZuLsPb1JWKfN", + "ya4vUYDhuV1IzK9Kb9jilGSjYp5N0/v88OdQsvQmkWw=", + ], + "application-id": 842125965, + "foreign-apps": [], + "foreign-assets": [], + "global-state-schema": { "num-byte-slice": 0, "num-uint": 0 }, + "local-state-schema": { "num-byte-slice": 0, "num-uint": 0 }, + "on-completion": "noop", + }, + "close-rewards": 0, + "closing-amount": 0, + "confirmed-round": 40085294, + fee: 0, + "first-valid": 40085291, + "genesis-hash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=", + "genesis-id": "mainnet-v1.0", + group: "G2RHZhcMt6mtjYBl4tE6CP0Lfw+KNLf3Lj8ciEKby3U=", + id: "Y2PTYVGAJYDALKNN4KVIJ4HBJNY5ZZO3BMYT7AR3KBBYQVXHP3JA", + "intra-round-offset": 55, + "last-valid": 40086291, + "receiver-rewards": 0, + "round-time": 1719311110, + sender: "EZATROXX2HISIRZDRGXW4LRQ46Z6IUJYYIHU3PJGP7P5IQDPKVX42N767A", + "sender-rewards": 0, + signature: { + logicsig: { + args: [], + logic: + "BiAEAQAgFCYBADEgMgMSRDEBIxJEMRCBBhJENhoBNhoDNhoCiAADRCJDNQI1ATUAKDXwKDXxNAAVNQUjNQMjNQQ0AzQFDEEARDQBNAA0A4FBCCJYFzQANAMiCCRYNAA0A4EhCCRYBwA18TXwNAI0BCVYNPA08VACVwwUEkQ0A4FCCDUDNAQlCDUEQv+0Iok=", + }, + }, + "tx-type": "appl", + }, + { + "application-transaction": { + accounts: [ + "22DBCQI25XZ52JB5QPBQ72BCMHIYGCEJ3ODIRVOD5MRQTIV6IQUHAT7T5A", + "XJC32PG73M4VIWAAZQZX6LRHPDAX2DMGVGQJJZSWJUZ5LECEVFMGJENX2M", + ], + "application-args": [ + "dmVyaWZ5U2lncw==", + "CBZCCH+WTb6bYLPJC+nzzjvHA73QDH4lDkA4KjiJq3Z8MXRqRn6vFR7vjszj9NJCRw9xD6xscpMW0sNXcMFDDqsACYzV/6FXG/XZ7Kd4FYfoQ2ZHLSeRxRF4Xrt5YvsyhxOTHdj0S2b7eb7B41CxDUKzhYvWGf3V4WhYfvRgMFou944BDC8szu1vORgXN/EWBAJwBRmjOZXpHNH7GRIFupubGSQPIDtuom2/DHo2F77vxKC7HXVMVHbV7C3oOgB8w2lXfSABD9wQtyp+4hZAHFNOclrkTNgO/BRFaxXbTwKzKMv1ax3+O21cf3eSlYfdlwTz2DJrts/M/72v18EZriEFGPnFtHUAEOa6k9hnDJZys1ZJ4cSsd77DIFSRTc6GxbaWBTYXfMOjTMSArzdUl5XQKFTRIrDMHRY+sM4dz5dAWc7IM1rYcIoAES8aaahKohCHoR/d//Z0ZT1P4+LcgrF4rABIxki6mDbSaz+86v+derrWYHErb8+s5OpH5qwGbrftiXsDlebiyN8A", + "dKO/kTlT1pUmDYi8GqJaTu42PvAACsAHZyezX76i2sKP7lzLD+p2jtLMN6TcA2qNIytI9izdRzFBL0iQgZK25zh8zXaCd8F9qxt6UCfAs88XjiGtLneuBnEVSc+7H5x6nYCW6F4Uh/NVFdAqknU1BKjXVHG59J7b", + "ya4vUYDhuV1IzK9Kb9jilGSjYp5N0/v88OdQsvQmkWw=", + ], + "application-id": 842125965, + "foreign-apps": [], + "foreign-assets": [], + "global-state-schema": { "num-byte-slice": 0, "num-uint": 0 }, + "local-state-schema": { "num-byte-slice": 0, "num-uint": 0 }, + "on-completion": "noop", + }, + "close-rewards": 0, + "closing-amount": 0, + "confirmed-round": 40085294, + fee: 0, + "first-valid": 40085291, + "genesis-hash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=", + "genesis-id": "mainnet-v1.0", + group: "G2RHZhcMt6mtjYBl4tE6CP0Lfw+KNLf3Lj8ciEKby3U=", + id: "KXFFIB7C7Z7SYMAFN4BVJEYMUT6DGWFNGYJKK2V2PGMUDJ6I3XHA", + "intra-round-offset": 56, + "last-valid": 40086291, + "receiver-rewards": 0, + "round-time": 1719311110, + sender: "EZATROXX2HISIRZDRGXW4LRQ46Z6IUJYYIHU3PJGP7P5IQDPKVX42N767A", + "sender-rewards": 0, + signature: { + logicsig: { + args: [], + logic: + "BiAEAQAgFCYBADEgMgMSRDEBIxJEMRCBBhJENhoBNhoDNhoCiAADRCJDNQI1ATUAKDXwKDXxNAAVNQUjNQMjNQQ0AzQFDEEARDQBNAA0A4FBCCJYFzQANAMiCCRYNAA0A4EhCCRYBwA18TXwNAI0BCVYNPA08VACVwwUEkQ0A4FCCDUDNAQlCDUEQv+0Iok=", + }, + }, + "tx-type": "appl", + }, + { + "application-transaction": { + accounts: [ + "22DBCQI25XZ52JB5QPBQ72BCMHIYGCEJ3ODIRVOD5MRQTIV6IQUHAT7T5A", + "XJC32PG73M4VIWAAZQZX6LRHPDAX2DMGVGQJJZSWJUZ5LECEVFMGJENX2M", + ], + "application-args": [ + "dmVyaWZ5U2lncw==", + "Eg6Rp7M4ooj+qsdqgzt4DUmLgUU2eqBVF1nWuEt0lJgNWAQ2ZPgqp2R+NcsxXEys/lIpSaYgxT3pGLbFWRdpLWgA", + "b768iY9APkdz6V/rFegMmpnINI0=", + "ya4vUYDhuV1IzK9Kb9jilGSjYp5N0/v88OdQsvQmkWw=", + ], + "application-id": 842125965, + "foreign-apps": [], + "foreign-assets": [], + "global-state-schema": { "num-byte-slice": 0, "num-uint": 0 }, + "local-state-schema": { "num-byte-slice": 0, "num-uint": 0 }, + "on-completion": "noop", + }, + "close-rewards": 0, + "closing-amount": 0, + "confirmed-round": 40085294, + fee: 0, + "first-valid": 40085291, + "genesis-hash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=", + "genesis-id": "mainnet-v1.0", + group: "G2RHZhcMt6mtjYBl4tE6CP0Lfw+KNLf3Lj8ciEKby3U=", + id: "WKV3GMESISL2DV3CQJZQ365CIZDPRQ4YJ2Q2J775YTYVTFPMWQQQ", + "intra-round-offset": 57, + "last-valid": 40086291, + "receiver-rewards": 0, + "round-time": 1719311110, + sender: "EZATROXX2HISIRZDRGXW4LRQ46Z6IUJYYIHU3PJGP7P5IQDPKVX42N767A", + "sender-rewards": 0, + signature: { + logicsig: { + args: [], + logic: + "BiAEAQAgFCYBADEgMgMSRDEBIxJEMRCBBhJENhoBNhoDNhoCiAADRCJDNQI1ATUAKDXwKDXxNAAVNQUjNQMjNQQ0AzQFDEEARDQBNAA0A4FBCCJYFzQANAMiCCRYNAA0A4EhCCRYBwA18TXwNAI0BCVYNPA08VACVwwUEkQ0A4FCCDUDNAQlCDUEQv+0Iok=", + }, + }, + "tx-type": "appl", + }, + { + "application-transaction": { + accounts: [ + "22DBCQI25XZ52JB5QPBQ72BCMHIYGCEJ3ODIRVOD5MRQTIV6IQUHAT7T5A", + "XJC32PG73M4VIWAAZQZX6LRHPDAX2DMGVGQJJZSWJUZ5LECEVFMGJENX2M", + ], + "application-args": [ + "dmVyaWZ5VkFB", + "AQAAAAQNAMvOqeysTxjYZtgYEOHVj77EPk0eVW0t1/xm5erC78GEJKVo/TJWgLpH+rPrv39ujSllqUNBG+4LCj1vVEO6HUYAASWlSS/cAIzz6mm+ZqpputkD25jFwEKycUGqjj39nP8yQzvR4PEPC75tIIgDLoj0HSzybbH1N7hFykOVE31Tg1cBAliHbjlP/OhEnOspo9WryTrmRmlQNkVGZ+5TRHkwy5WtVVBj5W0BE0M/hnkVAlny37QN99DRTl6cIs0IM4Bpw+QBA3gXwF6aqPF8aVp3vieQNweFLukFFqGXSdxV6KYOmfojc8/P7wLrD+4P9I2Y1XNfi9IoyihI+anU9IFDPpUkmuQABMJXlTMcms2Yit3JBLVjbiImzbtzPIMCRMLXG082cJVtevhCFtxR94pkOF75THhZy3vZ7v2oCQJXp6fssKGP2jgABihy7P7j+ovzB0/i+emkEXZMAoJcrviPbx11A+hAS36ubl+pk1sO1K4d7FM4HjP/f0WosNoCBXqfyD3jCrlZO/kACBZCCH+WTb6bYLPJC+nzzjvHA73QDH4lDkA4KjiJq3Z8MXRqRn6vFR7vjszj9NJCRw9xD6xscpMW0sNXcMFDDqsACYzV/6FXG/XZ7Kd4FYfoQ2ZHLSeRxRF4Xrt5YvsyhxOTHdj0S2b7eb7B41CxDUKzhYvWGf3V4WhYfvRgMFou944BDC8szu1vORgXN/EWBAJwBRmjOZXpHNH7GRIFupubGSQPIDtuom2/DHo2F77vxKC7HXVMVHbV7C3oOgB8w2lXfSABD9wQtyp+4hZAHFNOclrkTNgO/BRFaxXbTwKzKMv1ax3+O21cf3eSlYfdlwTz2DJrts/M/72v18EZriEFGPnFtHUAEOa6k9hnDJZys1ZJ4cSsd77DIFSRTc6GxbaWBTYXfMOjTMSArzdUl5XQKFTRIrDMHRY+sM4dz5dAWc7IM1rYcIoAES8aaahKohCHoR/d//Z0ZT1P4+LcgrF4rABIxki6mDbSaz+86v+derrWYHErb8+s5OpH5qwGbrftiXsDlebiyN8AEg6Rp7M4ooj+qsdqgzt4DUmLgUU2eqBVF1nWuEt0lJgNWAQ2ZPgqp2R+NcsxXEys/lIpSaYgxT3pGLbFWRdpLWgAZnqa+gADj5AABgAAAAAAAAAAAAAAAA4ILwb/ZX2UMQy4zosNmgRUHYBSAAAAAAADXAIBAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACw7dfAAAAAAAAAAAAAAAAuX7574c0xxkE2AAvi2vGbdnEim4ABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABK8EJrAAgAAAAAAAAAAAAAAABTZwZsNNSHRYgR77UGp6Kv2tuOi3dvcm1ob2xlRGVwb3NpdAAAAAAAAAAAAAAAAAurcc/Wfp8DdXO0rccmziZuKxssAAAAAAAAAAAAAAAASvBCawAXAAAAAAAAAAAAAAAAC6txz9Z+nwN1c7StxybOJm4rGyw=", + ], + "application-id": 842125965, + "foreign-apps": [], + "foreign-assets": [], + "global-state-schema": { "num-byte-slice": 0, "num-uint": 0 }, + "local-state-schema": { "num-byte-slice": 0, "num-uint": 0 }, + "on-completion": "noop", + }, + "close-rewards": 0, + "closing-amount": 0, + "confirmed-round": 40085294, + fee: 4000, + "first-valid": 40085291, + "genesis-hash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=", + "genesis-id": "mainnet-v1.0", + group: "G2RHZhcMt6mtjYBl4tE6CP0Lfw+KNLf3Lj8ciEKby3U=", + id: "3E7KILAIPB4HE4XF5TWUBDHIRXGECZZSLKV3ERPZF64OJXEPRBSA", + "intra-round-offset": 58, + "last-valid": 40086291, + "receiver-rewards": 0, + "round-time": 1719311110, + sender: "C3EXCPEEMYTIJ2EYUMEMLBDHIJ7J2KAHGKFHWD4GQX5MP7PYZO7O2C6YZE", + "sender-rewards": 0, + signature: { + sig: "hSaeNt/qY/+QVVDWc44yYcYlt0SejQMLPs/HJp73Io1KzW/0OvKLvWchVu+9YGZdaEc+6xwq8kHMLBrlohpIAA==", + }, + "tx-type": "appl", + }, + ], + }); +}; diff --git a/blockchain-watcher/test/infrastructure/repositories/RepositoriesBuilder.test.ts b/blockchain-watcher/test/infrastructure/repositories/RepositoriesBuilder.test.ts index 9e9e17d6..88064bc7 100644 --- a/blockchain-watcher/test/infrastructure/repositories/RepositoriesBuilder.test.ts +++ b/blockchain-watcher/test/infrastructure/repositories/RepositoriesBuilder.test.ts @@ -2,9 +2,11 @@ import { mockRpcPool } from "../../mocks/mockRpcPool"; mockRpcPool(); import { RateLimitedWormchainJsonRPCBlockRepository } from "../../../src/infrastructure/repositories/wormchain/RateLimitedWormchainJsonRPCBlockRepository"; +import { RateLimitedAlgorandJsonRPCBlockRepository } from "../../../src/infrastructure/repositories/algorand/RateLimitedAlgorandJsonRPCBlockRepository"; 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 { RateLimitedSeiJsonRPCBlockRepository } from "../../../src/infrastructure/repositories/sei/RateLimitedSeiJsonRPCBlockRepository"; import { describe, expect, it } from "@jest/globals"; import { RepositoriesBuilder } from "../../../src/infrastructure/repositories/RepositoriesBuilder"; import { configMock } from "../../mocks/configMock"; @@ -14,7 +16,6 @@ import { PromStatRepository, SnsEventRepository, } from "../../../src/infrastructure/repositories"; -import { RateLimitedSeiJsonRPCBlockRepository } from "../../../src/infrastructure/repositories/sei/RateLimitedSeiJsonRPCBlockRepository"; describe("RepositoriesBuilder", () => { it("should be throw error because dose not have any chain", async () => { @@ -114,6 +115,7 @@ describe("RepositoriesBuilder", () => { expect(repos.getEvmBlockRepository("xlayer")).toBeInstanceOf( RateLimitedEvmJsonRPCBlockRepository ); + expect(repos.getAlgorandRepository()).toBeInstanceOf(RateLimitedAlgorandJsonRPCBlockRepository); expect(repos.getAptosRepository()).toBeInstanceOf(RateLimitedAptosJsonRPCBlockRepository); expect(repos.getMetadataRepository()).toBeInstanceOf(FileMetadataRepository); expect(repos.getSnsEventRepository()).toBeInstanceOf(SnsEventRepository); diff --git a/blockchain-watcher/test/infrastructure/repositories/StaticJobRepository.test.ts b/blockchain-watcher/test/infrastructure/repositories/StaticJobRepository.test.ts index ee7ff511..2f7b2764 100644 --- a/blockchain-watcher/test/infrastructure/repositories/StaticJobRepository.test.ts +++ b/blockchain-watcher/test/infrastructure/repositories/StaticJobRepository.test.ts @@ -9,6 +9,7 @@ import { WormchainRepository, EvmBlockRepository, MetadataRepository, + AlgorandRepository, AptosRepository, StatRepository, SeiRepository, @@ -25,6 +26,7 @@ const suiRepo = {} as any as SuiRepository; const aptosRepo = {} as any as AptosRepository; const wormchainRepo = {} as any as WormchainRepository; const seiRepo = {} as any as SeiRepository; +const algorandRepo = {} as any as AlgorandRepository; let repo: StaticJobRepository; @@ -42,6 +44,7 @@ describe("StaticJobRepository", () => { aptosRepo, wormchainRepo, seiRepo, + algorandRepo, }); }); diff --git a/blockchain-watcher/test/mocks/configMock.ts b/blockchain-watcher/test/mocks/configMock.ts index 0bf50bbd..195e506f 100644 --- a/blockchain-watcher/test/mocks/configMock.ts +++ b/blockchain-watcher/test/mocks/configMock.ts @@ -38,6 +38,13 @@ export const configMock = (): Config => { rpcs: ["http://localhost"], timeout: 10000, }, + algorand: { + name: "algorand", + network: "testnet", + chainId: 8, + rpcs: [["http://localhost"], ["http://localhost"]] as any, + timeout: 10000, + }, fantom: { name: "fantom", network: "testnet", @@ -254,7 +261,7 @@ export const configMock = (): Config => { dir: "./metadata-repo/jobs", }, chains: chainsRecord, - enabledPlatforms: ["solana", "evm", "sui", "aptos", "wormchain", "sei"], + enabledPlatforms: ["solana", "evm", "sui", "aptos", "wormchain", "sei", "algorand"], }; return cfg; diff --git a/deploy/blockchain-watcher/workers/source-events-3.yaml b/deploy/blockchain-watcher/workers/source-events-3.yaml index 1e7fe541..dee23beb 100644 --- a/deploy/blockchain-watcher/workers/source-events-3.yaml +++ b/deploy/blockchain-watcher/workers/source-events-3.yaml @@ -105,6 +105,37 @@ data: } } ] + }, + { + "id": "poll-log-message-published-algorand", + "chain": "algorand", + "source": { + "action": "PollAlgorand", + "config": { + "blockBatchSize": 100, + "commitment": "latest", + "interval": 25000, + "applicationIds": ["86525623"], + "chain": "algorand", + "chainId": 8 + } + }, + "handlers": [ + { + "action": "HandleAlgorandTransactions", + "target": "sns", + "mapper": "algorandLogMessagePublishedMapper", + "config": { + "metricName": "process_source_event", + "filter": [ + { + "applicationIds": "86525623", + "applicationAddress": "C2SZBD4ZFFDXANBCUTG5GBUEWMQ34JS5LFGDRTEVJBAXDRF6ZWB7Q4KHHM" + } + ] + } + } + ] } ] mainnet-jobs.json: |- @@ -183,6 +214,37 @@ data: } } ] + }, + { + "id": "poll-log-message-published-algorand", + "chain": "algorand", + "source": { + "action": "PollAlgorand", + "config": { + "blockBatchSize": 100, + "commitment": "latest", + "interval": 15000, + "applicationIds": ["842125965"], + "chain": "algorand", + "chainId": 8 + } + }, + "handlers": [ + { + "action": "HandleAlgorandTransactions", + "target": "sns", + "mapper": "algorandLogMessagePublishedMapper", + "config": { + "metricName": "process_source_event", + "filter": [ + { + "applicationIds": "842125965", + "applicationAddress": "J476J725L4JTOI2YU6DAI4E23LYUECLZR7RCYZ3LK6QFHX4M54ZI53SGXQ" + } + ] + } + } + ] } ] --- @@ -231,6 +293,10 @@ spec: - name: APTOS_RPCS value: '{{ .APTOS_RPCS }}' {{ end }} + {{ if .ALGORAND_RPCS }} + - name: ALGORAND_RPCS + value: '{{ .ALGORAND_RPCS }}' + {{ end }} image: {{ .IMAGE_NAME }} resources: limits: diff --git a/deploy/blockchain-watcher/workers/target-events-3.yaml b/deploy/blockchain-watcher/workers/target-events-3.yaml index 059322e7..8709b6de 100644 --- a/deploy/blockchain-watcher/workers/target-events-3.yaml +++ b/deploy/blockchain-watcher/workers/target-events-3.yaml @@ -166,6 +166,37 @@ data: } } ] + }, + { + "id": "poll-redeemed-transactions-algorand", + "chain": "algorand", + "source": { + "action": "PollAlgorand", + "config": { + "blockBatchSize": 100, + "commitment": "latest", + "interval": 25000, + "applicationIds": ["86525641"], + "chain": "algorand", + "chainId": 8 + } + }, + "handlers": [ + { + "action": "HandleAlgorandTransactions", + "target": "sns", + "mapper": "algorandRedeemedTransactionFoundMapper", + "config": { + "filter": [ + { + "applicationIds": "86525641", + "applicationAddress": "MJA77XADFNUTX64FISCY6BAD33EG6LQXECXZ6NHY2ZP6K5FWEOGH6D62HA" + } + ], + "metricName": "process_vaa_event" + } + } + ] } ] mainnet-jobs.json: |- @@ -333,6 +364,37 @@ data: } } ] + }, + { + "id": "poll-redeemed-transactions-algorand", + "chain": "algorand", + "source": { + "action": "PollAlgorand", + "config": { + "blockBatchSize": 100, + "commitment": "latest", + "interval": 15000, + "applicationIds": ["842126029"], + "chain": "algorand", + "chainId": 8 + } + }, + "handlers": [ + { + "action": "HandleAlgorandTransactions", + "target": "sns", + "mapper": "algorandRedeemedTransactionFoundMapper", + "config": { + "filter": [ + { + "applicationIds": "842126029", + "applicationAddress": "M7UT7JWIVROIDGMQVJZUBQGBNNIIVOYRPC7JWMGQES4KYJIZHVCRZEGFRQ" + } + ], + "metricName": "process_vaa_event" + } + } + ] } ] --- @@ -381,6 +443,18 @@ spec: - name: SUI_RPCS value: '{{ .SUI_RPCS }}' {{ end }} + {{ if .INJECTIVE_RPCS }} + - name: INJECTIVE_RPCS + value: '{{ .INJECTIVE_RPCS }}' + {{ end }} + {{ if .OSMOSIS_RPCS }} + - name: OSMOSIS_RPCS + value: '{{ .OSMOSIS_RPCS }}' + {{ end }} + {{ if .ALGORAND_RPCS }} + - name: ALGORAND_RPCS + value: '{{ .ALGORAND_RPCS }}' + {{ end }} image: {{ .IMAGE_NAME }} resources: limits: