|
1 | 1 | import { address, networks, Block, Transaction, type Network } from "bitcoinjs-lib"; |
2 | 2 | import { BtcNet, type BlockQueueRecord, calculateConfirmations } from "@gonative-cc/lib/nbtc"; |
3 | 3 | import type { SuiNet } from "@gonative-cc/lib/nsui"; |
| 4 | +import { SUI_GRAPHQL_URLS } from "@gonative-cc/lib/nsui"; |
4 | 5 | import type { Service } from "@cloudflare/workers-types"; |
5 | 6 | import type { WorkerEntrypoint } from "cloudflare:workers"; |
6 | 7 | import type { SuiIndexerRpc } from "@gonative-cc/sui-indexer/rpc-interface"; |
7 | 8 | import { logError, logger } from "@gonative-cc/lib/logger"; |
8 | | - |
9 | 9 | import { OP_RETURN } from "./opcodes"; |
10 | 10 | import { BitcoinMerkleTree } from "./bitcoin-merkle-tree"; |
11 | 11 | import { SuiClient, type SuiClientI } from "./sui_client"; |
| 12 | +import { SuiGraphQLClient } from "./graphql-client"; |
12 | 13 | import type { |
13 | 14 | Deposit, |
14 | 15 | PendingTx, |
@@ -317,7 +318,7 @@ export class Indexer { |
317 | 318 | }); |
318 | 319 | let finalRecipient = suiRecipient; |
319 | 320 | if (!finalRecipient) { |
320 | | - finalRecipient = config.sui_fallback_address; |
| 321 | + finalRecipient = config.nbtc_fallback_addr; |
321 | 322 | } |
322 | 323 |
|
323 | 324 | deposits.push({ |
@@ -350,16 +351,85 @@ export class Indexer { |
350 | 351 | if (!finalizedTxs || finalizedTxs.length === 0) { |
351 | 352 | return; |
352 | 353 | } |
| 354 | + |
| 355 | + const txsToProcess = await this.filterAlreadyMinted(finalizedTxs); |
| 356 | + |
| 357 | + if (txsToProcess.length === 0) { |
| 358 | + logger.info({ msg: "No new deposits to process after front-run check" }); |
| 359 | + return; |
| 360 | + } |
| 361 | + |
353 | 362 | logger.info({ |
354 | 363 | msg: "Minting: Found deposits to process", |
355 | | - count: finalizedTxs.length, |
| 364 | + count: txsToProcess.length, |
356 | 365 | }); |
357 | 366 |
|
358 | | - const txsByBlock = this.groupTransactionsByBlock(finalizedTxs); |
| 367 | + const txsByBlock = this.groupTransactionsByBlock(txsToProcess); |
359 | 368 | const { batches } = await this.prepareMintBatches(txsByBlock); |
360 | 369 | await this.executeMintBatches(batches); |
361 | 370 | } |
362 | 371 |
|
| 372 | + // Filters out txs that have already been minted on-chain and updates the database (front-run detection). |
| 373 | + private async filterAlreadyMinted(finalizedTxs: FinalizedTxRow[]): Promise<FinalizedTxRow[]> { |
| 374 | + const txsBySetupId = new Map<number, FinalizedTxRow[]>(); |
| 375 | + for (const tx of finalizedTxs) { |
| 376 | + const list = txsBySetupId.get(tx.setup_id) || []; |
| 377 | + list.push(tx); |
| 378 | + txsBySetupId.set(tx.setup_id, list); |
| 379 | + } |
| 380 | + |
| 381 | + const txsToProcess: FinalizedTxRow[] = []; |
| 382 | + |
| 383 | + for (const [setupId, txs] of txsBySetupId) { |
| 384 | + try { |
| 385 | + const config = this.getPackageConfig(setupId); |
| 386 | + const suiClient = this.getSuiClient(config.sui_network); |
| 387 | + const tableId = await suiClient.getMintedTxsTableId(); |
| 388 | + |
| 389 | + const graphqlUrl = SUI_GRAPHQL_URLS[config.sui_network]; |
| 390 | + if (!graphqlUrl) { |
| 391 | + logger.warn({ msg: "No GraphQL URL for network", network: config.sui_network }); |
| 392 | + txsToProcess.push(...txs); |
| 393 | + continue; |
| 394 | + } |
| 395 | + |
| 396 | + const graphqlClient = new SuiGraphQLClient(graphqlUrl); |
| 397 | + const txIds = txs.map((t) => t.tx_id); |
| 398 | + const mintedTxIds = await graphqlClient.checkMintedStatus(tableId, txIds); |
| 399 | + |
| 400 | + for (const tx of txs) { |
| 401 | + if (mintedTxIds.has(tx.tx_id)) { |
| 402 | + logger.info({ |
| 403 | + msg: "Front-run detected: Transaction already minted", |
| 404 | + txId: tx.tx_id, |
| 405 | + }); |
| 406 | + await this.storage.batchUpdateNbtcMintTxs([ |
| 407 | + { |
| 408 | + txId: tx.tx_id, |
| 409 | + vout: tx.vout, |
| 410 | + status: MintTxStatus.Minted, |
| 411 | + }, |
| 412 | + ]); |
| 413 | + } else { |
| 414 | + txsToProcess.push(tx); |
| 415 | + } |
| 416 | + } |
| 417 | + } catch (e) { |
| 418 | + logError( |
| 419 | + { |
| 420 | + msg: "Error checking pre-mint status via GraphQL", |
| 421 | + method: "filterAlreadyMinted", |
| 422 | + setupId, |
| 423 | + }, |
| 424 | + e, |
| 425 | + ); |
| 426 | + txsToProcess.push(...txs); |
| 427 | + } |
| 428 | + } |
| 429 | + |
| 430 | + return txsToProcess; |
| 431 | + } |
| 432 | + |
363 | 433 | /** |
364 | 434 | * Groups a list of blockchain transactions (or any object containing a block_hash) by their block hash. |
365 | 435 | * This optimization allows fetching and parsing the block data once for all related transactions. |
@@ -742,12 +812,10 @@ export class Indexer { |
742 | 812 | msg: "Finalization: Updating reorged transactions", |
743 | 813 | count: reorgedTxIds.length, |
744 | 814 | }); |
745 | | - // This requires a new method in the Storage interface like: |
746 | | - // updateTxsStatus(txIds: string[], status: TxStatus): Promise<void> |
747 | 815 | await this.storage.updateNbtcTxsStatus(reorgedTxIds, MintTxStatus.Reorg); |
748 | 816 | } |
749 | 817 |
|
750 | | - // TODO: add a unit test for it so we make sure we do not finalize reorrged tx. |
| 818 | + // TODO: add a unit test for it so we make sure we do not finalize reorged tx. |
751 | 819 | const validPendingTxs = pendingTxs.filter((tx) => !reorgedTxIds.includes(tx.tx_id)); |
752 | 820 | const { activeTxIds, inactiveTxIds } = this.selectFinalizedNbtcTxs( |
753 | 821 | validPendingTxs, |
|
0 commit comments