Skip to content

Commit

Permalink
feat: add block dataloader and rename types to improve readability
Browse files Browse the repository at this point in the history
  • Loading branch information
ahonn committed Jul 16, 2024
1 parent d674083 commit a3180d5
Show file tree
Hide file tree
Showing 14 changed files with 156 additions and 35 deletions.
11 changes: 11 additions & 0 deletions backend/.vercel/README.txt
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../../../../../tsconfig.json",
"include": [
"../../../../../src/main.ts"
]
}
1 change: 1 addition & 0 deletions backend/.vercel/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"projectId":"prj_XBfS7RIGQLWKOZBjUV6VpbXY9qkT","orgId":"team_BHVHlLGzAXdFN0HU9HDLZzOh"}
2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions backend/src/modules/api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -36,5 +38,11 @@ import { CellModule } from './cell/cell.module';
TransactionModule,
CellModule,
],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: DataLoaderInterceptor,
},
],
})
export class ApiModule { }
36 changes: 36 additions & 0 deletions backend/src/modules/block/block.dataloader.ts
Original file line number Diff line number Diff line change
@@ -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<string, BaseBlock> {
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<string, BaseTransaction[]> {
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)));
};
}
}
4 changes: 2 additions & 2 deletions backend/src/modules/block/block.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Block, 'minFee' | 'maxFee' | 'transactions'>;
export type BaseBlock = Omit<Block, 'minFee' | 'maxFee' | 'transactions'>;

@ObjectType({ description: 'block' })
export class Block {
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion backend/src/modules/block/block.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { }
47 changes: 35 additions & 12 deletions backend/src/modules/block/block.resolver.ts
Original file line number Diff line number Diff line change
@@ -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<BlockWithoutResolveFields[]> {
const blocks = await this.blockService.getLatestBlocks();
public async latestBlocks(
@Loader(BlockLoader)
blockLoader: DataLoader<string, BaseBlock>,
): Promise<BaseBlock[]> {
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<BlockWithoutResolveFields> {
const block = await this.blockService.getBlock(heightOrHash);
public async block(
heightOrHash: string,
@Loader(BlockLoader)
blockLoader: DataLoader<string, BaseBlock>,
): Promise<BaseBlock> {
const block = await blockLoader.load(heightOrHash);
return block;
}

@ResolveField(() => Float)
public async minFee(@Parent() block: Block): Promise<number> {
const transactions = await this.blockService.getBlockTransactions(block.hash);
public async minFee(
@Parent() block: Block,
@Loader(BlockTransactionsLoader)
blockTransactionsLoader: DataLoader<string, BaseTransaction[]>,
): Promise<number> {
const transactions = await blockTransactionsLoader.load(block.hash);
const nonCellbaseTransactions = transactions.filter((tx) => !tx.isCellbase);
if (nonCellbaseTransactions.length === 0) {
return 0;
Expand All @@ -30,8 +45,12 @@ export class BlockResolver {
}

@ResolveField(() => Float)
public async maxFee(@Parent() block: Block): Promise<number> {
const transactions = await this.blockService.getBlockTransactions(block.hash);
public async maxFee(
@Parent() block: Block,
@Loader(BlockTransactionsLoader)
blockTransactionsLoader: DataLoader<string, BaseTransaction[]>,
): Promise<number> {
const transactions = await blockTransactionsLoader.load(block.hash);
const nonCellbaseTransactions = transactions.filter((tx) => !tx.isCellbase);
if (nonCellbaseTransactions.length === 0) {
return 0;
Expand All @@ -40,8 +59,12 @@ export class BlockResolver {
}

@ResolveField(() => [String])
public async transactions(@Parent() block: Block): Promise<Transaction[]> {
const transactions = await this.blockService.getBlockTransactions(block.hash);
public async transactions(
@Parent() block: Block,
@Loader(BlockTransactionsLoader)
blockTransactionsLoader: DataLoader<string, BaseTransaction[]>,
): Promise<BaseTransaction[]> {
const transactions = await blockTransactionsLoader.load(block.hash);
return transactions;
}
}
17 changes: 6 additions & 11 deletions backend/src/modules/block/block.service.ts
Original file line number Diff line number Diff line change
@@ -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<BlockWithoutResolveFields[]> {
public async getLatestBlockNumbers(): Promise<string[]> {
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<BlockWithoutResolveFields> {
public async getBlock(heightOrHash: string): Promise<BaseBlock> {
const block = await this.ckbExplorerService.getBlock(heightOrHash);
return Block.fromCKBExplorer(block.data.attributes);
}

public async getBlockTransactions(blockHash: string): Promise<TransactionWithoutResolveFields[]> {
public async getBlockTransactions(blockHash: string): Promise<BaseTransaction[]> {
const blockTransactions = await this.ckbExplorerService.getBlockTransactions(blockHash);
return blockTransactions.data.map((transaction) =>
Transaction.fromCKBExplorer(transaction.attributes),
Expand Down
4 changes: 2 additions & 2 deletions backend/src/modules/cell/cell.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Cell, 'txHash' | 'index' | 'capacity'>;
export type BaseCell = Pick<Cell, 'txHash' | 'index' | 'capacity'>;

@ObjectType({ description: 'cell' })
export class Cell {
Expand All @@ -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,
Expand Down
16 changes: 12 additions & 4 deletions backend/src/modules/transaction/transaction.model.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
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<Transaction, 'block'> & {
inputs: BaseCell[];
};

@ObjectType({ description: 'transaction' })
export class Transaction {
@Field(() => Boolean)
isCellbase: boolean;

@Field(() => Int)
blockNumber: number;

@Field(() => String)
hash: string;

Expand All @@ -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,
Expand All @@ -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)),
Expand Down
20 changes: 17 additions & 3 deletions backend/src/modules/transaction/transaction.resolver.ts
Original file line number Diff line number Diff line change
@@ -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<string, BaseBlock>,
): Promise<BaseBlock> {
const block = await blockLoader.load(transaction.blockNumber.toString());
return block;
}
}
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit a3180d5

Please sign in to comment.