From d20e34f1fa3e57176a3abd17b5660d0816d12a59 Mon Sep 17 00:00:00 2001 From: Victor Hugo Date: Mon, 12 Jan 2026 16:10:51 -0300 Subject: [PATCH 1/4] feat: Implement the service to verify the transaction status on the network and handle it in the database --- src/core/service/verifier/verifier.errors.ts | 38 ++++ src/core/service/verifier/verifier.process.ts | 189 ++++++++++++++++++ src/core/service/verifier/verifier.service.ts | 59 ++++++ src/core/service/verifier/verifier.types.ts | 15 ++ 4 files changed, 301 insertions(+) create mode 100644 src/core/service/verifier/verifier.errors.ts create mode 100644 src/core/service/verifier/verifier.process.ts create mode 100644 src/core/service/verifier/verifier.service.ts create mode 100644 src/core/service/verifier/verifier.types.ts diff --git a/src/core/service/verifier/verifier.errors.ts b/src/core/service/verifier/verifier.errors.ts new file mode 100644 index 0000000..f9004d2 --- /dev/null +++ b/src/core/service/verifier/verifier.errors.ts @@ -0,0 +1,38 @@ +import { PlatformError } from "@/error/index.ts"; + +export enum VERIFIER_ERROR_CODES { + TRANSACTION_NOT_FOUND = "VRF_001", + VERIFICATION_FAILED = "VRF_002", +} + +const source = "@service/verifier"; + +/** + * Error thrown when a transaction is not found on the network + */ +export class TRANSACTION_NOT_FOUND extends PlatformError<{ transactionId: string }> { + constructor(transactionId: string) { + super({ + source, + code: VERIFIER_ERROR_CODES.TRANSACTION_NOT_FOUND, + message: "Transaction not found on network", + details: `The transaction with ID '${transactionId}' was not found on the Stellar network.`, + meta: { transactionId }, + }); + } +} + +/** + * Error thrown when transaction verification fails + */ +export class VERIFICATION_FAILED extends PlatformError<{ transactionId: string; reason: string }> { + constructor(transactionId: string, reason: string) { + super({ + source, + code: VERIFIER_ERROR_CODES.VERIFICATION_FAILED, + message: "Transaction verification failed", + details: `Failed to verify transaction '${transactionId}': ${reason}`, + meta: { transactionId, reason }, + }); + } +} diff --git a/src/core/service/verifier/verifier.process.ts b/src/core/service/verifier/verifier.process.ts new file mode 100644 index 0000000..d2c4306 --- /dev/null +++ b/src/core/service/verifier/verifier.process.ts @@ -0,0 +1,189 @@ +import { LOG } from "@/config/logger.ts"; +import { drizzleClient } from "@/persistence/drizzle/config.ts"; +import { BundleStatus } from "@/persistence/drizzle/entity/operations-bundle.entity.ts"; +import { TransactionStatus } from "@/persistence/drizzle/entity/transaction.entity.ts"; +import { MEMPOOL_VERIFIER_INTERVAL_MS, NETWORK_RPC_SERVER } from "@/config/env.ts"; +import { verifyTransactionOnNetwork } from "@/core/service/verifier/verifier.service.ts"; +import { + TransactionRepository, + BundleTransactionRepository, + OperationsBundleRepository, +} from "@/persistence/drizzle/repository/index.ts"; + +const VERIFIER_CONFIG = { + INTERVAL_MS: MEMPOOL_VERIFIER_INTERVAL_MS, +} as const; + +const transactionRepository = new TransactionRepository(); +const bundleTransactionRepository = new BundleTransactionRepository(); +const operationsBundleRepository = new OperationsBundleRepository(drizzleClient); + +/** + * Updates transaction status in database + */ +async function updateTransactionStatus( + txId: string, + status: TransactionStatus +): Promise { + await transactionRepository.update(txId, { + status, + updatedAt: new Date(), + }); +} + +/** + * Updates bundles status based on transaction verification result + */ +async function updateBundlesStatus( + bundleIds: string[], + status: BundleStatus +): Promise { + for (const bundleId of bundleIds) { + try { + await operationsBundleRepository.update(bundleId, { + status, + updatedAt: new Date(), + }); + } catch (error) { + LOG.error(`Failed to update bundle ${bundleId} status`, { error }); + } + } +} + +/** + * Handles verification failure by updating transaction and bundles + */ +async function handleVerificationFailure( + txId: string, + reason: string, + bundleIds: string[] +): Promise { + LOG.warn("Transaction verification failed", { txId, reason, bundleIds }); + + // Update transaction status to FAILED + await updateTransactionStatus(txId, TransactionStatus.FAILED); + + // Update bundles back to PENDING for potential retry + await updateBundlesStatus(bundleIds, BundleStatus.PENDING); +} + +/** + * Handles successful verification by updating transaction and bundles + */ +async function handleVerificationSuccess( + txId: string, + bundleIds: string[] +): Promise { + LOG.info("Transaction verified successfully", { txId, bundleCount: bundleIds.length }); + + // Update transaction status to VERIFIED + await updateTransactionStatus(txId, TransactionStatus.VERIFIED); + + // Update bundles to COMPLETED + await updateBundlesStatus(bundleIds, BundleStatus.COMPLETED); +} + +/** + * Verifier Service for monitoring and verifying transactions on the network + */ +export class Verifier { + private intervalId: number | null = null; + private isRunning: boolean = false; + + /** + * Starts the verifier loop + */ + start(): void { + if (this.isRunning) { + LOG.warn("Verifier is already running"); + return; + } + + this.isRunning = true; + LOG.info("Verifier started", { intervalMs: VERIFIER_CONFIG.INTERVAL_MS }); + + // Verify immediately, then on interval + this.verifyTransactions(); + + this.intervalId = setInterval(() => { + this.verifyTransactions(); + }, VERIFIER_CONFIG.INTERVAL_MS) as unknown as number; + } + + /** + * Stops the verifier loop + */ + stop(): void { + if (!this.isRunning) { + return; + } + + this.isRunning = false; + if (this.intervalId !== null) { + clearInterval(this.intervalId); + this.intervalId = null; + } + LOG.info("Verifier stopped"); + } + + /** + * Verifies all unverified transactions + */ + async verifyTransactions(): Promise { + try { + // Get all unverified transactions + const unverifiedTransactions = await transactionRepository.findByStatus( + TransactionStatus.UNVERIFIED + ); + + if (unverifiedTransactions.length === 0) { + // No transactions to verify + return; + } + + LOG.debug(`Verifying ${unverifiedTransactions.length} transactions`); + + // Verify each transaction + for (const transaction of unverifiedTransactions) { + await this.verifyTransaction(transaction.id); + } + } catch (error) { + LOG.error("Error during transaction verification", { + error: error instanceof Error ? error.message : String(error), + }); + } + } + + /** + * Verifies a single transaction + */ + private async verifyTransaction(txId: string): Promise { + try { + // Get bundles linked to this transaction + const bundleTransactions = await bundleTransactionRepository.findByTransactionId(txId); + const bundleIds = bundleTransactions.map((bt) => bt.bundleId); + + if (bundleIds.length === 0) { + LOG.warn(`No bundles found for transaction ${txId}`); + return; + } + + // Verify transaction on network + const result = await verifyTransactionOnNetwork(txId, NETWORK_RPC_SERVER); + + // Handle verification result + if (result.status === "VERIFIED") { + await handleVerificationSuccess(txId, bundleIds); + } else if (result.status === "FAILED") { + await handleVerificationFailure(txId, result.reason, bundleIds); + } else { + // PENDING - transaction not yet found on network, will check again next cycle + LOG.debug(`Transaction ${txId} still pending verification`); + } + } catch (error) { + LOG.error(`Failed to verify transaction ${txId}`, { + error: error instanceof Error ? error.message : String(error), + }); + } + } +} diff --git a/src/core/service/verifier/verifier.service.ts b/src/core/service/verifier/verifier.service.ts new file mode 100644 index 0000000..7f7d4b8 --- /dev/null +++ b/src/core/service/verifier/verifier.service.ts @@ -0,0 +1,59 @@ +import type { Server } from "stellar-sdk/rpc"; +import type { VerificationResult } from "@/core/service/verifier/verifier.types.ts"; + +/** + * Verifies a transaction on the Stellar network + * Checks if the transaction was included in a ledger + * + * @param txHash - Transaction hash to verify + * @param rpcServer - Stellar RPC server instance + * @returns Verification result: VERIFIED, FAILED, or PENDING + */ +export async function verifyTransactionOnNetwork( + txHash: string, + rpcServer: Server +): Promise { + try { + // Try to get transaction by hash + const txResponse = await rpcServer.getTransaction(txHash); + + if (!txResponse) { + // Transaction not found - might be pending or failed + return { status: "PENDING" }; + } + + // Check if transaction was successful + if (txResponse.successful === true) { + return { + status: "VERIFIED", + ledgerSequence: txResponse.ledger?.toString(), + }; + } + + // Transaction was included but failed + if (txResponse.successful === false) { + const resultCode = txResponse.resultXdr || "unknown"; + return { + status: "FAILED", + reason: `Transaction failed with result code: ${resultCode}`, + }; + } + + // Transaction found but status unclear + return { status: "PENDING" }; + } catch (error) { + // If transaction is not found, it might still be pending + // Check if it's a 404 or similar + const errorMessage = error instanceof Error ? error.message : String(error); + + if (errorMessage.includes("not found") || errorMessage.includes("404")) { + return { status: "PENDING" }; + } + + // Other errors might indicate the transaction failed + return { + status: "FAILED", + reason: errorMessage, + }; + } +} diff --git a/src/core/service/verifier/verifier.types.ts b/src/core/service/verifier/verifier.types.ts new file mode 100644 index 0000000..dc8395a --- /dev/null +++ b/src/core/service/verifier/verifier.types.ts @@ -0,0 +1,15 @@ +/** + * Result of verifying a transaction on the network + */ +export type VerificationResult = + | { status: "VERIFIED"; ledgerSequence?: string } + | { status: "FAILED"; reason: string } + | { status: "PENDING" }; + +/** + * Transaction verification data + */ +export type TransactionVerification = { + transactionId: string; + result: VerificationResult; +}; From 7eb04128110994ecc28e0a25710026491d4f3e2d Mon Sep 17 00:00:00 2001 From: Victor Hugo Date: Mon, 12 Jan 2026 19:13:44 -0300 Subject: [PATCH 2/4] feat: Implement the Mempool System initialization on the application bootstrap --- .env.example | 8 ++ src/core/mempool/index.ts | 80 ++++++++++++++++++- src/main.ts | 36 +++++++-- .../operations-bundle.repository.ts | 2 +- 4 files changed, 117 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 6fc42f3..a732ff5 100644 --- a/.env.example +++ b/.env.example @@ -26,3 +26,11 @@ SERVICE_AUTH_SECRET=S... # AUTH CHALLENGE_TTL=900 #15m SESSION_TTL=21600 #6h + +# MEMPOOL CONFIGURATION +MEMPOOL_SLOT_CAPACITY=100 +MEMPOOL_EXPENSIVE_OP_WEIGHT=10 +MEMPOOL_CHEAP_OP_WEIGHT=1 +MEMPOOL_EXECUTOR_INTERVAL_MS=5000 +MEMPOOL_VERIFIER_INTERVAL_MS=10000 +MEMPOOL_TTL_CHECK_INTERVAL_MS=60000 \ No newline at end of file diff --git a/src/core/mempool/index.ts b/src/core/mempool/index.ts index 4378353..74d17e0 100644 --- a/src/core/mempool/index.ts +++ b/src/core/mempool/index.ts @@ -1,5 +1,8 @@ import { Mempool } from "@/core/service/mempool/mempool.process.ts"; -import { MEMPOOL_SLOT_CAPACITY } from "@/config/env.ts"; +import { Executor } from "@/core/service/executor/executor.process.ts"; +import { Verifier } from "@/core/service/verifier/verifier.process.ts"; +import { MEMPOOL_SLOT_CAPACITY, MEMPOOL_TTL_CHECK_INTERVAL_MS } from "@/config/env.ts"; +import { LOG } from "@/config/logger.ts"; /** * Singleton instance of the Mempool @@ -7,6 +10,23 @@ import { MEMPOOL_SLOT_CAPACITY } from "@/config/env.ts"; */ export let mempool: Mempool; +/** + * Singleton instance of the Executor + * Will be initialized during application startup + */ +export let executor: Executor; + +/** + * Singleton instance of the Verifier + * Will be initialized during application startup + */ +export let verifier: Verifier; + +/** + * Interval ID for TTL check + */ +let ttlCheckIntervalId: number | null = null; + /** * Initializes the mempool singleton instance * Should be called during application startup @@ -29,3 +49,61 @@ export function getMempool(): Mempool { return mempool; } +/** + * Initializes the complete mempool system + * - Initializes Mempool and loads pending bundles from database + * - Starts Executor service + * - Starts Verifier service + * - Starts periodic TTL check + */ +export async function initializeMempoolSystem(): Promise { + LOG.info("Initializing mempool system..."); + + // Initialize Mempool + initializeMempool(); + await mempool.initialize(); + + // Initialize Executor + executor = new Executor(); + executor.start(); + + // Initialize Verifier + verifier = new Verifier(); + verifier.start(); + + // Start periodic TTL check + ttlCheckIntervalId = setInterval(async () => { + try { + await mempool.expireBundles(); + } catch (error) { + LOG.error("Error during TTL check", { + error: error instanceof Error ? error.message : String(error), + }); + } + }, MEMPOOL_TTL_CHECK_INTERVAL_MS) as unknown as number; + + LOG.info("Mempool system initialized successfully"); +} + +/** + * Shuts down the mempool system gracefully + * Stops all services and clears intervals + */ +export function shutdownMempoolSystem(): void { + LOG.info("Shutting down mempool system..."); + + if (executor) { + executor.stop(); + } + + if (verifier) { + verifier.stop(); + } + + if (ttlCheckIntervalId !== null) { + clearInterval(ttlCheckIntervalId); + ttlCheckIntervalId = null; + } + + LOG.info("Mempool system shut down successfully"); +} diff --git a/src/main.ts b/src/main.ts index 9fc97cb..683cbd4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,18 +6,40 @@ import { appendRequestIdMiddleware } from "@/http/middleware/append-request-id.t import { appendResponseHeadersMiddleware } from "@/http/middleware/append-response-headers.ts"; import { PORT } from "@/config/env.ts"; import { LOG } from "@/config/logger.ts"; +import { initializeMempoolSystem, shutdownMempoolSystem } from "@/core/mempool/index.ts"; async function bootstrap() { - const app = new Application(); + try { + // Initialize mempool system before starting HTTP server + await initializeMempoolSystem(); - app.use(globalRateLimitMiddleware); - app.use(appendRequestIdMiddleware); - app.use(appendResponseHeadersMiddleware); - app.use(apiVi.routes()); + const app = new Application(); - LOG.info(`🚀 Executer Server running on http://localhost:${PORT}`); + app.use(globalRateLimitMiddleware); + app.use(appendRequestIdMiddleware); + app.use(appendResponseHeadersMiddleware); + app.use(apiVi.routes()); - await app.listen({ port: Number(PORT) }); + LOG.info(`🚀 Executer Server running on http://localhost:${PORT}`); + + // Setup graceful shutdown + const shutdown = () => { + LOG.info("Shutting down server..."); + shutdownMempoolSystem(); + Deno.exit(0); + }; + + Deno.addSignalListener("SIGINT", shutdown); + Deno.addSignalListener("SIGTERM", shutdown); + + await app.listen({ port: Number(PORT) }); + } catch (error) { + LOG.error("Failed to start server", { + error: error instanceof Error ? error.message : String(error), + }); + shutdownMempoolSystem(); + Deno.exit(1); + } } bootstrap(); diff --git a/src/persistence/drizzle/repository/operations-bundle.repository.ts b/src/persistence/drizzle/repository/operations-bundle.repository.ts index fae3f9c..6313e0d 100644 --- a/src/persistence/drizzle/repository/operations-bundle.repository.ts +++ b/src/persistence/drizzle/repository/operations-bundle.repository.ts @@ -4,7 +4,7 @@ import { operationsBundle, type OperationsBundle, type NewOperationsBundle, - type BundleStatus, + BundleStatus, } from "@/persistence/drizzle/entity/operations-bundle.entity.ts"; import { BaseRepository } from "@/persistence/drizzle/repository/base.repository.ts"; From bbbaad826f90c525df8733a53a83fef9d9c06aa9 Mon Sep 17 00:00:00 2001 From: Victor Hugo Date: Fri, 16 Jan 2026 18:53:04 -0300 Subject: [PATCH 3/4] fix: Fix the executor idempotency on slots processing --- deno.json | 4 +- deno.lock | 107 +++++++++++------- src/config/env.ts | 1 + src/config/network.ts | 4 +- src/core/service/executor/executor.process.ts | 77 ++++++++----- src/core/service/executor/executor.service.ts | 5 + src/core/service/mempool/mempool.process.ts | 21 ++++ src/core/service/verifier/verifier.service.ts | 5 +- 8 files changed, 146 insertions(+), 78 deletions(-) diff --git a/deno.json b/deno.json index 28aa0e7..58e11b3 100644 --- a/deno.json +++ b/deno.json @@ -19,10 +19,10 @@ "nodeModulesDir": "auto", "imports": { "@/": "./src/", - "@colibri/core": "jsr:@colibri/core@^0.6.0", + "@colibri/core": "jsr:@colibri/core@^0.15.0", "@drizzle-team/brocli": "npm:@drizzle-team/brocli@^0.11.0", "@fifo/convee": "jsr:@fifo/convee@^0.10.0", - "@moonlight/moonlight-sdk": "jsr:@moonlight/moonlight-sdk@^0.6.0", + "@moonlight/moonlight-sdk": "jsr:@moonlight/moonlight-sdk@^0.6.1", "@types/pg": "npm:@types/pg@^8.15.6", "@zaubrik/djwt": "jsr:@zaubrik/djwt@^3.0.2", "drizzle-kit": "npm:drizzle-kit@^0.31.6", diff --git a/deno.lock b/deno.lock index b311057..39b63c5 100644 --- a/deno.lock +++ b/deno.lock @@ -1,35 +1,38 @@ { "version": "5", "specifiers": { - "jsr:@colibri/core@0.6": "0.6.0", - "jsr:@colibri/core@~0.5.3": "0.5.3", + "jsr:@colibri/core@0.15": "0.15.0", "jsr:@fifo/convee@0.10": "0.10.0", - "jsr:@fifo/convee@0.9": "0.9.2", - "jsr:@moonlight/moonlight-sdk@0.6": "0.6.0", + "jsr:@fifo/convee@~0.9.2": "0.9.2", + "jsr:@moonlight/moonlight-sdk@~0.6.1": "0.6.1", "jsr:@noble/curves@^1.8.0": "1.9.0", "jsr:@noble/hashes@1.8.0": "1.8.0", "jsr:@noble/hashes@^1.6.1": "1.8.0", "jsr:@oak/commons@1": "1.0.1", "jsr:@oak/oak@^17.1.4": "17.2.0", - "jsr:@olli/kvdex@^3.1.4": "3.4.0", + "jsr:@olli/kvdex@^3.1.4": "3.4.2", "jsr:@std/assert@1": "1.0.16", "jsr:@std/bytes@1": "1.0.6", "jsr:@std/bytes@^1.0.4": "1.0.6", + "jsr:@std/cli@1.0.12": "1.0.12", "jsr:@std/collections@^1.0.9": "1.1.3", + "jsr:@std/collections@^1.1.3": "1.1.3", "jsr:@std/crypto@1": "1.0.5", "jsr:@std/encoding@0.224.0": "0.224.0", "jsr:@std/encoding@1": "1.0.10", "jsr:@std/encoding@^1.0.10": "1.0.10", - "jsr:@std/http@1": "1.0.22", + "jsr:@std/http@1": "1.0.23", "jsr:@std/internal@^1.0.12": "1.0.12", "jsr:@std/media-types@1": "1.1.0", - "jsr:@std/path@1": "1.1.3", + "jsr:@std/path@1": "1.1.4", + "jsr:@std/toml@^1.0.11": "1.0.11", "jsr:@std/ulid@1": "1.0.0", "jsr:@zaubrik/djwt@^3.0.2": "3.0.2", "npm:@drizzle-team/brocli@0.11": "0.11.0", - "npm:@stellar/stellar-sdk@14": "14.2.0", "npm:@stellar/stellar-sdk@14.2.0": "14.2.0", "npm:@stellar/stellar-sdk@^14.2.0": "14.2.0", + "npm:@stellar/stellar-sdk@^14.3.3": "14.4.3", + "npm:@types/node@*": "24.2.0", "npm:@types/pg@^8.15.6": "8.15.6", "npm:asn1js@3.0.5": "3.0.5", "npm:buffer@6.0.3": "6.0.3", @@ -41,24 +44,16 @@ "npm:path-to-regexp@^6.3.0": "6.3.0", "npm:pg@^8.16.3": "8.16.3", "npm:postgres@^3.4.7": "3.4.7", - "npm:zod@3.24.2": "3.24.2" + "npm:zod@3.24.2": "3.24.2", + "npm:zod@^3.24.0": "3.24.2" }, "jsr": { - "@colibri/core@0.5.3": { - "integrity": "1c236c37e067d557fe03e027a4bba2a82e8ada4a917eb28450e64fbf2a3c2dd9", + "@colibri/core@0.15.0": { + "integrity": "f5df213b5428a945e4436e9603282db2a9b79c4dc1530aa95f56f616c05e7d21", "dependencies": [ - "jsr:@colibri/core@~0.5.3", - "jsr:@fifo/convee@0.9", - "npm:@stellar/stellar-sdk@14", - "npm:buffer@^6.0.3" - ] - }, - "@colibri/core@0.6.0": { - "integrity": "9cbe232cf9d910659b74f5f44b76e470b5249d7d390ea535d903342530884edf", - "dependencies": [ - "jsr:@colibri/core@0.6", - "jsr:@fifo/convee@0.9", - "npm:@stellar/stellar-sdk@14", + "jsr:@fifo/convee@~0.9.2", + "jsr:@std/toml", + "npm:@stellar/stellar-sdk@^14.3.3", "npm:buffer@^6.0.3" ] }, @@ -68,10 +63,10 @@ "@fifo/convee@0.10.0": { "integrity": "d1e85f9d6a8288f2e267a80dc21ac45e899ecf33558c5dd596ceebb556253276" }, - "@moonlight/moonlight-sdk@0.6.0": { - "integrity": "538e77a7a9b1b8358182ca9c9607003be10ac7e8920d9e8049f17c4cef4edf7c", + "@moonlight/moonlight-sdk@0.6.1": { + "integrity": "a2d09c7be4e332f9ab15adf7c36b330782df3f9d4ff2f65b0de8f1b46eaef2f9", "dependencies": [ - "jsr:@colibri/core@~0.5.3", + "jsr:@colibri/core", "jsr:@noble/curves", "jsr:@noble/hashes@^1.6.1", "npm:@stellar/stellar-sdk@^14.2.0", @@ -111,12 +106,14 @@ "npm:path-to-regexp" ] }, - "@olli/kvdex@3.4.0": { - "integrity": "007b1dca50a20f7d036d186fb24875ce2eb19d9c0b3a32878edc1d0e8181aa9a", + "@olli/kvdex@3.4.2": { + "integrity": "ab830819efc3a6e7024f865e0743f381dc8b57f63f0a3e590fe1e24ef17847a7", "dependencies": [ "jsr:@std/bytes@^1.0.4", - "jsr:@std/collections", - "jsr:@std/ulid" + "jsr:@std/cli", + "jsr:@std/collections@^1.0.9", + "jsr:@std/ulid", + "npm:zod@^3.24.0" ] }, "@std/assert@1.0.16": { @@ -125,6 +122,9 @@ "@std/bytes@1.0.6": { "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" }, + "@std/cli@1.0.12": { + "integrity": "e5cfb7814d189da174ecd7a34fbbd63f3513e24a1b307feb2fcd5da47a070d90" + }, "@std/collections@1.1.3": { "integrity": "bf8b0818886df6a32b64c7d3b037a425111f28278d69fd0995aeb62777c986b0" }, @@ -137,8 +137,8 @@ "@std/encoding@1.0.10": { "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" }, - "@std/http@1.0.22": { - "integrity": "53f0bb70e23a2eec3e17c4240a85bb23d185b2e20635adb37ce0f03cc4ca012a", + "@std/http@1.0.23": { + "integrity": "6634e9e034c589bf35101c1b5ee5bbf052a5987abca20f903e58bdba85c80dee", "dependencies": [ "jsr:@std/encoding@^1.0.10" ] @@ -149,12 +149,18 @@ "@std/media-types@1.1.0": { "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" }, - "@std/path@1.1.3": { - "integrity": "b015962d82a5e6daea980c32b82d2c40142149639968549c649031a230b1afb3", + "@std/path@1.1.4": { + "integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5", "dependencies": [ "jsr:@std/internal" ] }, + "@std/toml@1.0.11": { + "integrity": "e084988b872ca4bad6aedfb7350f6eeed0e8ba88e9ee5e1590621c5b5bb8f715", + "dependencies": [ + "jsr:@std/collections@^1.1.3" + ] + }, "@std/ulid@1.0.0": { "integrity": "d41c3d27a907714413649fee864b7cde8d42ee68437d22b79d5de4f81d808780" }, @@ -440,8 +446,8 @@ "@stellar/js-xdr@3.1.2": { "integrity": "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==" }, - "@stellar/stellar-base@14.0.1": { - "integrity": "sha512-mI6Kjh9hGWDA1APawQTtCbR7702dNT/8Te1uuRFPqqdoAKBk3WpXOQI3ZSZO+5olW7BSHpmVG5KBPZpIpQxIvw==", + "@stellar/stellar-base@14.0.4": { + "integrity": "sha512-UbNW6zbdOBXJwLAV2mMak0bIC9nw3IZVlQXkv2w2dk1jgCbJjy3oRVC943zeGE5JAm0Z9PHxrIjmkpGhayY7kw==", "dependencies": [ "@noble/curves", "@stellar/js-xdr", @@ -465,6 +471,19 @@ ], "scripts": true }, + "@stellar/stellar-sdk@14.4.3": { + "integrity": "sha512-QfaScSNd4Ku0GGfaZjR8679+M5gLHG+09OLLqV3Bv1VaDKXjHmhf8ikalz2jlx3oFnmlEpEgnqXIdf4kdD2x/w==", + "dependencies": [ + "@stellar/stellar-base", + "axios", + "bignumber.js", + "eventsource", + "feaxios", + "randombytes", + "toml", + "urijs" + ] + }, "@types/node@24.2.0": { "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", "dependencies": [ @@ -496,8 +515,8 @@ "possible-typed-array-names" ] }, - "axios@1.12.2": { - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "axios@1.13.2": { + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "dependencies": [ "follow-redirects", "form-data", @@ -711,8 +730,8 @@ "is-callable" ] }, - "form-data@4.0.4": { - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "form-data@4.0.5": { + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dependencies": [ "asynckit", "combined-stream", @@ -892,8 +911,8 @@ "tslib" ] }, - "pvutils@1.1.3": { - "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==" + "pvutils@1.1.5": { + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==" }, "randombytes@2.1.0": { "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", @@ -994,9 +1013,9 @@ }, "workspace": { "dependencies": [ - "jsr:@colibri/core@0.6", + "jsr:@colibri/core@0.15", "jsr:@fifo/convee@0.10", - "jsr:@moonlight/moonlight-sdk@0.6", + "jsr:@moonlight/moonlight-sdk@~0.6.1", "jsr:@oak/oak@^17.1.4", "jsr:@olli/kvdex@^3.1.4", "jsr:@zaubrik/djwt@^3.0.2", diff --git a/src/config/env.ts b/src/config/env.ts index 7416f1f..686cdda 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -46,6 +46,7 @@ const NETWORK_FEE = requireBaseFee("NETWORK_FEE"); export const NETWORK_RPC_SERVER = new Server(NETWORK_CONFIG.rpcUrl as string); export const OPEX_SIGNER = LocalSigner.fromSecret(OPEX_SK); +export const PROVIDER_SIGNER = LocalSigner.fromSecret(PROVIDER_SK); export const TX_CONFIG: TransactionConfig = { source: OPEX_PK, diff --git a/src/config/network.ts b/src/config/network.ts index 5cea5c3..4d7b67a 100644 --- a/src/config/network.ts +++ b/src/config/network.ts @@ -1,4 +1,4 @@ -import { type ContractId, type NetworkConfig, TestNet } from "@colibri/core"; +import { NetworkConfig, NetworkProviders, type ContractId } from "@colibri/core"; import { StellarNetworkId } from "@moonlight/moonlight-sdk"; import * as E from "@/config/error.ts"; import { logAndThrow } from "@/utils/error/log-and-throw.ts"; @@ -13,7 +13,7 @@ export function selectNetwork(envNetwork: string): { switch (envNetwork) { case "testnet": return { - NETWORK_CONFIG: TestNet(), + NETWORK_CONFIG: NetworkProviders.Nodies.TestNet(), NETWORK: StellarNetworkId.Testnet, CHANNEL_ASSET: { code: requireEnv("CHANNEL_ASSET_CODE"), diff --git a/src/core/service/executor/executor.process.ts b/src/core/service/executor/executor.process.ts index 682a817..a626c81 100644 --- a/src/core/service/executor/executor.process.ts +++ b/src/core/service/executor/executor.process.ts @@ -5,10 +5,10 @@ import { TransactionStatus } from "@/persistence/drizzle/entity/transaction.enti import { getMempool } from "@/core/mempool/index.ts"; import { MEMPOOL_EXECUTOR_INTERVAL_MS } from "@/config/env.ts"; import { CHANNEL_CLIENT } from "@/core/channel-client/index.ts"; -import { TX_CONFIG, NETWORK_RPC_SERVER } from "@/config/env.ts"; +import { TX_CONFIG, NETWORK_RPC_SERVER, PROVIDER_SIGNER } from "@/config/env.ts"; import { ChannelInvokeMethods } from "@moonlight/moonlight-sdk"; import type { SIM_ERRORS } from "@colibri/core"; -import { buildTransactionFromSlot } from "./executor.service.ts"; +import { buildTransactionFromSlot } from "@/core/service/executor/executor.service.ts"; import type { MoonlightTransactionBuilder } from "@moonlight/moonlight-sdk"; import { OperationsBundleRepository, @@ -40,8 +40,8 @@ async function submitTransactionToNetwork( txBuilder: MoonlightTransactionBuilder, expiration: number ): Promise { - await txBuilder.signWithProvider(TX_CONFIG.signers[1], expiration); - + await txBuilder.signWithProvider(PROVIDER_SIGNER, expiration); + try { const { hash } = await CHANNEL_CLIENT.invokeRaw({ operationArgs: { @@ -124,6 +124,7 @@ async function handleExecutionFailure( export class Executor { private intervalId: number | null = null; private isRunning: boolean = false; + private isProcessing: boolean = false; /** * Starts the executor loop @@ -165,22 +166,42 @@ export class Executor { * Executes the next slot from the mempool */ async executeNext(): Promise { + // Check if already processing a slot + if (this.isProcessing) { + LOG.debug("Executor already processing a slot, skipping"); + return; + } + + const mempool = getMempool(); + let slot: ReturnType = null; + let bundleIds: string[] = []; + try { - const mempool = getMempool(); - const slot = mempool.getNextSlot(); + // Set processing lock + this.isProcessing = true; + + // Remove slot from mempool BEFORE processing to prevent concurrent execution + slot = mempool.removeFirstSlot(); if (!slot || slot.isEmpty()) { // No slots to process return; } + // Get bundle IDs before processing + bundleIds = slot.getBundles().map((b) => b.bundleId); + LOG.debug("Executing slot", { bundleCount: slot.getBundleCount(), - weight: slot.getTotalWeight() + weight: slot.getTotalWeight(), + bundleIds }); // Build transaction from slot - const { txBuilder, bundleIds } = await buildTransactionFromSlot(slot); + const { txBuilder, bundleIds: buildBundleIds } = await buildTransactionFromSlot(slot); + + // Use bundleIds from build result to ensure consistency + bundleIds = buildBundleIds; // Get transaction expiration const expiration = await getTransactionExpiration(); @@ -197,39 +218,41 @@ export class Executor { // Create transaction record and link bundles await createTransactionRecord(transactionHash, bundleIds); - // Remove slot from mempool (successfully processed) - mempool.removeFirstSlot(); - LOG.info("Slot executed successfully", { transactionHash, bundleCount: bundleIds.length }); } catch (error) { - const mempool = getMempool(); - const slot = mempool.getNextSlot(); + const errorMessage = error instanceof Error ? error.message : String(error); + LOG.error("Slot execution failed", { + error: errorMessage, + bundleIds + }); - if (slot && !slot.isEmpty()) { - const bundleIds = slot.getBundles().map((b) => b.bundleId); - - // Handle failure + // Handle failure: re-add bundles to mempool and update status + if (slot && !slot.isEmpty() && bundleIds.length > 0) { + // Re-add bundles to mempool for retry + const bundles = slot.getBundles(); + await mempool.reAddBundles(bundles); + + // Update bundle statuses to PENDING await handleExecutionFailure( - error instanceof Error ? error : new Error(String(error)), + error instanceof Error ? error : new Error(errorMessage), bundleIds ); - // Remove failed slot from mempool - mempool.removeFirstSlot(); - - LOG.error("Slot execution failed", { - error: error instanceof Error ? error.message : String(error), - bundleIds - }); + LOG.info("Bundles re-added to mempool for retry", { bundleIds }); } else { - LOG.error("Execution error with no slot", { - error: error instanceof Error ? error.message : String(error) + LOG.error("Execution error with no slot or bundles to re-add", { + error: errorMessage, + hasSlot: !!slot, + bundleCount: bundleIds.length }); } + } finally { + // Always release processing lock + this.isProcessing = false; } } } diff --git a/src/core/service/executor/executor.service.ts b/src/core/service/executor/executor.service.ts index 3c5ee65..d772d8a 100644 --- a/src/core/service/executor/executor.service.ts +++ b/src/core/service/executor/executor.service.ts @@ -4,6 +4,7 @@ import type { TransactionBuildResult } from "@/core/service/executor/executor.ty import { CHANNEL_CLIENT } from "@/core/channel-client/index.ts"; import { UtxoBasedStellarAccount, UTXOStatus } from "@moonlight/moonlight-sdk"; import { OPEX_SK } from "@/config/env.ts"; +import { LOG } from "../../../config/logger.ts"; const EXECUTOR_CONFIG = { OPEX_UTXO_BATCH_SIZE: 200, @@ -59,21 +60,25 @@ export async function buildTransactionFromSlot( for (const bundle of bundles) { // Add deposit operations bundle.operations.deposit.forEach((op) => { + LOG.info("Adding deposit operation", { mlxdr: op.toMLXDR() }); txBuilder.addOperation(op); }); // Add create operations bundle.operations.create.forEach((op) => { + LOG.info("Adding create operation", { mlxdr: op.toMLXDR() }); txBuilder.addOperation(op); }); // Add spend operations bundle.operations.spend.forEach((op) => { + LOG.info("Adding spend operation", { mlxdr: op.toMLXDR() }); txBuilder.addOperation(op); }); // Add withdraw operations bundle.operations.withdraw.forEach((op) => { + LOG.info("Adding withdraw operation", { mlxdr: op.toMLXDR() }); txBuilder.addOperation(op); }); } diff --git a/src/core/service/mempool/mempool.process.ts b/src/core/service/mempool/mempool.process.ts index ff665f9..c969d79 100644 --- a/src/core/service/mempool/mempool.process.ts +++ b/src/core/service/mempool/mempool.process.ts @@ -300,6 +300,27 @@ export class Mempool { return this.slots.shift() || null; } + /** + * Re-adds bundles to the mempool after execution failure + * Used to restore bundles that failed during execution + * + * @param bundles - Array of bundles to re-add + */ + async reAddBundles(bundles: SlotBundle[]): Promise { + LOG.debug(`Re-adding ${bundles.length} bundles to mempool after execution failure`); + + for (const bundle of bundles) { + try { + await this.addBundle(bundle); + LOG.debug(`Bundle ${bundle.bundleId} re-added to mempool`); + } catch (error) { + LOG.error(`Failed to re-add bundle ${bundle.bundleId}`, { + error: error instanceof Error ? error.message : String(error), + }); + } + } + } + /** * Expires bundles that have passed their TTL */ diff --git a/src/core/service/verifier/verifier.service.ts b/src/core/service/verifier/verifier.service.ts index 7f7d4b8..148cfa2 100644 --- a/src/core/service/verifier/verifier.service.ts +++ b/src/core/service/verifier/verifier.service.ts @@ -16,14 +16,13 @@ export async function verifyTransactionOnNetwork( try { // Try to get transaction by hash const txResponse = await rpcServer.getTransaction(txHash); - if (!txResponse) { // Transaction not found - might be pending or failed return { status: "PENDING" }; } // Check if transaction was successful - if (txResponse.successful === true) { + if (txResponse.status === "SUCCESS") { return { status: "VERIFIED", ledgerSequence: txResponse.ledger?.toString(), @@ -31,7 +30,7 @@ export async function verifyTransactionOnNetwork( } // Transaction was included but failed - if (txResponse.successful === false) { + if (txResponse.status === "FAILED") { const resultCode = txResponse.resultXdr || "unknown"; return { status: "FAILED", From 5a2e15f6b3b9e6cd53093624731054dd741af97e Mon Sep 17 00:00:00 2001 From: Victor Hugo Date: Tue, 17 Feb 2026 10:07:46 -0300 Subject: [PATCH 4/4] fix: Fix the spend amount calculation to get the value directly to the network --- src/core/service/bundle/add-bundle.process.ts | 40 ++++++++---- src/core/service/bundle/bundle.service.ts | 61 ++++++++++++++++--- src/core/service/executor/executor.process.ts | 2 + src/core/service/executor/executor.service.ts | 8 ++- src/core/service/mempool/mempool.process.ts | 2 +- 5 files changed, 92 insertions(+), 21 deletions(-) diff --git a/src/core/service/bundle/add-bundle.process.ts b/src/core/service/bundle/add-bundle.process.ts index ba19149..e2d80a7 100644 --- a/src/core/service/bundle/add-bundle.process.ts +++ b/src/core/service/bundle/add-bundle.process.ts @@ -16,9 +16,10 @@ import { calculateBundleTtl, calculateBundleWeight, calculatePriorityScore, + fetchUtxoBalances, } from "@/core/service/bundle/bundle.service.ts"; -import type { SlotBundle, WeightConfig } from "@/core/service/bundle/bundle.types.ts"; import { MEMPOOL_EXPENSIVE_OP_WEIGHT, MEMPOOL_CHEAP_OP_WEIGHT } from "@/config/env.ts"; +import type { SlotBundle, WeightConfig } from "@/core/service/bundle/bundle.types.ts"; import { getMempool } from "@/core/mempool/index.ts"; import * as E from "@/core/service/bundle/bundle.errors.ts"; import type { ClassifiedOperations } from "@/core/service/bundle/bundle.types.ts"; @@ -30,11 +31,6 @@ import { UtxoRepository, } from "@/persistence/drizzle/repository/index.ts"; -// Configuration constants -const BUNDLE_CONFIG = { - TTL_HOURS: 24, -} as const; - // Repositories const sessionRepository = new SessionRepository(drizzleClient); const utxoRepository = new UtxoRepository(drizzleClient); @@ -63,7 +59,7 @@ async function validateSession(sessionId: string) { * Validates that a bundle with the given ID does not exist, or if it does, ensures it is expired. * Throws an error if an active bundle exists. */ -async function assertBundleIsNotExpired(bundleId: string): Promise { +async function assertBundleIsExpired(bundleId: string): Promise { const existingBundle = await operationsBundleRepository.findById(bundleId); if (!existingBundle) @@ -131,22 +127,40 @@ async function persistCreateOperations( /** * Updates UTXOs in the database from spend operations + * + * Note: The spend amount is fetched directly from the network since + * SpendOperation intentionally does not have an amount attribute. */ async function persistSpendOperations( operations: OperationTypes.SpendOperation[], bundleId: string, accountId: string ): Promise { - for (const operation of operations) { - const utxoId = operation.getUtxo().toString(); + if (operations.length === 0) { + return; + } + + // Fetch all UTXO balances from the network in batch for better performance + const utxoPublicKeys = operations.map((op) => op.getUtxo()); + const balances = await fetchUtxoBalances(utxoPublicKeys); + + for (let i = 0; i < operations.length; i++) { + const operation = operations[i]; + const utxoPublicKey = operation.getUtxo(); + // Convert UTXO public key to base64 string to match the format used in persistCreateOperations + const utxoId = Buffer.from(utxoPublicKey).toString("base64"); + LOG.info(` /n/n utxo Id: ${utxoId} /n/n`); const utxo = await utxoRepository.findById(utxoId); if (!utxo) { logAndThrow(new E.UTXO_NOT_FOUND(utxoId)); } + + // The spend amount is the full balance of the UTXO (since we're spending it entirely) + const spendAmount = balances[i] || BigInt(0); await utxoRepository.update(utxo.id, { - amount: utxo.amount - operation.getAmount(), + amount: utxo.amount - spendAmount, updatedAt: new Date(), updatedBy: accountId, spentAtBundleId: bundleId, @@ -193,7 +207,7 @@ export const P_AddOperationsBundle = ProcessEngine.create( // 2. Bundle ID generation and validation const bundleId = await generateBundleId(operationsMLXDR); - const isBundleExpired = await assertBundleIsNotExpired(bundleId); + const isBundleExpired = await assertBundleIsExpired(bundleId); // 3. Parse and classify operations const operations = await parseOperations(operationsMLXDR); @@ -201,7 +215,9 @@ export const P_AddOperationsBundle = ProcessEngine.create( validateSpendOperations(classified.spend); // 4. Fee calculation - const amounts = calculateOperationAmounts(classified); + LOG.info("before calculateOperationAmounts: ", classified); + const amounts = await calculateOperationAmounts(classified); + LOG.info("amounts: ", amounts); const feeCalculation = calculateFee(amounts); // 5. Bundle update or creation diff --git a/src/core/service/bundle/bundle.service.ts b/src/core/service/bundle/bundle.service.ts index 90b8a2d..b9e6ad7 100644 --- a/src/core/service/bundle/bundle.service.ts +++ b/src/core/service/bundle/bundle.service.ts @@ -1,8 +1,9 @@ import { Buffer } from "buffer"; import type { MoonlightOperation } from "@moonlight/moonlight-sdk"; -import { sha256Hash, type OperationTypes } from "@moonlight/moonlight-sdk"; +import { sha256Hash, type OperationTypes, ChannelReadMethods, type UTXOPublicKey } from "@moonlight/moonlight-sdk"; import type { ClassifiedOperations, FeeCalculation, OperationAmounts } from "@/core/service/bundle/bundle.types.ts"; import type { OperationsBundle } from "@/persistence/drizzle/entity/operations-bundle.entity.ts"; +import { CHANNEL_CLIENT } from "@/core/channel-client/index.ts"; /** * Classifies operations by type @@ -21,6 +22,43 @@ export function classifyOperations( }; } +/** + * Fetches the balance of one or more UTXOs directly from the network + * + * @param utxoPublicKeys - Array of UTXO public keys + * @returns Array of balances corresponding to each UTXO (in bigint) + */ +export async function fetchUtxoBalances( + utxoPublicKeys: UTXOPublicKey[] +): Promise { + if (utxoPublicKeys.length === 0) { + return []; + } + + const result = await CHANNEL_CLIENT.read({ + method: ChannelReadMethods.utxo_balances, + methodArgs: { + utxos: utxoPublicKeys.map((u) => Buffer.from(u)), + }, + }); + + // The result is an array of balances, convert to bigint + return (result as Array).map((balance) => BigInt(balance)); +} + +/** + * Fetches the balance of a single UTXO + * + * @param utxoPublicKey - UTXO public key + * @returns Balance of the UTXO (in bigint) + */ +export async function fetchUtxoBalance( + utxoPublicKey: UTXOPublicKey +): Promise { + const balances = await fetchUtxoBalances([utxoPublicKey]); + return balances[0] || BigInt(0); +} + /** * Calculates the total of a list of operations (DRY) * @@ -38,21 +76,30 @@ export function calculateOperationsTotal( /** * Calculates the totals for each operation type * + * Note: For spend operations, the amount is fetched directly from the network + * since SpendOperation intentionally does not have an amount attribute. + * * @param classified - Classified operations * @returns Breakdown of amounts by operation type */ -export function calculateOperationAmounts( +export async function calculateOperationAmounts( classified: ClassifiedOperations -): OperationAmounts { +): Promise { + // Fetch spend operation amounts from the network + const spendUtxos = classified.spend.map((op) => op.getUtxo()); + const spendBalances = await fetchUtxoBalances(spendUtxos); + + const totalSpendAmount = spendBalances.reduce( + (acc, balance) => acc + balance, + BigInt(0) + ); + return { totalCreateAmount: classified.create.reduce( (acc, op) => acc + op.getAmount(), BigInt(0) ), - totalSpendAmount: classified.spend.reduce( - (acc, op) => acc + op.getAmount(), - BigInt(0) - ), + totalSpendAmount, totalDepositAmount: classified.deposit.reduce( (acc, op) => acc + op.getAmount(), BigInt(0) diff --git a/src/core/service/executor/executor.process.ts b/src/core/service/executor/executor.process.ts index a626c81..9818142 100644 --- a/src/core/service/executor/executor.process.ts +++ b/src/core/service/executor/executor.process.ts @@ -206,6 +206,8 @@ export class Executor { // Get transaction expiration const expiration = await getTransactionExpiration(); + LOG.info("Transaction", { txBuilder: txBuilder.buildXDR().toXDR() }); + // Submit transaction to network const transactionHash = await submitTransactionToNetwork(txBuilder, expiration); diff --git a/src/core/service/executor/executor.service.ts b/src/core/service/executor/executor.service.ts index d772d8a..c37a96f 100644 --- a/src/core/service/executor/executor.service.ts +++ b/src/core/service/executor/executor.service.ts @@ -47,7 +47,13 @@ export async function buildTransactionFromSlot( } // Calculate total fee from all bundles - const totalFee = bundles.reduce((sum, bundle) => sum + bundle.fee, BigInt(0)); + const totalFee = bundles.reduce((sum, bundle) => { + LOG.info("bundle: ", bundle.bundleId); + LOG.info("fee: ", bundle.fee.toString()); + return sum + bundle.fee; + }, BigInt(0)); + + LOG.info("Creating fee operation with total fee: ", totalFee.toString()); // Create fee operation const feeOperation = MoonlightOperation.create( diff --git a/src/core/service/mempool/mempool.process.ts b/src/core/service/mempool/mempool.process.ts index c969d79..523a123 100644 --- a/src/core/service/mempool/mempool.process.ts +++ b/src/core/service/mempool/mempool.process.ts @@ -80,7 +80,7 @@ async function createSlotBundleFromEntity( async function loadPendingBundlesFromDB(): Promise { const bundles = await operationsBundleRepository.findPendingOrProcessing(); const slotBundles = await Promise.all( - bundles.map((bundle) => createSlotBundleFromEntity(bundle)) + bundles.map((bundle: OperationsBundle) => createSlotBundleFromEntity(bundle)) ); return slotBundles; }