diff --git a/.vscode/launch.json b/.vscode/launch.json index 759b86b..94f3abd 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,7 +4,7 @@ { "type": "node", "request": "launch", - "name": "Launch via NPM", + "name": "Debug e2e Test", "runtimeExecutable": "npm", "runtimeArgs": [ "run", diff --git a/e2e/indexer.e2e-spec.ts b/e2e/indexer.e2e-spec.ts index 7b773bd..6c9bffa 100644 --- a/e2e/indexer.e2e-spec.ts +++ b/e2e/indexer.e2e-spec.ts @@ -1,5 +1,5 @@ import { UTXO, WalletHelper, AddressType } from '@e2e/helpers/wallet.helper'; -import { transactionToEntity } from '@e2e/helpers/common.helper'; +import { btcToSats, transactionToEntity } from '@e2e/helpers/common.helper'; import { initialiseDep } from '@e2e/setup'; import { ApiHelper } from '@e2e/helpers/api.helper'; import { SilentBlocksService } from '@/silent-blocks/silent-blocks.service'; @@ -74,4 +74,62 @@ describe('Indexer', () => { expect(response.data).toEqual(silentBlock); }, ); + + it('Should exclude spent transactions when the "FilterSpent" flag is set to true', async () => { + const taprootOutput = walletHelper.generateAddresses( + 1, + AddressType.P2TR, + )[0]; + const outputs = walletHelper.generateAddresses(6, AddressType.P2WPKH); + const utxos: UTXO[] = []; + + for (const [index, output] of outputs.entries()) { + const utxo = await walletHelper.addFundToUTXO( + output, + 1, + AddressType.P2WPKH, + index, + ); + utxos.push(utxo); + } + + const { transaction, txid, blockHash } = + await walletHelper.craftAndSendTransaction( + utxos, + taprootOutput, + 5.999, + 0.001, + ); + + const utxo: UTXO = { + rawTx: transaction.toHex(), + txid, + vout: 0, + value: btcToSats(5.999), + addressType: AddressType.P2TR, + index: 0, + }; + + await walletHelper.craftAndSendTransaction( + [utxo], + taprootOutput, + 5.998, + 0.001, + ); + + await new Promise((resolve) => setTimeout(resolve, 30000)); + const response = await apiHelper.get( + `/silent-block/hash/${blockHash}?filterSpent=true`, + { + responseType: 'arraybuffer', + }, + ); + + const silentBlock = new SilentBlocksService( + {} as any, + {} as any, + ).encodeSilentBlock([]); + + expect(response.data).toEqual(silentBlock); + }); }); diff --git a/migrations/1736332353003-migrations.ts b/migrations/1736332353003-migrations.ts new file mode 100644 index 0000000..be84a7d --- /dev/null +++ b/migrations/1736332353003-migrations.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class Migrations1736332353003 implements MigrationInterface { + name = 'MIgrations1736332353003'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "transaction_output" ( + "id" integer PRIMARY KEY NOT NULL, + "pubKey" text NOT NULL, + "vout" integer NOT NULL, + "value" integer NOT NULL, + "isSpent" boolean NOT NULL DEFAULT false, + "transactionId" INTEGER NOT NULL, + CONSTRAINT "FK_transaction_transaction_output" FOREIGN KEY ("transactionId") REFERENCES "transaction"("id") ON DELETE CASCADE + ) + `); + await queryRunner.query(` + ALTER TABLE "transaction" DROP COLUMN "isSpent" + `); + await queryRunner.query(` + ALTER TABLE "transaction" DROP COLUMN "outputs" + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DROP TABLE "transaction_output" + `); + await queryRunner.query(` + ALTER TABLE "transaction" ADD COLUMN "isSpent" boolean NOT NULL + `); + await queryRunner.query(` + ALTER TABLE "transaction" ADD COLUMN "outputs" text NOT NULL + `); + } +} diff --git a/src/app.module.ts b/src/app.module.ts index ce8fafb..74f21e1 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -10,7 +10,6 @@ import { OperationStateModule } from '@/operation-state/operation-state.module'; import { ScheduleModule } from '@nestjs/schedule'; import { BlockProviderModule } from '@/block-data-providers/block-provider.module'; import { EventEmitterModule } from '@nestjs/event-emitter'; -import { TransactionOutputModule } from '@/transaction-output/transaction-output.module'; @Module({ imports: [ @@ -35,7 +34,6 @@ import { TransactionOutputModule } from '@/transaction-output/transaction-output SilentBlocksModule, OperationStateModule, BlockProviderModule, - TransactionOutputModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/indexer/indexer.service.ts b/src/indexer/indexer.service.ts index 7d42976..cd6eee6 100644 --- a/src/indexer/indexer.service.ts +++ b/src/indexer/indexer.service.ts @@ -1,5 +1,5 @@ import { Transaction } from '@/transactions/transaction.entity'; -import { TransactionOutput as TransactionOutputEntity } from '@/transaction-output/transaction-output.entity'; +import { TransactionOutput as TransactionOutputEntity } from '@/transactions/transaction-output.entity'; import { createTaggedHash, extractPubKeyFromScript } from '@/common/common'; import { publicKeyCombine, publicKeyTweakMul } from 'secp256k1'; import { Injectable } from '@nestjs/common'; @@ -40,8 +40,6 @@ export class IndexerService { transaction.scanTweak = scanTweak.toString('hex'); transaction.outputs = eligibleOutputs; - console.log(transaction); - await manager.save(Transaction, transaction); } } @@ -57,13 +55,17 @@ export class IndexerService { })), }); + if (outputs.length == 0) { + return; + } + // Mark each output as spent const spentOutputs = outputs.map((output) => ({ ...output, isSpent: true, })); - await manager.save(spentOutputs); + await manager.save(TransactionOutputEntity, spentOutputs); } public deriveOutputsAndComputeScanTweak( diff --git a/src/silent-blocks/silent-blocks.controller.ts b/src/silent-blocks/silent-blocks.controller.ts index 4b12625..a656bf6 100644 --- a/src/silent-blocks/silent-blocks.controller.ts +++ b/src/silent-blocks/silent-blocks.controller.ts @@ -25,15 +25,15 @@ export class SilentBlocksController { @Get('hash/:blockHash') async getSilentBlockByHash( @Param('blockHash') blockHash: string, - @Query('unspent') unspent = 'false', + @Query('filterSpent') filterSpentFlag = 'false', @Res() res: Response, ) { - // Convert unspent to boolean - const unspentFlag = unspent === 'true'; + // Convert to boolean + const filterSpent = filterSpentFlag === 'true'; const buffer = await this.silentBlocksService.getSilentBlockByHash( blockHash, - unspentFlag, + filterSpent, ); res.set({ diff --git a/src/silent-blocks/silent-blocks.service.ts b/src/silent-blocks/silent-blocks.service.ts index e949054..17611e3 100644 --- a/src/silent-blocks/silent-blocks.service.ts +++ b/src/silent-blocks/silent-blocks.service.ts @@ -62,22 +62,20 @@ export class SilentBlocksService { await this.transactionsService.getTransactionByBlockHeight( blockHeight, ); - console.log("outputs length", transactions[0]); + return this.encodeSilentBlock(transactions); } async getSilentBlockByHash( blockHash: string, - unspentFlag: boolean, + filterSpent: boolean, ): Promise { let transactions = await this.transactionsService.getTransactionByBlockHash(blockHash); - console.log("outputs length", transactions[0]); - - if (unspentFlag) { + if (filterSpent) { transactions = transactions.filter((transaction) => - transaction.outputs.every((output) => output.isSpent), + transaction.outputs.some((output) => !output.isSpent), ); } diff --git a/src/transaction-output/transaction-output.module.ts b/src/transaction-output/transaction-output.module.ts deleted file mode 100644 index fa54631..0000000 --- a/src/transaction-output/transaction-output.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { TransactionOutput } from '@/transaction-output/transaction-output.entity'; - -@Module({ - imports: [TypeOrmModule.forFeature([TransactionOutput])], -}) -export class TransactionOutputModule {} diff --git a/src/transaction-output/transaction-output.entity.ts b/src/transactions/transaction-output.entity.ts similarity index 100% rename from src/transaction-output/transaction-output.entity.ts rename to src/transactions/transaction-output.entity.ts diff --git a/src/transactions/transaction.controller.spec.ts b/src/transactions/transaction.controller.spec.ts index 7bfe6dd..3d6a9c6 100644 --- a/src/transactions/transaction.controller.spec.ts +++ b/src/transactions/transaction.controller.spec.ts @@ -17,9 +17,11 @@ const mockTransactions: Transaction[] = [ pubKey: '51203e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1', vout: 1, value: 100000, + id: 0, + isSpent: false, + transaction: new Transaction(), }, ], - isSpent: false, }, { id: '2', @@ -33,9 +35,11 @@ const mockTransactions: Transaction[] = [ pubKey: '51203e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1', vout: 2, value: 100000, + id: 0, + isSpent: false, + transaction: new Transaction(), }, ], - isSpent: true, }, { id: '3', @@ -49,9 +53,11 @@ const mockTransactions: Transaction[] = [ pubKey: '5120f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38', vout: 3, value: 100000, + id: 0, + isSpent: false, + transaction: new Transaction(), }, ], - isSpent: false, }, ]; diff --git a/src/transactions/transaction.entity.ts b/src/transactions/transaction.entity.ts index e937483..b133691 100644 --- a/src/transactions/transaction.entity.ts +++ b/src/transactions/transaction.entity.ts @@ -1,4 +1,4 @@ -import { TransactionOutput } from '@/transaction-output/transaction-output.entity'; +import { TransactionOutput } from '@/transactions/transaction-output.entity'; import { Column, Entity, OneToMany, PrimaryColumn } from 'typeorm'; @Entity() diff --git a/src/transactions/transactions.module.ts b/src/transactions/transactions.module.ts index 97c33a1..ee16fe1 100644 --- a/src/transactions/transactions.module.ts +++ b/src/transactions/transactions.module.ts @@ -1,11 +1,14 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { TransactionsService } from '@/transactions/transactions.service'; -import { Transaction } from '@/transactions/transaction.entity'; +import { + Transaction, + TransactionOutput, +} from '@/transactions/transaction.entity'; import { TransactionController } from '@/transactions/transactions.controller'; @Module({ - imports: [TypeOrmModule.forFeature([Transaction])], + imports: [TypeOrmModule.forFeature([Transaction, TransactionOutput])], controllers: [TransactionController], providers: [TransactionsService], exports: [TransactionsService], diff --git a/src/transactions/transactions.service.ts b/src/transactions/transactions.service.ts index a94bd88..fbcef5f 100644 --- a/src/transactions/transactions.service.ts +++ b/src/transactions/transactions.service.ts @@ -13,11 +13,17 @@ export class TransactionsService { async getTransactionByBlockHeight( blockHeight: number, ): Promise { - return this.transactionRepository.find({ where: { blockHeight } }); + return this.transactionRepository.find({ + where: { blockHeight }, + relations: { outputs: true }, + }); } async getTransactionByBlockHash(blockHash: string): Promise { - return this.transactionRepository.find({ where: { blockHash }, relations: { outputs: true} }); + return this.transactionRepository.find({ + where: { blockHash }, + relations: { outputs: true }, + }); } async saveTransaction(transaction: Transaction): Promise { diff --git a/~/.silent-pay-indexer/db/database.sqlite b/~/.silent-pay-indexer/db/database.sqlite new file mode 100644 index 0000000..50e8bee Binary files /dev/null and b/~/.silent-pay-indexer/db/database.sqlite differ