diff --git a/backend/.vercel/README.txt b/backend/.vercel/README.txt new file mode 100644 index 00000000..525d8ce8 --- /dev/null +++ b/backend/.vercel/README.txt @@ -0,0 +1,11 @@ +> Why do I have a folder named ".vercel" in my project? +The ".vercel" folder is created when you link a directory to a Vercel project. + +> What does the "project.json" file contain? +The "project.json" file contains: +- The ID of the Vercel project that you linked ("projectId") +- The ID of the user or team your Vercel project is owned by ("orgId") + +> Should I commit the ".vercel" folder? +No, you should not share the ".vercel" folder with anyone. +Upon creation, it will be automatically added to your ".gitignore" file. diff --git a/backend/.vercel/cache/node/src/main.ts/tsconfig-with-tsconfig-json.json b/backend/.vercel/cache/node/src/main.ts/tsconfig-with-tsconfig-json.json new file mode 100644 index 00000000..09d1f943 --- /dev/null +++ b/backend/.vercel/cache/node/src/main.ts/tsconfig-with-tsconfig-json.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../../../tsconfig.json", + "include": [ + "../../../../../src/main.ts" + ] +} \ No newline at end of file diff --git a/backend/.vercel/project.json b/backend/.vercel/project.json new file mode 100644 index 00000000..85959366 --- /dev/null +++ b/backend/.vercel/project.json @@ -0,0 +1 @@ +{"projectId":"prj_XBfS7RIGQLWKOZBjUV6VpbXY9qkT","orgId":"team_BHVHlLGzAXdFN0HU9HDLZzOh"} \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index a5c66a85..3ecfc042 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@apollo/server": "^4.10.4", + "@applifting-io/nestjs-dataloader": "^1.1.5", "@nestjs/apollo": "^12.2.0", "@nestjs/cache-manager": "^2.2.2", "@nestjs/common": "^10.0.0", @@ -35,6 +36,7 @@ "@types/ws": "^8.5.11", "axios": "^1.7.2", "cache-manager": "^5.7.1", + "dataloader": "^2.2.2", "graphql": "^16.9.0", "lodash": "^4.17.21", "reflect-metadata": "^0.2.0", diff --git a/backend/src/modules/api.module.ts b/backend/src/modules/api.module.ts index 3ee3f1a8..7ce043d7 100644 --- a/backend/src/modules/api.module.ts +++ b/backend/src/modules/api.module.ts @@ -7,6 +7,8 @@ import { BlockchainModule } from './blockchain/blockchain.module'; import { ConfigService } from '@nestjs/config'; import { SentryService } from '@ntegral/nestjs-sentry'; import { CellModule } from './cell/cell.module'; +import { APP_INTERCEPTOR } from '@nestjs/core'; +import { DataLoaderInterceptor } from '@applifting-io/nestjs-dataloader'; @Module({ imports: [ @@ -36,5 +38,11 @@ import { CellModule } from './cell/cell.module'; TransactionModule, CellModule, ], + providers: [ + { + provide: APP_INTERCEPTOR, + useClass: DataLoaderInterceptor, + }, + ], }) export class ApiModule { } diff --git a/backend/src/modules/block/block.dataloader.ts b/backend/src/modules/block/block.dataloader.ts new file mode 100644 index 00000000..cf43aa94 --- /dev/null +++ b/backend/src/modules/block/block.dataloader.ts @@ -0,0 +1,36 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { NestDataLoader } from '@applifting-io/nestjs-dataloader'; +import { BaseTransaction } from '../transaction/transaction.model'; +import { BlockService } from './block.service'; +import { BaseBlock } from './block.model'; + +@Injectable() +export class BlockLoader implements NestDataLoader { + private logger = new Logger(BlockLoader.name); + + constructor(private readonly blockService: BlockService) { } + + public getBatchFunction() { + return (heightOrHashList: string[]) => { + this.logger.debug(`Loading blocks: ${heightOrHashList.join(', ')}`); + return Promise.all( + heightOrHashList.map((heightOrHash) => this.blockService.getBlock(heightOrHash)), + ); + }; + } +} + +@Injectable() +export class BlockTransactionsLoader + implements NestDataLoader { + private logger = new Logger(BlockTransactionsLoader.name); + + constructor(private readonly blockService: BlockService) { } + + public getBatchFunction() { + return (hashs: string[]) => { + this.logger.debug(`Loading transactions for blocks: ${hashs.join(', ')}`); + return Promise.all(hashs.map((key) => this.blockService.getBlockTransactions(key))); + }; + } +} diff --git a/backend/src/modules/block/block.model.ts b/backend/src/modules/block/block.model.ts index 8334a728..cf5a8905 100644 --- a/backend/src/modules/block/block.model.ts +++ b/backend/src/modules/block/block.model.ts @@ -3,7 +3,7 @@ import * as CKBExplorer from 'src/core/ckb-explorer/ckb-explorer.interface'; import { toNumber } from 'lodash'; import { Transaction } from 'src/modules/transaction/transaction.model'; -export type BlockWithoutResolveFields = Omit; +export type BaseBlock = Omit; @ObjectType({ description: 'block' }) export class Block { @@ -37,7 +37,7 @@ export class Block { @Field(() => [Transaction]) transactions: Transaction[]; - public static fromCKBExplorer(block: CKBExplorer.Block): BlockWithoutResolveFields { + public static fromCKBExplorer(block: CKBExplorer.Block): BaseBlock { return { version: toNumber(block.version), hash: block.block_hash, diff --git a/backend/src/modules/block/block.module.ts b/backend/src/modules/block/block.module.ts index 78d49332..02a3c77b 100644 --- a/backend/src/modules/block/block.module.ts +++ b/backend/src/modules/block/block.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { BlockResolver } from './block.resolver'; import { BlockService } from './block.service'; import { CKBExplorerModule } from 'src/core/ckb-explorer/ckb-explorer.module'; +import { BlockLoader, BlockTransactionsLoader } from './block.dataloader'; @Module({ imports: [CKBExplorerModule], - providers: [BlockResolver, BlockService], + providers: [BlockResolver, BlockService, BlockTransactionsLoader, BlockLoader], }) export class BlockModule { } diff --git a/backend/src/modules/block/block.resolver.ts b/backend/src/modules/block/block.resolver.ts index 6cf0f699..c68facb4 100644 --- a/backend/src/modules/block/block.resolver.ts +++ b/backend/src/modules/block/block.resolver.ts @@ -1,27 +1,42 @@ +import DataLoader from 'dataloader'; import { Float, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'; -import { Block, BlockWithoutResolveFields } from './block.model'; +import { Block, BaseBlock } from './block.model'; import { BlockService } from './block.service'; -import { Transaction } from '../transaction/transaction.model'; +import { BaseTransaction } from '../transaction/transaction.model'; +import { BlockLoader, BlockTransactionsLoader } from './block.dataloader'; +import { Loader } from '@applifting-io/nestjs-dataloader'; @Resolver(() => Block) export class BlockResolver { constructor(private blockService: BlockService) { } @Query(() => [Block]) - public async latestBlocks(): Promise { - const blocks = await this.blockService.getLatestBlocks(); + public async latestBlocks( + @Loader(BlockLoader) + blockLoader: DataLoader, + ): Promise { + const blockList = await this.blockService.getLatestBlockNumbers(); + const blocks = await Promise.all(blockList.map((block) => blockLoader.load(block))); return blocks; } @Query(() => Block) - public async block(heightOrHash: string): Promise { - const block = await this.blockService.getBlock(heightOrHash); + public async block( + heightOrHash: string, + @Loader(BlockLoader) + blockLoader: DataLoader, + ): Promise { + const block = await blockLoader.load(heightOrHash); return block; } @ResolveField(() => Float) - public async minFee(@Parent() block: Block): Promise { - const transactions = await this.blockService.getBlockTransactions(block.hash); + public async minFee( + @Parent() block: Block, + @Loader(BlockTransactionsLoader) + blockTransactionsLoader: DataLoader, + ): Promise { + const transactions = await blockTransactionsLoader.load(block.hash); const nonCellbaseTransactions = transactions.filter((tx) => !tx.isCellbase); if (nonCellbaseTransactions.length === 0) { return 0; @@ -30,8 +45,12 @@ export class BlockResolver { } @ResolveField(() => Float) - public async maxFee(@Parent() block: Block): Promise { - const transactions = await this.blockService.getBlockTransactions(block.hash); + public async maxFee( + @Parent() block: Block, + @Loader(BlockTransactionsLoader) + blockTransactionsLoader: DataLoader, + ): Promise { + const transactions = await blockTransactionsLoader.load(block.hash); const nonCellbaseTransactions = transactions.filter((tx) => !tx.isCellbase); if (nonCellbaseTransactions.length === 0) { return 0; @@ -40,8 +59,12 @@ export class BlockResolver { } @ResolveField(() => [String]) - public async transactions(@Parent() block: Block): Promise { - const transactions = await this.blockService.getBlockTransactions(block.hash); + public async transactions( + @Parent() block: Block, + @Loader(BlockTransactionsLoader) + blockTransactionsLoader: DataLoader, + ): Promise { + const transactions = await blockTransactionsLoader.load(block.hash); return transactions; } } diff --git a/backend/src/modules/block/block.service.ts b/backend/src/modules/block/block.service.ts index 0522190b..c8633b54 100644 --- a/backend/src/modules/block/block.service.ts +++ b/backend/src/modules/block/block.service.ts @@ -1,28 +1,23 @@ import { Injectable } from '@nestjs/common'; import { CKBExplorerService } from 'src/core/ckb-explorer/ckb-explorer.service'; -import { Block, BlockWithoutResolveFields } from './block.model'; -import { Transaction, TransactionWithoutResolveFields } from '../transaction/transaction.model'; +import { Block, BaseBlock } from './block.model'; +import { Transaction, BaseTransaction } from '../transaction/transaction.model'; @Injectable() export class BlockService { constructor(private ckbExplorerService: CKBExplorerService) { } - public async getLatestBlocks(): Promise { + public async getLatestBlockNumbers(): Promise { const blockList = await this.ckbExplorerService.getBlockList(); - const blocks = await Promise.all( - blockList.data.map(async (block) => { - return this.ckbExplorerService.getBlock(block.attributes.number); - }), - ); - return blocks.map((block) => Block.fromCKBExplorer(block.data.attributes)); + return blockList.data.map((block) => block.attributes.number); } - public async getBlock(heightOrHash: string): Promise { + public async getBlock(heightOrHash: string): Promise { const block = await this.ckbExplorerService.getBlock(heightOrHash); return Block.fromCKBExplorer(block.data.attributes); } - public async getBlockTransactions(blockHash: string): Promise { + public async getBlockTransactions(blockHash: string): Promise { const blockTransactions = await this.ckbExplorerService.getBlockTransactions(blockHash); return blockTransactions.data.map((transaction) => Transaction.fromCKBExplorer(transaction.attributes), diff --git a/backend/src/modules/cell/cell.model.ts b/backend/src/modules/cell/cell.model.ts index 83b3fce0..e8ad333d 100644 --- a/backend/src/modules/cell/cell.model.ts +++ b/backend/src/modules/cell/cell.model.ts @@ -2,7 +2,7 @@ import { Field, Float, Int, ObjectType } from '@nestjs/graphql'; import { toNumber } from 'lodash'; import * as CKBExplorer from 'src/core/ckb-explorer/ckb-explorer.interface'; -export type CellWithoutResolveFields = Pick; +export type BaseCell = Pick; @ObjectType({ description: 'cell' }) export class Cell { @@ -20,7 +20,7 @@ export class Cell { public static fromCKBExplorer( input: CKBExplorer.DisplayInput | CKBExplorer.DisplayOutput, index: number, - ): CellWithoutResolveFields { + ): BaseCell { return { index, txHash: input.generated_tx_hash, diff --git a/backend/src/modules/transaction/transaction.model.ts b/backend/src/modules/transaction/transaction.model.ts index 2b7ee780..88689c52 100644 --- a/backend/src/modules/transaction/transaction.model.ts +++ b/backend/src/modules/transaction/transaction.model.ts @@ -1,10 +1,11 @@ import { Field, Float, Int, ObjectType } from '@nestjs/graphql'; import { toNumber } from 'lodash'; import * as CKBExplorer from 'src/core/ckb-explorer/ckb-explorer.interface'; -import { Cell, CellWithoutResolveFields } from '../cell/cell.model'; +import { Cell, BaseCell } from '../cell/cell.model'; +import { Block } from '../block/block.model'; -export type TransactionWithoutResolveFields = Transaction & { - inputs: CellWithoutResolveFields[]; +export type BaseTransaction = Omit & { + inputs: BaseCell[]; }; @ObjectType({ description: 'transaction' }) @@ -12,6 +13,9 @@ export class Transaction { @Field(() => Boolean) isCellbase: boolean; + @Field(() => Int) + blockNumber: number; + @Field(() => String) hash: string; @@ -33,7 +37,10 @@ export class Transaction { @Field(() => [Cell]) outputs: Cell[]; - public static fromCKBExplorer(tx: CKBExplorer.Transaction): TransactionWithoutResolveFields { + @Field(() => Block) + block: Block; + + public static fromCKBExplorer(tx: CKBExplorer.Transaction): BaseTransaction { const outputSum = tx.display_outputs.reduce( (sum, output) => sum + toNumber(output.capacity), 0, @@ -43,6 +50,7 @@ export class Transaction { return { isCellbase: tx.is_cellbase, + blockNumber: toNumber(tx.block_number), hash: tx.transaction_hash, index: toNumber(tx.block_number), timestamp: new Date(toNumber(tx.block_timestamp)), diff --git a/backend/src/modules/transaction/transaction.resolver.ts b/backend/src/modules/transaction/transaction.resolver.ts index 9099713c..561aee04 100644 --- a/backend/src/modules/transaction/transaction.resolver.ts +++ b/backend/src/modules/transaction/transaction.resolver.ts @@ -1,5 +1,19 @@ -import { Resolver } from '@nestjs/graphql'; -import { Transaction } from './transaction.model'; +import DataLoader from 'dataloader'; +import { Parent, ResolveField, Resolver } from '@nestjs/graphql'; +import { Transaction, BaseTransaction } from './transaction.model'; +import { Block, BaseBlock } from '../block/block.model'; +import { Loader } from '@applifting-io/nestjs-dataloader'; +import { BlockLoader } from '../block/block.dataloader'; @Resolver(() => Transaction) -export class TransactionResolver {} +export class TransactionResolver { + @ResolveField(() => Block) + public async block( + @Parent() transaction: BaseTransaction, + @Loader(BlockLoader) + blockLoader: DataLoader, + ): Promise { + const block = await blockLoader.load(transaction.blockNumber.toString()); + return block; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53befff4..db860522 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,9 @@ importers: '@apollo/server': specifier: ^4.10.4 version: 4.10.4(graphql@16.9.0) + '@applifting-io/nestjs-dataloader': + specifier: ^1.1.5 + version: 1.1.5 '@nestjs/apollo': specifier: ^12.2.0 version: 12.2.0(@apollo/server@4.10.4(graphql@16.9.0))(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/graphql@12.2.0(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1))(graphql@16.9.0)(reflect-metadata@0.2.2))(graphql@16.9.0) @@ -55,6 +58,9 @@ importers: cache-manager: specifier: ^5.7.1 version: 5.7.1 + dataloader: + specifier: ^2.2.2 + version: 2.2.2 graphql: specifier: ^16.9.0 version: 16.9.0 @@ -358,6 +364,9 @@ packages: '@apollographql/graphql-playground-html@1.6.29': resolution: {integrity: sha512-xCcXpoz52rI4ksJSdOCxeOCn2DLocxwHf9dVT/Q90Pte1LX+LY+91SFtJF3KXVHH8kEin+g1KKCQPKBjZJfWNA==} + '@applifting-io/nestjs-dataloader@1.1.5': + resolution: {integrity: sha512-OUJd0Jyo+0LLiVkmcuxd6ld5pn9vIlfgHbWLF9FhBeuNjdYcvo32enMFNEXTWNW5MOwpMHwrDyr9E87Zkp6B5w==} + '@ark-ui/anatomy@3.4.0': resolution: {integrity: sha512-yRqNfI12UiQ3PliVvE8j81vLX4Nhhk07BWK6NLY6uaDUzMUjcv219zdPDxMnF6Pb9kG9Kxpz1opwsbe4LNB3wQ==} @@ -3206,6 +3215,9 @@ packages: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} + dataloader@2.2.2: + resolution: {integrity: sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==} + date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} @@ -6230,6 +6242,8 @@ snapshots: dependencies: xss: 1.0.15 + '@applifting-io/nestjs-dataloader@1.1.5': {} + '@ark-ui/anatomy@3.4.0(@internationalized/date@3.5.4)': dependencies: '@zag-js/accordion': 0.58.2 @@ -10227,6 +10241,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 + dataloader@2.2.2: {} + date-fns@3.6.0: {} debug@2.6.9: