[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 <julianmerlo@julians-MacBook-Pro-2.local>
This commit is contained in:
Julian 2024-06-25 16:56:15 -03:00 committed by GitHub
parent 2646baa9f1
commit 6f7d457386
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 1574 additions and 13 deletions

View File

@ -242,6 +242,13 @@
"__name": "SEI_RPCS",
"__format": "json"
}
},
"algorand": {
"network": "ALGORAND_NETWORK",
"rpcs": {
"__name": "ALGORAND_RPCS",
"__format": "json"
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<void>,
private readonly statsRepo: StatRepository
) {}
public async handle(txs: AlgorandTransaction[]): Promise<TransactionFoundEvent[]> {
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;
};

View File

@ -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<PollAlgorandMetadata>;
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<PollAlgorandMetadata>,
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<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] PollAlgorand: (${this.cfg.id}) Finished processing all blocks from ${this.cfg.fromBlock} to ${this.cfg.toBlock}`
);
}
return !hasFinished;
}
protected async get(): Promise<AlgorandTransaction[]> {
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<void> {
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;
};

View File

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

View File

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

View File

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

View File

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

View File

@ -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<bigint>;
@ -97,6 +98,15 @@ export interface SeiRepository {
getBlockTimestamp(blockNumber: bigint): Promise<number | undefined>;
}
export interface AlgorandRepository {
getTransactions(
applicationId: string,
fromBlock: bigint,
toBlock: bigint
): Promise<AlgorandTransaction[]>;
getBlockHeight(): Promise<bigint | undefined>;
}
export interface MetadataRepository<Metadata> {
get(id: string): Promise<Metadata | undefined>;
save(id: string, metadata: Metadata): Promise<void>;

View File

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

View File

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

View File

@ -1778,6 +1778,21 @@
]
}
]
},
{
"chain": "algorand",
"protocols": [
{
"addresses": ["842126029", "86525641"],
"type": "Token Bridge",
"methods": [
{
"methodId": "completeTransfer",
"method": "Token Bridge"
}
]
}
]
}
]
}

View File

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

View File

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

View File

@ -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<InstrumentedHttpProvider>;
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<InstrumentedHttpProvider>,
algoIndexerPools: ProviderPool<InstrumentedHttpProvider>
) {
this.logger = winston.child({ module: "AlgorandJsonRPCBlockRepository" });
this.algoV2Pools = algoV2Pools;
this.algoIndexerPools = algoIndexerPools;
}
async getBlockHeight(): Promise<bigint | undefined> {
let result: ResultStatus;
result = await this.algoV2Pools.get().get<typeof result>(STATUS_ENDPOINT);
return BigInt(result["last-round"]);
}
async getTransactions(
applicationId: string,
fromBlock: bigint,
toBlock: bigint
): Promise<AlgorandTransaction[]> {
try {
let result: ResultTransactions;
result = await this.algoIndexerPools
.get()
.get<typeof result>(
`${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[];
};
}[];
}[];
};

View File

@ -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<AlgorandRepository>
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<AlgorandTransaction[]> {
return this.breaker
.fn(() => this.delegate.getTransactions(applicationId, fromBlock, toBlock))
.execute();
}
getBlockHeight(): Promise<bigint | undefined> {
return this.breaker.fn(() => this.delegate.getBlockHeight()).execute();
}
}

View File

@ -20,3 +20,4 @@ export * from "./solana/Web3SolanaSlotRepository";
export * from "./solana/RateLimitedSolanaSlotRepository";
export * from "./sui/SuiJsonRPCBlockRepository";
export * from "./wormchain/WormchainJsonRPCBlockRepository";
export * from "./algorand/AlgorandJsonRPCBlockRepository";

View File

@ -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<InstrumentedHttpProvider>;
@ -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) => {

View File

@ -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<AlgorandRepository["getBlockHeight"]>;
let getTransactionsSpy: jest.SpiedFunction<AlgorandRepository["getTransactions"]>;
let metadataSaveSpy: jest.SpiedFunction<MetadataRepository<PollAlgorandMetadata>["save"]>;
let handlerSpy: jest.SpiedFunction<(txs: AlgorandTransaction[]) => Promise<void>>;
let metadataRepo: MetadataRepository<PollAlgorandMetadata>;
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]);
};

View File

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

View File

@ -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<AlgorandRepository["getBlockHeight"]>;
let getTransactionsSpy: jest.SpiedFunction<AlgorandRepository["getTransactions"]>;
let handlerSpy: jest.SpiedFunction<(txs: AlgorandTransaction[]) => Promise<void>>;
let metadataSaveSpy: jest.SpiedFunction<MetadataRepository<PollAlgorandMetadata>["save"]>;
let metadataRepo: MetadataRepository<PollAlgorandMetadata>;
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]);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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