diff --git a/Dockerfile b/Dockerfile index d9089614..f0ceb3cd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ ARG NODE_VERSION=20 -FROM node:${NODE_VERSION}-slim AS base +FROM node:${NODE_VERSION} AS base ARG GIT_BRANCH ENV PNPM_HOME="/pnpm" @@ -15,7 +15,7 @@ FROM base AS prod-deps RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile --ignore-scripts RUN pnpm run --filter backend postinstall -FROM base as build +FROM base AS build RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile RUN pnpm run --filter backend build diff --git a/backend/prisma/migrations/20240916084317_transaction/migration.sql b/backend/prisma/migrations/20240916084317_transaction/migration.sql new file mode 100644 index 00000000..2329c108 --- /dev/null +++ b/backend/prisma/migrations/20240916084317_transaction/migration.sql @@ -0,0 +1,35 @@ +/* + Warnings: + + - You are about to drop the column `difficulty` on the `Block` table. All the data in the column will be lost. + - You are about to drop the column `maxFee` on the `Block` table. All the data in the column will be lost. + - You are about to drop the column `minFee` on the `Block` table. All the data in the column will be lost. + - You are about to drop the column `size` on the `Block` table. All the data in the column will be lost. + - You are about to drop the column `totalFee` on the `Block` table. All the data in the column will be lost. + - You are about to drop the column `fee` on the `Transaction` table. All the data in the column will be lost. + - You are about to drop the column `size` on the `Transaction` table. All the data in the column will be lost. + - You are about to drop the column `timestamp` on the `Transaction` table. All the data in the column will be lost. + - You are about to drop the `Output` table. If the table is not empty, all the data it contains will be lost. + - Changed the type of `index` on the `Transaction` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + +*/ +-- DropForeignKey +ALTER TABLE "Output" DROP CONSTRAINT "Output_chainId_txHash_fkey"; + +-- AlterTable +ALTER TABLE "Block" DROP COLUMN "difficulty", +DROP COLUMN "maxFee", +DROP COLUMN "minFee", +DROP COLUMN "size", +DROP COLUMN "totalFee"; + +-- AlterTable +ALTER TABLE "Transaction" DROP COLUMN "fee", +DROP COLUMN "size", +DROP COLUMN "timestamp", +ADD COLUMN "btcTxid" TEXT, +DROP COLUMN "index", +ADD COLUMN "index" INTEGER NOT NULL; + +-- DropTable +DROP TABLE "Output"; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 316b5ab1..27c87a00 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -39,11 +39,6 @@ model Block { number Int timestamp DateTime transactionsCount Int - size Int - totalFee BigInt - minFee BigInt - maxFee BigInt - difficulty BigInt createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -58,13 +53,11 @@ model Transaction { id Int @id @default(autoincrement()) chainId Int hash String - index String + index Int blockNumber Int - timestamp DateTime - fee BigInt - size Int isCellbase Boolean @default(false) isRgbpp Boolean @default(false) + btcTxid String? leapDirection LeapDirection? inputCount Int outputCount Int @@ -73,33 +66,10 @@ model Transaction { chain Chain @relation(fields: [chainId], references: [id]) block Block @relation(fields: [chainId, blockNumber], references: [chainId, number]) - outputs Output[] @@unique([chainId, hash]) } -model Output { - id Int @id @default(autoincrement()) - chainId Int - txHash String - index String - consumedByTxHash String? - consumedByIndex String? - capacity BigInt - lockScriptHash String @db.Char(66) - typeScriptHash String? @db.Char(66) - isLive Boolean @default(true) - rgbppBound Boolean @default(false) - boundBtcTxId String? - boundBtcTxIndex Int? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - transaction Transaction @relation(fields: [chainId, txHash], references: [chainId, hash]) - - @@unique([chainId, txHash, index]) -} - model LockScript { id Int @id @default(autoincrement()) chainId Int diff --git a/backend/src/bootstrap.service.ts b/backend/src/bootstrap.service.ts index 67d196fa..7a01c186 100644 --- a/backend/src/bootstrap.service.ts +++ b/backend/src/bootstrap.service.ts @@ -16,7 +16,7 @@ export class BootstrapService { for (const chain of chains) { this.logger.log(`Indexing assets for chain ${chain.name}`); const indexerService = await this.IndexerServiceFactory.getService(chain.id); - await indexerService.startAssetsIndexing(); + await indexerService.start(); } } } diff --git a/backend/src/core/blockchain/blockchain.service.ts b/backend/src/core/blockchain/blockchain.service.ts index 8e75d17f..a7f6c631 100644 --- a/backend/src/core/blockchain/blockchain.service.ts +++ b/backend/src/core/blockchain/blockchain.service.ts @@ -96,8 +96,17 @@ export class BlockchainService { } @Cacheable({ - namespace: 'CkbRpcWebsocketService', - key: (txHash: string) => `getTransaction:${txHash}`, + namespace: 'BlockchainService', + key: (txHash: string, withData: boolean, withWitness: boolean) => { + let key = `getTransaction:${txHash}`; + if (withData) { + key += ':withData'; + } + if (withWitness) { + key += ':withWitness'; + } + return key; + }, ttl: ONE_MONTH_MS, shouldCache: async (tx: TransactionWithStatusResponse, that: BlockchainService) => { if (tx.tx_status.status !== 'committed' || !tx.tx_status.block_number) { @@ -106,29 +115,119 @@ export class BlockchainService { return that.isSafeConfirmations(tx.tx_status.block_number); }, }) - public async getTransaction(txHash: string): Promise { + public async getTransaction( + txHash: string, + withData: boolean = false, + withWitness: boolean = false, + ): Promise { await this.websocketReady; this.logger.debug(`get_transaction - txHash: ${txHash}`); - const tx = await this.websocket.call('get_transaction', [txHash]); - return tx as TransactionWithStatusResponse; + const response = await this.websocket.call('get_transaction', [txHash]); + const tx = response as TransactionWithStatusResponse; + // XXX: we don't need these fields by default, remove them to save cache/memory space + if (!withData) { + tx.transaction.outputs_data = []; + } + if (!withWitness) { + tx.transaction.witnesses = []; + } + return tx; } - public async getBlock(blockHash: string): Promise { + @Cacheable({ + namespace: 'BlockchainService', + key: (blockHash: string, withTxData: boolean, withTxWitness: boolean) => { + let key = `getBlock:${blockHash}`; + if (withTxData) { + key += ':withTxData'; + } + if (withTxWitness) { + key += ':withTxWitness'; + } + return key; + }, + ttl: ONE_MONTH_MS, + shouldCache: async (block: Block, that: BlockchainService) => { + if (!block?.header) { + return false; + } + const { number } = block.header; + return that.isSafeConfirmations(number); + }, + }) + public async getBlock( + blockHash: string, + withTxData: boolean = false, + withTxWitness: boolean = false, + ): Promise { await this.websocketReady; this.logger.debug(`get_block - blockHash: ${blockHash}`); - const block = await this.websocket.call('get_block', [blockHash]); - return block as Block; + const response = await this.websocket.call('get_block', [blockHash]); + const block = response as Block; + if (!withTxData) { + block.transactions = block.transactions.map((tx) => { + tx.outputs_data = []; + return tx; + }); + } + if (!withTxWitness) { + block.transactions = block.transactions.map((tx) => { + tx.witnesses = []; + return tx; + }); + } + return block; } - public async getBlockByNumber(blockNumber: string): Promise { + @Cacheable({ + namespace: 'BlockchainService', + key: (blockNumber: string, withTxData: boolean, withTxWitness: boolean) => { + let key = `getBlockByNumber:${blockNumber}`; + if (withTxData) { + key += ':withTxData'; + } + if (withTxWitness) { + key += ':withTxWitness'; + } + return key; + }, + ttl: ONE_MONTH_MS, + shouldCache: async (block: Block, that: BlockchainService) => { + const { number } = block.header; + return that.isSafeConfirmations(number); + }, + }) + public async getBlockByNumber( + blockNumber: string, + withTxData: boolean = false, + withTxWitness: boolean = false, + ): Promise { await this.websocketReady; this.logger.debug(`get_block_by_number - blockNumber: ${blockNumber}`); - const block = await this.websocket.call('get_block_by_number', [ + const response = await this.websocket.call('get_block_by_number', [ BI.from(blockNumber).toHexString(), ]); - return block as Block; + const block = response as Block; + if (!withTxData) { + block.transactions = block.transactions.map((tx) => { + tx.outputs_data = []; + return tx; + }); + } + if (!withTxWitness) { + block.transactions = block.transactions.map((tx) => { + tx.witnesses = []; + return tx; + }); + } + return block; } + @Cacheable({ + namespace: 'BlockchainService', + key: (blockHash: string) => `getBlockEconomicState:${blockHash}`, + ttl: ONE_MONTH_MS, + }) public async getBlockEconomicState(blockHash: string): Promise { await this.websocketReady; this.logger.debug(`get_block_economic_state - blockHash: ${blockHash}`); @@ -136,6 +235,12 @@ export class BlockchainService { return blockEconomicState as BlockEconomicState; } + @Cacheable({ + namespace: 'BlockchainService', + key: 'getTipBlockNumber', + // just cache for 1 second to avoid too many requests + ttl: 1000, + }) public async getTipBlockNumber(): Promise { await this.websocketReady; this.logger.debug('get_tip_block_number'); @@ -153,13 +258,14 @@ export class BlockchainService { this.logger.debug( `get_transactions - searchKey: ${JSON.stringify(searchKey)}, order: ${order}, limit: ${limit}, after: ${after}`, ); - const transactions = await this.websocket.call('get_transactions', [ + const result = await this.websocket.call('get_transactions', [ searchKey, order, limit, after, ]); - return transactions as GetTransactionsResult; + const transactions = result as GetTransactionsResult; + return transactions; } public async getCells( @@ -167,12 +273,20 @@ export class BlockchainService { order: 'asc' | 'desc', limit: string, after?: string, + withData: boolean = false, ): Promise { await this.websocketReady; this.logger.debug( `get_cells - searchKey: ${JSON.stringify(searchKey)}, order: ${order}, limit: ${limit}, after: ${after}`, ); - const cells = await this.websocket.call('get_cells', [searchKey, order, limit, after]); - return cells as GetCellsResult; + const result = await this.websocket.call('get_cells', [searchKey, order, limit, after]); + const cells = result as GetCellsResult; + cells.objects = cells.objects.map((cell) => { + if (!withData) { + cell.output_data = ''; + } + return cell; + }); + return cells; } } diff --git a/backend/src/core/core.service.ts b/backend/src/core/core.service.ts index 2efaed95..5fb1fea4 100644 --- a/backend/src/core/core.service.ts +++ b/backend/src/core/core.service.ts @@ -1,4 +1,4 @@ -import { Script } from '@ckb-lumos/lumos'; +import { BI, HashType, Script } from '@ckb-lumos/lumos'; import { bytes } from '@ckb-lumos/lumos/codec'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; @@ -11,19 +11,19 @@ import { } from '@rgbpp-sdk/ckb'; import { BtcTestnetTypeMap, NetworkType } from 'src/constants'; import { Env } from 'src/env'; - -export enum LeapDirection { - LeapIn = 'leap_in', - LeapOut = 'leap_out', - Within = 'within', -} +import { Transaction } from './blockchain/blockchain.interface'; +import { BlockchainServiceFactory } from './blockchain/blockchain.factory'; +import { LeapDirection } from '@prisma/client'; export const CELLBASE_TX_HASH = '0x0000000000000000000000000000000000000000000000000000000000000000'; @Injectable() export class CoreService { - constructor(private configService: ConfigService) {} + constructor( + private configService: ConfigService, + private blockchainServiceFactory: BlockchainServiceFactory, + ) {} public get rgbppLockScript() { const network = this.configService.get('NETWORK'); @@ -71,4 +71,52 @@ export class CoreService { this.btcTimeLockScript, ); } + + public async getLeapDirectionByCkbTx(chainId: number, ckbTx: Transaction) { + const blockchainService = this.blockchainServiceFactory.getService(chainId); + const inputCells = await Promise.all( + ckbTx.inputs.map(async (input) => { + const inputTx = await blockchainService.getTransaction(input.previous_output.tx_hash); + const index = BI.from(input.previous_output.index).toNumber(); + return inputTx?.transaction.outputs?.[index] ?? null; + }), + ); + const hasRgbppLockInput = inputCells.some( + (cell) => + cell?.lock && + this.isRgbppLockScript({ + codeHash: cell.lock.code_hash, + hashType: cell.lock.hash_type as HashType, + args: cell.lock.args, + }), + ); + const hasRgbppLockOuput = ckbTx.outputs.some( + (output) => + output?.lock && + this.isRgbppLockScript({ + codeHash: output.lock.code_hash, + hashType: output.lock.hash_type as HashType, + args: output.lock.args, + }), + ); + const hasBtcTimeLockOutput = ckbTx.outputs.some( + (output) => + output.lock && + this.isBtcTimeLockScript({ + codeHash: output.lock.code_hash, + hashType: output.lock.hash_type as HashType, + args: output.lock.args, + }), + ); + if (hasRgbppLockInput && hasBtcTimeLockOutput) { + return LeapDirection.LeapOut; + } + if (hasRgbppLockInput && hasRgbppLockOuput) { + return LeapDirection.Within; + } + if (!hasRgbppLockInput && hasRgbppLockOuput) { + return LeapDirection.LeapIn; + } + return null; + } } diff --git a/backend/src/core/database/database.health.ts b/backend/src/core/database/database.health.ts new file mode 100644 index 00000000..27a513fa --- /dev/null +++ b/backend/src/core/database/database.health.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus'; +import * as Sentry from '@sentry/nestjs'; +import { PrismaService } from './prisma/prisma.service'; + +@Injectable() +export class DatabaseHealthIndicator extends HealthIndicator { + constructor( + private prismaService: PrismaService, + ) { + super(); + } + + public async isHealthy(): Promise { + try { + await this.prismaService.$queryRaw`SELECT 1`; + return this.getStatus('database.prisma', true); + } catch (e) { + Sentry.captureException(e); + throw new HealthCheckError('BitcoinApiService failed', e); + } + } +} diff --git a/backend/src/core/database/database.module.ts b/backend/src/core/database/database.module.ts index 5b96b762..1a5e1904 100644 --- a/backend/src/core/database/database.module.ts +++ b/backend/src/core/database/database.module.ts @@ -1,9 +1,11 @@ import { Global, Module } from '@nestjs/common'; import { PrismaModule } from './prisma/prisma.module'; +import { DatabaseHealthIndicator } from './database.health'; @Global() @Module({ imports: [PrismaModule], - exports: [], + providers: [DatabaseHealthIndicator], + exports: [DatabaseHealthIndicator], }) export class DatabaseModule {} diff --git a/backend/src/core/health/health.controller.ts b/backend/src/core/health/health.controller.ts index 1c7106c7..729296c7 100644 --- a/backend/src/core/health/health.controller.ts +++ b/backend/src/core/health/health.controller.ts @@ -3,6 +3,8 @@ import { HealthCheckService, HealthCheck, HttpHealthIndicator } from '@nestjs/te import { CkbRpcHealthIndicator, CkbRpcHealthIndicatorKey } from '../ckb-rpc/ckb-rpc.health'; import { BitcoinApiHealthIndicator } from '../bitcoin-api/bitcoin-api.health'; import { CkbExplorerHealthIndicator } from '../ckb-explorer/ckb-explorer.health'; +import { IndexerHealthIndicator, IndexerHealthIndicatorKey } from '../indexer/indexer.health'; +import { DatabaseHealthIndicator } from '../database/database.health'; @Controller('health') export class HealthController { @@ -12,7 +14,9 @@ export class HealthController { private bitcoinApiHealthIndicator: BitcoinApiHealthIndicator, private ckbRpcHealthIndicator: CkbRpcHealthIndicator, private ckbExplorerHealthIndicator: CkbExplorerHealthIndicator, - ) { } + private indexerHealthIndicator: IndexerHealthIndicator, + private databaseHealthIndicator: DatabaseHealthIndicator, + ) {} @Get() @HealthCheck() @@ -22,6 +26,9 @@ export class HealthController { this.http.pingCheck('graphql', 'http://localhost:3000/graphql?query=%7B__typename%7D', { headers: { 'apollo-require-preflight': true }, }), + () => this.databaseHealthIndicator.isHealthy(), + () => this.indexerHealthIndicator.isHealthy(IndexerHealthIndicatorKey.Asset), + () => this.indexerHealthIndicator.isHealthy(IndexerHealthIndicatorKey.Transaction), () => this.bitcoinApiHealthIndicator.isHealthy(), () => this.ckbRpcHealthIndicator.isHealthy(CkbRpcHealthIndicatorKey.Websocket), () => this.ckbExplorerHealthIndicator.isHealthy(), diff --git a/backend/src/core/health/health.module.ts b/backend/src/core/health/health.module.ts index ddd97191..5e6671b1 100644 --- a/backend/src/core/health/health.module.ts +++ b/backend/src/core/health/health.module.ts @@ -5,9 +5,19 @@ import { CkbRpcModule } from '../ckb-rpc/ckb-rpc.module'; import { CkbExplorerModule } from '../ckb-explorer/ckb-explorer.module'; import { BitcoinApiModule } from '../bitcoin-api/bitcoin-api.module'; import { HttpModule } from '@nestjs/axios'; +import { IndexerModule } from '../indexer/indexer.module'; +import { DatabaseModule } from '../database/database.module'; @Module({ - imports: [TerminusModule, HttpModule, BitcoinApiModule, CkbRpcModule, CkbExplorerModule], + imports: [ + TerminusModule, + HttpModule, + BitcoinApiModule, + CkbRpcModule, + CkbExplorerModule, + IndexerModule, + DatabaseModule, + ], providers: [HealthController], controllers: [HealthController], }) diff --git a/backend/src/core/indexer/flow/assets.flow.ts b/backend/src/core/indexer/flow/assets.flow.ts new file mode 100644 index 00000000..c020a51a --- /dev/null +++ b/backend/src/core/indexer/flow/assets.flow.ts @@ -0,0 +1,96 @@ +import { Logger } from '@nestjs/common'; +import { AssetType, Chain } from '@prisma/client'; +import { EventEmitter } from 'node:events'; +import { CKB_MIN_SAFE_CONFIRMATIONS } from 'src/constants'; +import { BlockchainService } from 'src/core/blockchain/blockchain.service'; +import { PrismaService } from 'src/core/database/prisma/prisma.service'; +import { IndexerQueueService } from '../indexer.queue'; + +export enum IndexerAssetsEvent { + AssetIndexed = 'asset-indexed', + BlockAssetsIndexed = 'block-assets-indexed', +} + +export class IndexerAssetsFlow extends EventEmitter { + private readonly logger = new Logger(IndexerAssetsFlow.name); + + constructor( + private chain: Chain, + private blockchainService: BlockchainService, + private prismaService: PrismaService, + private indexerQueueService: IndexerQueueService, + ) { + super(); + } + + public async start() { + const assetTypeScripts = await this.prismaService.assetType.findMany({ + where: { chainId: this.chain.id }, + }); + this.logger.log(`Indexing ${assetTypeScripts.length} asset type scripts`); + assetTypeScripts.map((assetType) => this.indexAssets(assetType)); + this.setupAssetIndexedListener(assetTypeScripts.length); + } + + private async indexAssets(assetType: AssetType) { + const cursor = await this.indexerQueueService.getLatestAssetJobCursor(assetType); + if (cursor === '0x') { + this.emit(IndexerAssetsEvent.AssetIndexed, assetType); + return; + } + await this.indexerQueueService.addAssetJob({ + chainId: this.chain.id, + assetType, + cursor, + }); + } + + private setupAssetIndexedListener(totalAssetTypes: number) { + let completed = 0; + const onAssetIndexed = async (assetType: AssetType) => { + completed += 1; + this.logger.log(`Asset type ${assetType.codeHash} indexed`); + if (completed === totalAssetTypes) { + this.off(IndexerAssetsEvent.AssetIndexed, onAssetIndexed); + this.startBlockAssetsIndexing(); + this.setupBlockAssetsIndexedListener(); + } + }; + this.on(IndexerAssetsEvent.AssetIndexed, onAssetIndexed); + } + + private async startBlockAssetsIndexing() { + const tipBlockNumber = await this.blockchainService.getTipBlockNumber(); + + let latestIndexedBlockNumber = await this.indexerQueueService.getLatestIndexedBlock( + this.chain.id, + ); + if (!latestIndexedBlockNumber) { + const latestAsset = await this.prismaService.asset.findFirst({ + select: { blockNumber: true }, + where: { chainId: this.chain.id }, + orderBy: { blockNumber: 'desc' }, + }); + latestIndexedBlockNumber = latestAsset!.blockNumber; + } + const targetBlockNumber = tipBlockNumber - CKB_MIN_SAFE_CONFIRMATIONS; + if (targetBlockNumber <= latestIndexedBlockNumber) { + this.emit(IndexerAssetsEvent.BlockAssetsIndexed); + return; + } + + await this.indexerQueueService.addBlockAssetsJob({ + chainId: this.chain.id, + blockNumber: latestIndexedBlockNumber + 1, + targetBlockNumber, + }); + } + + private setupBlockAssetsIndexedListener() { + this.on(IndexerAssetsEvent.BlockAssetsIndexed, () => { + setTimeout(() => { + this.startBlockAssetsIndexing(); + }, 1000 * 10); + }); + } +} diff --git a/backend/src/core/indexer/flow/transactions.flow.ts b/backend/src/core/indexer/flow/transactions.flow.ts new file mode 100644 index 00000000..a34b6354 --- /dev/null +++ b/backend/src/core/indexer/flow/transactions.flow.ts @@ -0,0 +1,65 @@ +import { Logger } from '@nestjs/common'; +import { Chain } from '@prisma/client'; +import { EventEmitter } from 'node:events'; +import { CKB_MIN_SAFE_CONFIRMATIONS } from 'src/constants'; +import { BlockchainService } from 'src/core/blockchain/blockchain.service'; +import { PrismaService } from 'src/core/database/prisma/prisma.service'; +import { IndexerQueueService } from '../indexer.queue'; +import { ONE_DAY_MS } from 'src/common/date'; + +const CKB_24_HOURS_BLOCK_NUMBER = ONE_DAY_MS / 10000; + +export enum IndexerTransactionsEvent { + BlockIndexed = 'block-indexed', +} + +export class IndexerTransactionsFlow extends EventEmitter { + private readonly logger = new Logger(IndexerTransactionsFlow.name); + + constructor( + private chain: Chain, + private blockchainService: BlockchainService, + private prismaService: PrismaService, + private indexerQueueService: IndexerQueueService, + ) { + super(); + } + + public async start() { + this.startBlockAssetsIndexing(); + this.setupBlockAssetsIndexedListener(); + } + + public async startBlockAssetsIndexing() { + const tipBlockNumber = await this.blockchainService.getTipBlockNumber(); + let startBlockNumber = tipBlockNumber - CKB_24_HOURS_BLOCK_NUMBER; + const targetBlockNumber = tipBlockNumber - CKB_MIN_SAFE_CONFIRMATIONS; + + const block = await this.prismaService.block.findFirst({ + where: { chainId: this.chain.id }, + orderBy: { number: 'desc' }, + }); + if (block) { + startBlockNumber = Math.max(startBlockNumber, block.number + 1); + } + + if (startBlockNumber >= targetBlockNumber) { + this.emit(IndexerTransactionsEvent.BlockIndexed); + return; + } + this.logger.log(`Indexing blocks from ${startBlockNumber} to ${targetBlockNumber}`); + this.indexerQueueService.addBlockJob({ + chainId: this.chain.id, + blockNumber: startBlockNumber, + targetBlockNumber, + }); + } + + private setupBlockAssetsIndexedListener() { + this.on(IndexerTransactionsEvent.BlockIndexed, () => { + setTimeout(() => { + this.startBlockAssetsIndexing(); + }, 1000 * 10); + }); + } +} diff --git a/backend/src/core/indexer/indexer.factory.ts b/backend/src/core/indexer/indexer.factory.ts index 93ba5ab0..d3d6fb71 100644 --- a/backend/src/core/indexer/indexer.factory.ts +++ b/backend/src/core/indexer/indexer.factory.ts @@ -3,6 +3,7 @@ import { PrismaService } from '../database/prisma/prisma.service'; import { IndexerService } from './indexer.service'; import { BlockchainServiceFactory } from '../blockchain/blockchain.factory'; import { IndexerQueueService } from './indexer.queue'; +import { ModuleRef } from '@nestjs/core'; export class IndexerServiceFactoryError extends Error { constructor(message: string) { @@ -18,8 +19,8 @@ export class IndexerServiceFactory implements OnModuleDestroy { constructor( private blockchainServiceFactory: BlockchainServiceFactory, private prismaService: PrismaService, - private indexerQueueService: IndexerQueueService, - ) { } + private moduleRef: ModuleRef, + ) {} public async onModuleDestroy() { for (const service of this.services.values()) { @@ -35,12 +36,13 @@ export class IndexerServiceFactory implements OnModuleDestroy { throw new IndexerServiceFactoryError(`Chain with ID ${chainId} not found`); } if (!this.services.has(chain.id)) { + const indexerQueueService = this.moduleRef.get(IndexerQueueService); const blockchainService = this.blockchainServiceFactory.getService(chain.id); const service = new IndexerService( chain, blockchainService, this.prismaService, - this.indexerQueueService, + indexerQueueService, ); this.services.set(chain.id, service); } diff --git a/backend/src/core/indexer/indexer.health.ts b/backend/src/core/indexer/indexer.health.ts new file mode 100644 index 00000000..2f6199be --- /dev/null +++ b/backend/src/core/indexer/indexer.health.ts @@ -0,0 +1,101 @@ +import { Injectable } from '@nestjs/common'; +import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus'; +import * as Sentry from '@sentry/nestjs'; +import { PrismaService } from '../database/prisma/prisma.service'; +import { BlockchainServiceFactory } from '../blockchain/blockchain.factory'; +import { CKB_CHAIN_ID, CKB_MIN_SAFE_CONFIRMATIONS } from 'src/constants'; +import { IndexerQueueService } from './indexer.queue'; + +export enum IndexerHealthIndicatorKey { + Transaction = 'indexer.transaction', + Asset = 'indexer.asset', +} + +@Injectable() +export class IndexerHealthIndicator extends HealthIndicator { + constructor( + private prismaService: PrismaService, + private blockchainServiceFactory: BlockchainServiceFactory, + private indexerQueueService: IndexerQueueService, + ) { + super(); + } + + public async isHealthy(key: IndexerHealthIndicatorKey): Promise { + try { + let isHealthy = false; + let result: HealthIndicatorResult = {}; + + switch (key) { + case IndexerHealthIndicatorKey.Asset: + const assetHealthy = await this.isAssetIndexerHealthy(); + isHealthy = assetHealthy.isHealthy; + result = assetHealthy.result; + break; + case IndexerHealthIndicatorKey.Transaction: + const healthy = await this.isTransactionIndexerHealthy(); + isHealthy = healthy.isHealthy; + result = healthy.result; + break; + default: + throw new HealthCheckError(`Unknown health indicator key`, key); + } + + if (isHealthy) { + return result; + } + throw new HealthCheckError('IndexerService failed', result); + } catch (e) { + Sentry.captureException(e); + throw new HealthCheckError('IndexerService failed', e); + } + } + + private async isAssetIndexerHealthy(): Promise<{ + isHealthy: boolean; + result: HealthIndicatorResult; + }> { + const blockchainService = this.blockchainServiceFactory.getService(CKB_CHAIN_ID); + const tipBlockNumber = await blockchainService.getTipBlockNumber(); + const targetBlockNumber = tipBlockNumber - CKB_MIN_SAFE_CONFIRMATIONS; + const currentBlockNumber = await this.indexerQueueService.getLatestIndexedBlock(CKB_CHAIN_ID); + + const isHealthy = !!currentBlockNumber && currentBlockNumber >= targetBlockNumber - 2; + const result = this.getStatus('indexer.asset', isHealthy, { + targetBlockNumber, + currentBlockNumber, + }); + return { + isHealthy, + result, + }; + } + + private async isTransactionIndexerHealthy(): Promise<{ + isHealthy: boolean; + result: HealthIndicatorResult; + }> { + const blockchainService = this.blockchainServiceFactory.getService(CKB_CHAIN_ID); + const tipBlockNumber = await blockchainService.getTipBlockNumber(); + const targetBlockNumber = tipBlockNumber - CKB_MIN_SAFE_CONFIRMATIONS; + + const block = await this.prismaService.block.findFirst({ + where: { + chainId: CKB_CHAIN_ID, + }, + orderBy: { + number: 'desc', + }, + }); + + const isHealthy = !!block && block.number >= targetBlockNumber - 2; + const result = this.getStatus('indexer.transaction', isHealthy, { + targetBlockNumber, + currentBlockNumber: block?.number, + }); + return { + isHealthy, + result, + }; + } +} diff --git a/backend/src/core/indexer/indexer.module.ts b/backend/src/core/indexer/indexer.module.ts index d28f0fa2..3cf57146 100644 --- a/backend/src/core/indexer/indexer.module.ts +++ b/backend/src/core/indexer/indexer.module.ts @@ -3,12 +3,21 @@ import { IndexerServiceFactory } from './indexer.factory'; import { BullModule } from '@nestjs/bullmq'; import { INDEXER_ASSETS_QUEUE, IndexerAssetsProcessor } from './processor/assets.processor'; import { IndexerQueueService } from './indexer.queue'; -import { INDEXER_BLOCK_QUEUE, IndexerBlockProcessor } from './processor/block.processor'; +import { + INDEXER_BLOCK_ASSETS_QUEUE, + IndexerBlockAssetsProcessor, +} from './processor/block-assets.processor'; import { IndexerAssetsService } from './service/assets.service'; import { CoreModule } from '../core.module'; import { INDEXER_TYPE_QUEUE, IndexerTypeProcessor } from './processor/type.processor'; import { INDEXER_LOCK_QUEUE, IndexerLockProcessor } from './processor/lock.processor'; import { DefaultJobOptions } from 'bullmq'; +import { INDEXER_BLOCK_QUEUE, IndexerBlockProcessor } from './processor/block.processor'; +import { + INDEXER_TRANSACTION_QUEUE, + IndexerTransactionProcessor, +} from './processor/transaction.processor'; +import { IndexerHealthIndicator } from './indexer.health'; const commonAttemptsConfig: Pick = { attempts: 10, @@ -23,23 +32,43 @@ const commonAttemptsConfig: Pick = { imports: [ BullModule.registerQueue({ name: INDEXER_ASSETS_QUEUE, - ...commonAttemptsConfig, + defaultJobOptions: { + ...commonAttemptsConfig, + }, }), BullModule.registerQueue({ - name: INDEXER_BLOCK_QUEUE, + name: INDEXER_BLOCK_ASSETS_QUEUE, defaultJobOptions: { removeOnComplete: true, removeOnFail: true, ...commonAttemptsConfig, }, }), + BullModule.registerQueue({ + name: INDEXER_BLOCK_QUEUE, + defaultJobOptions: { + removeOnComplete: true, + ...commonAttemptsConfig, + }, + }), + BullModule.registerQueue({ + name: INDEXER_TRANSACTION_QUEUE, + defaultJobOptions: { + removeOnComplete: true, + ...commonAttemptsConfig, + }, + }), BullModule.registerQueue({ name: INDEXER_LOCK_QUEUE, - ...commonAttemptsConfig, + defaultJobOptions: { + ...commonAttemptsConfig, + }, }), BullModule.registerQueue({ name: INDEXER_TYPE_QUEUE, - ...commonAttemptsConfig, + defaultJobOptions: { + ...commonAttemptsConfig, + }, }), forwardRef(() => CoreModule), ], @@ -48,10 +77,13 @@ const commonAttemptsConfig: Pick = { IndexerAssetsService, IndexerQueueService, IndexerAssetsProcessor, - IndexerBlockProcessor, + IndexerBlockAssetsProcessor, IndexerLockProcessor, IndexerTypeProcessor, + IndexerBlockProcessor, + IndexerTransactionProcessor, + IndexerHealthIndicator, ], - exports: [IndexerServiceFactory, IndexerQueueService], + exports: [IndexerServiceFactory, IndexerQueueService, IndexerHealthIndicator], }) -export class IndexerModule { } +export class IndexerModule {} diff --git a/backend/src/core/indexer/indexer.queue.ts b/backend/src/core/indexer/indexer.queue.ts index 4f6272bd..d0f7beb1 100644 --- a/backend/src/core/indexer/indexer.queue.ts +++ b/backend/src/core/indexer/indexer.queue.ts @@ -6,9 +6,17 @@ import { HashType, Script } from '@ckb-lumos/lumos'; import { computeScriptHash } from '@ckb-lumos/lumos/utils'; import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager'; import { AssetType } from '@prisma/client'; -import { INDEXER_BLOCK_QUEUE, IndexerBlockJobData } from './processor/block.processor'; +import { + INDEXER_BLOCK_ASSETS_QUEUE, + IndexerBlockAssetsJobData, +} from './processor/block-assets.processor'; import { INDEXER_LOCK_QUEUE, IndexerLockJobData } from './processor/lock.processor'; import { INDEXER_TYPE_QUEUE, IndexerTypeJobData } from './processor/type.processor'; +import { INDEXER_BLOCK_QUEUE, IndexerBlockJobData } from './processor/block.processor'; +import { + INDEXER_TRANSACTION_QUEUE, + IndexerTransactionJobData, +} from './processor/transaction.processor'; @Injectable() export class IndexerQueueService { @@ -16,18 +24,25 @@ export class IndexerQueueService { constructor( @InjectQueue(INDEXER_ASSETS_QUEUE) public assetsQueue: Queue, - @InjectQueue(INDEXER_BLOCK_QUEUE) public blockQueue: Queue, + @InjectQueue(INDEXER_BLOCK_ASSETS_QUEUE) public blockAssetsQueue: Queue, @InjectQueue(INDEXER_LOCK_QUEUE) public lockQueue: Queue, @InjectQueue(INDEXER_TYPE_QUEUE) public typeQueue: Queue, + @InjectQueue(INDEXER_BLOCK_QUEUE) public blockQueue: Queue, + @InjectQueue(INDEXER_TRANSACTION_QUEUE) public transactionQueue: Queue, @Inject(CACHE_MANAGER) private cacheManager: Cache, ) { } public async moveActiveJobToDelay() { - const activeAssetsJobs = await this.assetsQueue.getJobs(['active']); - await Promise.all((activeAssetsJobs || []).map((job) => job.moveToDelayed(Date.now() + 1000))); - - const activeBlockJobs = await this.blockQueue.getJobs(['active']); - await Promise.all((activeBlockJobs || []).map((job) => job.moveToDelayed(Date.now() + 1000))); + await Promise.all([ + this.assetsQueue.getJobs(['active']), + this.blockAssetsQueue.getJobs(['active']), + this.lockQueue.getJobs(['active']), + this.typeQueue.getJobs(['active']), + this.blockQueue.getJobs(['active']), + this.transactionQueue.getJobs(['active']), + ]).then(async (jobs) => { + await Promise.all(jobs.flat().map((job) => job.moveToDelayed(Date.now() + 1000))); + }); } public async getLatestAssetJobCursor(assetType: AssetType) { @@ -62,23 +77,25 @@ export class IndexerQueueService { } public async getLatestIndexedBlock(chainId: number) { - const blockNumber = await this.cacheManager.get(`${INDEXER_BLOCK_QUEUE}:${chainId}`); + const blockNumber = await this.cacheManager.get( + `${INDEXER_BLOCK_ASSETS_QUEUE}:${chainId}`, + ); return blockNumber; } - public async addBlockJob(data: IndexerBlockJobData) { + public async addBlockAssetsJob(data: IndexerBlockAssetsJobData) { const { chainId, blockNumber } = data; const params = new URLSearchParams(); - params.append('jobType', 'index-block'); + params.append('jobType', 'index-block-assets'); params.append('chainId', chainId.toString()); params.append('blockNumber', blockNumber.toString()); const jobId = params.toString(); this.logger.debug( - `Added block job ${jobId} for chain ${chainId} with block number ${blockNumber}`, + `Added block assets job ${jobId} for chain ${chainId} with block number ${blockNumber}`, ); - await this.blockQueue.add(jobId, data, { jobId }); - await this.cacheManager.set(`${INDEXER_BLOCK_QUEUE}:${chainId}`, blockNumber); + await this.blockAssetsQueue.add(jobId, data, { jobId }); + await this.cacheManager.set(`${INDEXER_BLOCK_ASSETS_QUEUE}:${chainId}`, blockNumber); } public async addLockJob(data: IndexerLockJobData) { @@ -108,4 +125,31 @@ export class IndexerQueueService { ); await this.typeQueue.add(jobId, data, { jobId }); } + + public async addBlockJob(data: IndexerBlockJobData) { + const { chainId, blockNumber } = data; + const params = new URLSearchParams(); + params.append('jobType', 'index-block'); + params.append('chainId', chainId.toString()); + params.append('blockNumber', blockNumber.toString()); + const jobId = params.toString(); + + this.logger.debug( + `Added block job ${jobId} for chain ${chainId} with block number ${blockNumber}`, + ); + await this.blockQueue.add(jobId, data, { jobId }); + } + + public async addTransactionJob(data: IndexerTransactionJobData) { + const { chainId, transaction } = data; + const params = new URLSearchParams(); + params.append('jobType', 'index-transaction'); + params.append('chainId', chainId.toString()); + params.append('txHash', transaction.hash); + const jobId = params.toString(); + this.logger.debug( + `Added transaction job ${jobId} for chain ${chainId} with tx hash ${transaction.hash}`, + ); + await this.transactionQueue.add(jobId, data, { jobId }); + } } diff --git a/backend/src/core/indexer/indexer.service.ts b/backend/src/core/indexer/indexer.service.ts index d11a6a75..af8da99d 100644 --- a/backend/src/core/indexer/indexer.service.ts +++ b/backend/src/core/indexer/indexer.service.ts @@ -1,18 +1,13 @@ -import { Logger } from '@nestjs/common'; -import { AssetType, Chain } from '@prisma/client'; +import { Chain } from '@prisma/client'; +import { IndexerAssetsFlow } from './flow/assets.flow'; import { BlockchainService } from '../blockchain/blockchain.service'; import { PrismaService } from '../database/prisma/prisma.service'; import { IndexerQueueService } from './indexer.queue'; -import { EventEmitter } from 'node:events'; -import { CKB_MIN_SAFE_CONFIRMATIONS } from 'src/constants'; +import { IndexerTransactionsFlow } from './flow/transactions.flow'; -export enum IndexerEvent { - AssetIndexed = 'asset-indexed', - BlockIndexed = 'block-indexed', -} - -export class IndexerService extends EventEmitter { - private readonly logger = new Logger(IndexerService.name); +export class IndexerService { + public assetsFlow: IndexerAssetsFlow; + public transactionsFlow: IndexerTransactionsFlow; constructor( private chain: Chain, @@ -20,82 +15,29 @@ export class IndexerService extends EventEmitter { private prismaService: PrismaService, private indexerQueueService: IndexerQueueService, ) { - super(); + this.assetsFlow = new IndexerAssetsFlow( + this.chain, + this.blockchainService, + this.prismaService, + this.indexerQueueService, + ); + this.transactionsFlow = new IndexerTransactionsFlow( + this.chain, + this.blockchainService, + this.prismaService, + this.indexerQueueService, + ); } - public async startAssetsIndexing() { + public async start() { await this.indexerQueueService.moveActiveJobToDelay(); - const assetTypeScripts = await this.prismaService.assetType.findMany({ - where: { chainId: this.chain.id }, - }); - this.logger.log(`Indexing ${assetTypeScripts.length} asset type scripts`); - assetTypeScripts.map((assetType) => this.indexAssets(assetType)); - this.setupAssetIndexedListener(assetTypeScripts.length); + await Promise.all([ + this.assetsFlow.start(), + this.transactionsFlow.start(), + ]); } public async close() { this.blockchainService.close(); } - - private async indexAssets(assetType: AssetType) { - const cursor = await this.indexerQueueService.getLatestAssetJobCursor(assetType); - if (cursor === '0x') { - this.emit(IndexerEvent.AssetIndexed, assetType); - return; - } - await this.indexerQueueService.addAssetJob({ - chainId: this.chain.id, - assetType, - cursor, - }); - } - - private setupAssetIndexedListener(totalAssetTypes: number) { - let completed = 0; - const onAssetIndexed = async (assetType: AssetType) => { - completed += 1; - this.logger.log(`Asset type ${assetType.codeHash} indexed`); - if (completed === totalAssetTypes) { - this.off(IndexerEvent.AssetIndexed, onAssetIndexed); - this.startBlockIndexing(); - this.setupBlockIndexedListener(); - } - }; - this.on(IndexerEvent.AssetIndexed, onAssetIndexed); - } - - private async startBlockIndexing() { - const tipBlockNumber = await this.blockchainService.getTipBlockNumber(); - - let latestIndexedBlockNumber = await this.indexerQueueService.getLatestIndexedBlock( - this.chain.id, - ); - if (!latestIndexedBlockNumber) { - const latestAsset = await this.prismaService.asset.findFirst({ - select: { blockNumber: true }, - where: { chainId: this.chain.id }, - orderBy: { blockNumber: 'desc' }, - }); - latestIndexedBlockNumber = latestAsset!.blockNumber; - } - const targetBlockNumber = tipBlockNumber - CKB_MIN_SAFE_CONFIRMATIONS; - if (targetBlockNumber <= latestIndexedBlockNumber) { - this.emit(IndexerEvent.BlockIndexed); - return; - } - - await this.indexerQueueService.addBlockJob({ - chainId: this.chain.id, - blockNumber: latestIndexedBlockNumber + 1, - targetBlockNumber, - }); - } - - private setupBlockIndexedListener() { - this.on(IndexerEvent.BlockIndexed, () => { - setTimeout(() => { - this.startBlockIndexing(); - }, 1000 * 10); - }); - } } diff --git a/backend/src/core/indexer/processor/assets.processor.ts b/backend/src/core/indexer/processor/assets.processor.ts index 6838928e..32411c0b 100644 --- a/backend/src/core/indexer/processor/assets.processor.ts +++ b/backend/src/core/indexer/processor/assets.processor.ts @@ -9,8 +9,8 @@ import { IndexerQueueService } from '../indexer.queue'; import { ModuleRef } from '@nestjs/core'; import { IndexerServiceFactory } from '../indexer.factory'; import { IndexerAssetsService } from '../service/assets.service'; -import { IndexerEvent } from '../indexer.service'; import * as Sentry from '@sentry/node'; +import { IndexerAssetsEvent } from '../flow/assets.flow'; export const INDEXER_ASSETS_QUEUE = 'indexer-assets-queue'; @@ -57,6 +57,7 @@ export class IndexerAssetsProcessor extends WorkerHost { this.logger.error( `Indexing assets (code hash: ${assetType.codeHash}, cursor: ${cursor}) for chain ${chainId} failed`, ); + this.logger.error(error.stack); Sentry.captureException(error); } @@ -65,7 +66,7 @@ export class IndexerAssetsProcessor extends WorkerHost { if (cursor === '0x') { const indexerServiceFactory = this.moduleRef.get(IndexerServiceFactory); const indexerService = await indexerServiceFactory.getService(chainId); - indexerService.emit(IndexerEvent.AssetIndexed, assetType); + indexerService.assetsFlow.emit(IndexerAssetsEvent.AssetIndexed, assetType); return; } diff --git a/backend/src/core/indexer/processor/block-assets.processor.ts b/backend/src/core/indexer/processor/block-assets.processor.ts new file mode 100644 index 00000000..f6c0dfaf --- /dev/null +++ b/backend/src/core/indexer/processor/block-assets.processor.ts @@ -0,0 +1,149 @@ +import { BI } from '@ckb-lumos/bi'; +import { OnWorkerEvent, Processor, WorkerHost } from '@nestjs/bullmq'; +import { Logger } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; +import { Job } from 'bullmq'; +import { BlockchainServiceFactory } from 'src/core/blockchain/blockchain.factory'; +import { PrismaService } from 'src/core/database/prisma/prisma.service'; +import { IndexerQueueService } from '../indexer.queue'; +import { IndexerAssetsService } from '../service/assets.service'; +import { Cell, Transaction } from 'src/core/blockchain/blockchain.interface'; +import { IndexerServiceFactory } from '../indexer.factory'; +import * as Sentry from '@sentry/node'; +import { IndexerAssetsEvent } from '../flow/assets.flow'; + +export const INDEXER_BLOCK_ASSETS_QUEUE = 'indexer-block-assets-queue'; + +export interface IndexerBlockAssetsJobData { + chainId: number; + blockNumber: number; + targetBlockNumber: number; +} + +@Processor(INDEXER_BLOCK_ASSETS_QUEUE, { + concurrency: 100, +}) +export class IndexerBlockAssetsProcessor extends WorkerHost { + private logger = new Logger(IndexerBlockAssetsProcessor.name); + + constructor( + private blockchainServiceFactory: BlockchainServiceFactory, + private prismaService: PrismaService, + private moduleRef: ModuleRef, + ) { + super(); + } + + @OnWorkerEvent('active') + public onActive(job: Job) { + const { chainId, blockNumber } = job.data; + this.logger.debug(`Indexing block ${blockNumber} assets for chain ${chainId}`); + } + + @OnWorkerEvent('completed') + public onCompleted(job: Job) { + const { chainId, blockNumber } = job.data; + this.logger.log(`Indexing block ${blockNumber} assets for chain ${chainId} completed`); + } + + @OnWorkerEvent('failed') + public onFailed(job: Job, error: Error) { + const { chainId, blockNumber } = job.data; + this.logger.error(`Indexing block ${blockNumber} assets for chain ${chainId} failed`); + this.logger.error(error.stack); + Sentry.captureException(error); + } + + public async process(job: Job): Promise { + const { chainId, blockNumber, targetBlockNumber } = job.data; + const block = await this.getBlock(job); + + const assetTypeScripts = await this.prismaService.assetType.findMany({ where: { chainId } }); + await Promise.all( + block.transactions.map(async (tx, txIndex) => { + await this.updateInputAssetCellStatus(chainId, tx); + + for (let index = 0; index < tx.outputs.length; index += 1) { + const output = tx.outputs[index]; + if (!output.type) { + continue; + } + + const assetType = assetTypeScripts.find((assetType) => { + return ( + assetType.codeHash === output.type!.code_hash && + assetType.hashType === output.type!.hash_type + ); + }); + if (!assetType) { + continue; + } + + const indexerAssetsService = this.moduleRef.get(IndexerAssetsService); + const cell: Cell = { + block_number: block.header.number, + out_point: { + index: BI.from(index).toHexString(), + tx_hash: tx.hash, + }, + output, + output_data: tx.outputs_data[index], + tx_index: BI.from(txIndex).toHexString(), + }; + await indexerAssetsService.processAssetCell(chainId, cell, assetType); + } + }), + ); + + if (blockNumber < targetBlockNumber) { + const indexerQueueService = this.moduleRef.get(IndexerQueueService); + await indexerQueueService.addBlockAssetsJob({ + chainId, + blockNumber: blockNumber + 1, + targetBlockNumber, + }); + } else { + const indexerServiceFactory = this.moduleRef.get(IndexerServiceFactory); + const indexerService = await indexerServiceFactory.getService(chainId); + indexerService.assetsFlow.emit(IndexerAssetsEvent.BlockAssetsIndexed, block); + return; + } + } + + private async updateInputAssetCellStatus(chainId: number, tx: Transaction) { + for (const input of tx.inputs) { + const existingAsset = await this.prismaService.asset.findUnique({ + where: { + chainId_txHash_index: { + chainId: chainId, + txHash: input.previous_output.tx_hash, + index: input.previous_output.index, + }, + }, + }); + if (!existingAsset) { + continue; + } + + await this.prismaService.asset.update({ + where: { + chainId_txHash_index: { + chainId: chainId, + txHash: input.previous_output.tx_hash, + index: input.previous_output.index, + }, + }, + data: { + isLive: false, + }, + }); + } + } + + private async getBlock(job: Job) { + const { chainId, blockNumber } = job.data; + const blockchainService = this.blockchainServiceFactory.getService(chainId); + const block = await blockchainService.getBlockByNumber(BI.from(blockNumber).toHexString()); + return block; + } +} diff --git a/backend/src/core/indexer/processor/block.processor.ts b/backend/src/core/indexer/processor/block.processor.ts index ef777d30..3684fe46 100644 --- a/backend/src/core/indexer/processor/block.processor.ts +++ b/backend/src/core/indexer/processor/block.processor.ts @@ -5,12 +5,11 @@ import { ModuleRef } from '@nestjs/core'; import { Job } from 'bullmq'; import { BlockchainServiceFactory } from 'src/core/blockchain/blockchain.factory'; import { PrismaService } from 'src/core/database/prisma/prisma.service'; +import * as Sentry from '@sentry/node'; +import { toNumber } from 'lodash'; import { IndexerQueueService } from '../indexer.queue'; -import { IndexerAssetsService } from '../service/assets.service'; -import { Cell, Transaction } from 'src/core/blockchain/blockchain.interface'; import { IndexerServiceFactory } from '../indexer.factory'; -import { IndexerEvent } from '../indexer.service'; -import * as Sentry from '@sentry/node'; +import { IndexerTransactionsEvent } from '../flow/transactions.flow'; export const INDEXER_BLOCK_QUEUE = 'indexer-block-queue'; @@ -50,52 +49,41 @@ export class IndexerBlockProcessor extends WorkerHost { public onFailed(job: Job, error: Error) { const { chainId, blockNumber } = job.data; this.logger.error(`Indexing block ${blockNumber} for chain ${chainId} failed`); + this.logger.error(error.stack); Sentry.captureException(error); } public async process(job: Job): Promise { const { chainId, blockNumber, targetBlockNumber } = job.data; const block = await this.getBlock(job); + const indexerQueueService = this.moduleRef.get(IndexerQueueService); - const assetTypeScripts = await this.prismaService.assetType.findMany({ where: { chainId } }); - await Promise.all( - block.transactions.map(async (tx, txIndex) => { - await this.updateInputAssetCellStatus(chainId, tx); - - for (let index = 0; index < tx.outputs.length; index += 1) { - const output = tx.outputs[index]; - if (!output.type) { - continue; - } - - const assetType = assetTypeScripts.find((assetType) => { - return ( - assetType.codeHash === output.type!.code_hash && - assetType.hashType === output.type!.hash_type - ); - }); - if (!assetType) { - continue; - } - - const indexerAssetsService = this.moduleRef.get(IndexerAssetsService); - const cell: Cell = { - block_number: block.header.number, - out_point: { - index: BI.from(index).toHexString(), - tx_hash: tx.hash, - }, - output, - output_data: tx.outputs_data[index], - tx_index: BI.from(txIndex).toHexString(), - }; - await indexerAssetsService.processAssetCell(chainId, cell, assetType); - } - }), - ); + block.transactions.forEach((transaction, index) => { + indexerQueueService.addTransactionJob({ + chainId, + blockNumber, + index, + transaction, + }); + }); + await this.prismaService.block.upsert({ + where: { + chainId_number: { + chainId: job.data.chainId, + number: job.data.blockNumber, + }, + }, + update: {}, + create: { + chainId, + hash: block.header.hash, + number: BI.from(block.header.number).toNumber(), + timestamp: new Date(toNumber(block.header.timestamp)), + transactionsCount: block.transactions.length, + }, + }); if (blockNumber < targetBlockNumber) { - const indexerQueueService = this.moduleRef.get(IndexerQueueService); await indexerQueueService.addBlockJob({ chainId, blockNumber: blockNumber + 1, @@ -104,41 +92,11 @@ export class IndexerBlockProcessor extends WorkerHost { } else { const indexerServiceFactory = this.moduleRef.get(IndexerServiceFactory); const indexerService = await indexerServiceFactory.getService(chainId); - indexerService.emit(IndexerEvent.BlockIndexed, block); + indexerService.transactionsFlow.emit(IndexerTransactionsEvent.BlockIndexed, block); return; } } - private async updateInputAssetCellStatus(chainId: number, tx: Transaction) { - for (const input of tx.inputs) { - const existingAsset = await this.prismaService.asset.findUnique({ - where: { - chainId_txHash_index: { - chainId: chainId, - txHash: input.previous_output.tx_hash, - index: input.previous_output.index, - }, - }, - }); - if (!existingAsset) { - continue; - } - - await this.prismaService.asset.update({ - where: { - chainId_txHash_index: { - chainId: chainId, - txHash: input.previous_output.tx_hash, - index: input.previous_output.index, - }, - }, - data: { - isLive: false, - }, - }); - } - } - private async getBlock(job: Job) { const { chainId, blockNumber } = job.data; const blockchainService = this.blockchainServiceFactory.getService(chainId); diff --git a/backend/src/core/indexer/processor/lock.processor.ts b/backend/src/core/indexer/processor/lock.processor.ts index 7dff5f26..721a4b93 100644 --- a/backend/src/core/indexer/processor/lock.processor.ts +++ b/backend/src/core/indexer/processor/lock.processor.ts @@ -62,6 +62,7 @@ export class IndexerLockProcessor extends WorkerHost { this.logger.error( `Indexing lock script for chain ${chainId} with script hash ${computeScriptHash(script)} failed`, ); + this.logger.error(error.stack); Sentry.captureException(error); } diff --git a/backend/src/core/indexer/processor/transaction.processor.ts b/backend/src/core/indexer/processor/transaction.processor.ts new file mode 100644 index 00000000..2ab5d4e7 --- /dev/null +++ b/backend/src/core/indexer/processor/transaction.processor.ts @@ -0,0 +1,132 @@ +import { OnWorkerEvent, Processor, WorkerHost } from '@nestjs/bullmq'; +import { Logger } from '@nestjs/common'; +import { Job } from 'bullmq'; +import { PrismaService } from 'src/core/database/prisma/prisma.service'; +import { CoreService } from 'src/core/core.service'; +import { Transaction } from 'src/core/blockchain/blockchain.interface'; +import { Script, HashType } from '@ckb-lumos/lumos'; +import * as Sentry from '@sentry/node'; + +export const INDEXER_TRANSACTION_QUEUE = 'indexer-transaction-queue'; + +export interface IndexerTransactionJobData { + chainId: number; + blockNumber: number; + index: number; + transaction: Transaction; +} + +@Processor(INDEXER_TRANSACTION_QUEUE, { + concurrency: 100, +}) +export class IndexerTransactionProcessor extends WorkerHost { + private logger = new Logger(IndexerTransactionProcessor.name); + + constructor( + private prismaService: PrismaService, + private coreService: CoreService, + ) { + super(); + } + + @OnWorkerEvent('active') + public onActive(job: Job) { + const { chainId, transaction } = job.data; + this.logger.debug(`Indexing transaction ${transaction.hash} for chain ${chainId}`); + } + + @OnWorkerEvent('completed') + public onCompleted(job: Job) { + const { chainId, transaction } = job.data; + this.logger.log(`Indexing transaction ${transaction.hash} for chain ${chainId} completed`); + } + + @OnWorkerEvent('failed') + public onFailed(job: Job, error: Error) { + const { chainId, transaction } = job.data; + this.logger.error(`Indexing transaction ${transaction.hash} for chain ${chainId} failed`); + this.logger.error(error.stack); + Sentry.captureException(error); + } + + public async process(job: Job): Promise { + const { chainId, blockNumber, index, transaction } = job.data; + const assetTypeScripts = await this.prismaService.assetType.findMany({ + where: { chainId }, + }); + + const hasRgbppAssets = transaction.outputs.some((output) => { + if (!output.type) return false; + const typeScript: Script = { + codeHash: output.type.code_hash, + hashType: output.type.hash_type as HashType, + args: output.type.args, + }; + return assetTypeScripts.some((assetType) => { + return ( + assetType.codeHash === typeScript.codeHash && assetType.hashType === typeScript.hashType + ); + }); + }); + if (!hasRgbppAssets) { + this.logger.debug(`Transaction ${transaction.hash} does not contain any RGB++ assets`); + return; + } + + const { isRgbpp, btcTxid, leapDirection } = await this.parseTransaction(chainId, transaction); + await this.prismaService.transaction.upsert({ + where: { + chainId_hash: { + chainId, + hash: transaction.hash, + }, + }, + update: {}, + create: { + chainId, + hash: transaction.hash, + index, + blockNumber, + isCellbase: index === 0, + isRgbpp, + btcTxid, + leapDirection, + inputCount: transaction.inputs.length, + outputCount: transaction.outputs.length, + }, + }); + } + + private async parseTransaction(chainId: number, transaction: Transaction) { + const rgbppCell = transaction.outputs.find((output) => { + const lock: Script = { + codeHash: output.lock.code_hash, + hashType: output.lock.hash_type as HashType, + args: output.lock.args, + }; + return this.coreService.isRgbppLockScript(lock) || this.coreService.isBtcTimeLockScript(lock); + }); + + if (rgbppCell) { + let btcTxid: string | null = null; + try { + const args = this.coreService.parseRgbppLockArgs(rgbppCell.lock.args); + btcTxid = args.btcTxid; + } catch (err) { + this.logger.error(err); + } + + const leapDirection = await this.coreService.getLeapDirectionByCkbTx(chainId, transaction); + return { + isRgbpp: true, + btcTxid, + leapDirection, + }; + } + return { + isRgbpp: false, + btcTxid: null, + leapDirection: null, + }; + } +} diff --git a/backend/src/core/indexer/processor/type.processor.ts b/backend/src/core/indexer/processor/type.processor.ts index 0b7a0a6b..25879b41 100644 --- a/backend/src/core/indexer/processor/type.processor.ts +++ b/backend/src/core/indexer/processor/type.processor.ts @@ -45,6 +45,7 @@ export class IndexerTypeProcessor extends WorkerHost { this.logger.error( `Indexing lock script for chain ${chainId} with script hash ${computeScriptHash(script)} failed`, ); + this.logger.error(error.stack); Sentry.captureException(error); } diff --git a/backend/src/core/indexer/service/assets.service.ts b/backend/src/core/indexer/service/assets.service.ts index 403068a5..6bb62eb7 100644 --- a/backend/src/core/indexer/service/assets.service.ts +++ b/backend/src/core/indexer/service/assets.service.ts @@ -14,7 +14,7 @@ export class IndexerAssetsService { constructor( private prismaService: PrismaService, private moduleRef: ModuleRef, - ) { } + ) {} public async processAssetCell( chainId: number, @@ -44,9 +44,12 @@ export class IndexerAssetsService { script: typeScript, }); - const amount = assetType.fungible - ? BI.from(leToU128(remove0x(cell.output_data).slice(0, 32))).toString() - : 1; + let amount = '0'; + if (cell.output_data && cell.output_data !== '0x') { + amount = assetType.fungible + ? BI.from(leToU128(remove0x(cell.output_data).slice(0, 32))).toString() + : '1'; + } const data = { chainId, diff --git a/backend/src/middlewares/field-performance.middleware.ts b/backend/src/middlewares/field-performance.middleware.ts index c8ff97e5..d96b6ff1 100644 --- a/backend/src/middlewares/field-performance.middleware.ts +++ b/backend/src/middlewares/field-performance.middleware.ts @@ -1,19 +1,28 @@ -import { Logger } from '@nestjs/common'; import { FieldMiddleware, MiddlewareContext, NextFn } from '@nestjs/graphql'; +import * as Sentry from '@sentry/nestjs'; export const fieldPerformanceMiddleware: FieldMiddleware = async ( ctx: MiddlewareContext, next: NextFn, ) => { const now = performance.now(); + const memoryUsage = process.memoryUsage(); + const value = await next(); const executionTime = performance.now() - now; - if (executionTime > 300) { - const { path } = ctx.info; - logger.debug(`[${path.typename}.${path.key}]: ${executionTime}ms`); - } + const memoryUsageAfter = process.memoryUsage(); + + Sentry.setContext('graphql', { + executionTime, + field: ctx.info.fieldName, + parent: ctx.info.parentType.name, + }); + Sentry.setMeasurement('graphql.executionTime', executionTime, 'millisecond'); + Sentry.setMeasurement( + 'graphql.memoryUsage', + memoryUsageAfter.heapUsed - memoryUsage.heapUsed, + 'byte', + ); return value; }; - -const logger = new Logger(fieldPerformanceMiddleware.name); diff --git a/backend/src/modules/bitcoin/address/address.dataloader.ts b/backend/src/modules/bitcoin/address/address.dataloader.ts index 1dab6622..aee9bec6 100644 --- a/backend/src/modules/bitcoin/address/address.dataloader.ts +++ b/backend/src/modules/bitcoin/address/address.dataloader.ts @@ -80,6 +80,6 @@ export class BitcoinAddressTransactionsLoader }; } } -export type BitcoinAddressTransactionsLoaderType = DataLoader; +export type BitcoinAddressTransactionsLoaderType = DataLoader; export type BitcoinAddressTransactionsLoaderResponse = DataLoaderResponse; diff --git a/backend/src/modules/bitcoin/address/address.resolver.ts b/backend/src/modules/bitcoin/address/address.resolver.ts index 3625549b..ce7954dc 100644 --- a/backend/src/modules/bitcoin/address/address.resolver.ts +++ b/backend/src/modules/bitcoin/address/address.resolver.ts @@ -64,7 +64,10 @@ export class BitcoinAddressResolver { addressTxsLoader: BitcoinAddressTransactionsLoaderType, @Args('afterTxid', { nullable: true }) afterTxid?: string, ): Promise { - const list = await addressTxsLoader.load(`${address.address},${afterTxid}`); + const list = await addressTxsLoader.load({ + address: address.address, + afterTxid, + }); return list || null; } diff --git a/backend/src/modules/ckb/address/address.dataloader.ts b/backend/src/modules/ckb/address/address.dataloader.ts index 300b6809..20778108 100644 --- a/backend/src/modules/ckb/address/address.dataloader.ts +++ b/backend/src/modules/ckb/address/address.dataloader.ts @@ -12,24 +12,18 @@ import { NestDataLoader } from 'src/common/dataloader'; @Injectable() export class CkbAddressLoader - implements NestDataLoader + implements NestDataLoader { private logger = new Logger(CkbAddressLoader.name); constructor(private ckbExplorerService: CkbExplorerService) {} - public getOptions() { - return { - cacheKeyFn: (key: GetAddressParams) => key.address, - }; - } - public getBatchFunction() { - return async (batchParams: GetAddressParams[]) => { - this.logger.debug(`Loading CKB addresses info: ${batchParams}`); + return async (addresses: string[]) => { + this.logger.debug(`Loading CKB addresses info: ${addresses}`); const results = await Promise.allSettled( - batchParams.map(async (params) => { - const response = await this.ckbExplorerService.getAddress(params); + addresses.map(async (address) => { + const response = await this.ckbExplorerService.getAddress({ address }); return response.data.map((data) => data.attributes); }), ); @@ -37,14 +31,14 @@ export class CkbAddressLoader if (result.status === 'fulfilled') { return result.value; } - this.logger.error(`Requesting: ${batchParams[index]}, occurred error: ${result.reason}`); + this.logger.error(`Requesting: ${addresses[index]}, occurred error: ${result.reason}`); Sentry.captureException(result.reason); return null; }); }; } } -export type CkbAddressLoaderType = DataLoader; +export type CkbAddressLoaderType = DataLoader; export type CkbAddressLoaderResponse = DataLoaderResponse; export interface CkbAddressTransactionLoaderResult { diff --git a/backend/src/modules/ckb/address/address.resolver.ts b/backend/src/modules/ckb/address/address.resolver.ts index aeb2edd2..e2dd9807 100644 --- a/backend/src/modules/ckb/address/address.resolver.ts +++ b/backend/src/modules/ckb/address/address.resolver.ts @@ -31,7 +31,7 @@ export class CkbAddressResolver { @Parent() address: CkbAddress, @Loader(CkbAddressLoader) addressLoader: CkbAddressLoaderType, ): Promise { - const addressInfo = await addressLoader.load({ address: address.address }); + const addressInfo = await addressLoader.load(address.address); if (!addressInfo) { return null; } @@ -43,7 +43,7 @@ export class CkbAddressResolver { @Parent() address: CkbAddress, @Loader(CkbAddressLoader) addressLoader: CkbAddressLoaderType, ): Promise { - const addressInfo = await addressLoader.load({ address: address.address }); + const addressInfo = await addressLoader.load(address.address); if (!addressInfo) { return null; } @@ -82,7 +82,7 @@ export class CkbAddressResolver { @Parent() address: CkbAddress, @Loader(CkbAddressLoader) addressLoader: CkbAddressLoaderType, ): Promise { - const addressInfo = await addressLoader.load({ address: address.address }); + const addressInfo = await addressLoader.load(address.address); if (!addressInfo) { return null; } diff --git a/backend/src/modules/rgbpp/statistic/statistic.module.ts b/backend/src/modules/rgbpp/statistic/statistic.module.ts index cd635a99..7cb3c0d1 100644 --- a/backend/src/modules/rgbpp/statistic/statistic.module.ts +++ b/backend/src/modules/rgbpp/statistic/statistic.module.ts @@ -5,8 +5,6 @@ import { RgbppStatisticService } from './statistic.service'; import { CkbRpcModule } from 'src/core/ckb-rpc/ckb-rpc.module'; import { BitcoinApiModule } from 'src/core/bitcoin-api/bitcoin-api.module'; import { RgbppModule } from '../rgbpp.module'; -import { CronExpression, SchedulerRegistry } from '@nestjs/schedule'; -import { CronJob } from 'cron'; import { CkbScriptModule } from 'src/modules/ckb/script/script.module'; import { RgbppTransactionModule } from '../transaction/transaction.module'; @@ -23,16 +21,4 @@ import { RgbppTransactionModule } from '../transaction/transaction.module'; providers: [RgbppStatisticResolver, RgbppStatisticService], exports: [RgbppStatisticService], }) -export class RgbppStatisticModule { - constructor( - private rgbppStatisticService: RgbppStatisticService, - private schedulerRegistry: SchedulerRegistry, - ) { - this.schedulerRegistry.addCronJob( - 'collectLatest24HourRgbppTransactions', - new CronJob(CronExpression.EVERY_HOUR, () => { - this.rgbppStatisticService.collectLatest24HourRgbppTransactions(); - }), - ); - } -} +export class RgbppStatisticModule {} diff --git a/backend/src/modules/rgbpp/statistic/statistic.resolver.ts b/backend/src/modules/rgbpp/statistic/statistic.resolver.ts index 4cf149de..4dd3abf1 100644 --- a/backend/src/modules/rgbpp/statistic/statistic.resolver.ts +++ b/backend/src/modules/rgbpp/statistic/statistic.resolver.ts @@ -1,9 +1,8 @@ import { Args, Float, Int, Query, ResolveField, Resolver } from '@nestjs/graphql'; import { Layer, RgbppHolder, RgbppStatistic } from './statistic.model'; import { RgbppStatisticService } from './statistic.service'; -import { LeapDirection } from '../transaction/transaction.model'; import { OrderType } from 'src/modules/api.model'; -import { Holder } from '@prisma/client'; +import { Holder, LeapDirection } from '@prisma/client'; @Resolver(() => RgbppStatistic) export class RgbppStatisticResolver { @@ -41,22 +40,13 @@ export class RgbppStatisticResolver { @Args('leapDirection', { type: () => LeapDirection, nullable: true }) leapDirection?: LeapDirection, ): Promise { - const txids = await this.rgbppStatisticService.getLatest24L1Transactions(); - if (txids && leapDirection) { - const filteredTxhashs = await Promise.all( - txids.map(async (txid) => { - const direction = await this.rgbppStatisticService.getLeapDirectionByBtcTxid(txid); - return direction === leapDirection ? txid : null; - }), - ); - return filteredTxhashs.filter((txhash) => txhash !== null).length; - } - return txids?.length ?? null; + const transactions = await this.rgbppStatisticService.getLatest24L1Transactions(leapDirection); + return transactions?.length ?? null; } @ResolveField(() => Float, { nullable: true }) public async latest24HoursL2TransactionsCount(): Promise { - const txhashs = await this.rgbppStatisticService.getLatest24L2Transactions(); - return txhashs?.length ?? null; + const transactions = await this.rgbppStatisticService.getLatest24L2Transactions(); + return transactions.length ?? null; } } diff --git a/backend/src/modules/rgbpp/statistic/statistic.service.ts b/backend/src/modules/rgbpp/statistic/statistic.service.ts index 8b8dc404..4eee2f6f 100644 --- a/backend/src/modules/rgbpp/statistic/statistic.service.ts +++ b/backend/src/modules/rgbpp/statistic/statistic.service.ts @@ -1,25 +1,7 @@ -import { Inject, Injectable, Logger } from '@nestjs/common'; -import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager'; -import { ONE_DAY_MS } from 'src/common/date'; -import { CkbRpcWebsocketService } from 'src/core/ckb-rpc/ckb-rpc-websocket.service'; -import pLimit from 'p-limit'; -import { BI } from '@ckb-lumos/bi'; -import { CkbScriptService } from 'src/modules/ckb/script/script.service'; -import { CellType } from 'src/modules/ckb/script/script.model'; -import { isScriptEqual } from '@rgbpp-sdk/ckb'; -import { HashType, Script } from '@ckb-lumos/lumos'; -import { RgbppService } from '../rgbpp.service'; -import { RgbppTransactionService } from '../transaction/transaction.service'; -import { LeapDirection } from '../transaction/transaction.model'; +import { Injectable } from '@nestjs/common'; import { PrismaService } from 'src/core/database/prisma/prisma.service'; -import { Holder } from '@prisma/client'; - -// TODO: refactor the `Average Block Time` constant -// CKB testnet: ~8s, see https://pudge.explorer.nervos.org/charts/average-block-time -// CKB mainnet: ~10s, see https://explorer.nervos.org/charts/average-block-time -const CKB_24_HOURS_BLOCK_NUMBER = ONE_DAY_MS / 10000; -const RGBPP_ASSETS_CELL_TYPE = [CellType.XUDT, CellType.SUDT, CellType.DOB, CellType.MNFT]; -const limit = pLimit(200); +import { Holder, LeapDirection } from '@prisma/client'; +import { CKB_CHAIN_ID } from 'src/constants'; export interface GetRgbppAssetsHoldersParams { page: number; @@ -30,144 +12,38 @@ export interface GetRgbppAssetsHoldersParams { @Injectable() export class RgbppStatisticService { - private logger = new Logger(RgbppStatisticService.name); - private latest24L1TransactionsCacheKey = 'RgbppStatisticService:latest24L1Transactions'; - private latest24L2TransactionsCacheKey = 'RgbppStatisticService:latest24L2Transactions'; - private transactionLeapDirectionCachePrefix = 'RgbppStatisticService:leapDirection'; - - constructor( - private ckbRpcService: CkbRpcWebsocketService, - private ckbScriptService: CkbScriptService, - private rgbppTransactionService: RgbppTransactionService, - private rgbppService: RgbppService, - private prismaService: PrismaService, - @Inject(CACHE_MANAGER) protected cacheManager: Cache, - ) { - this.collectLatest24HourRgbppTransactions(); - } + constructor(private prismaService: PrismaService) { } - private get rgbppAssetsTypeScripts() { - return RGBPP_ASSETS_CELL_TYPE.map((type) => { - const service = this.ckbScriptService.getServiceByCellType(type); - const scripts = service.getScripts(); - return scripts; - }).flat(); - } - - public async getLatest24L1Transactions() { - const txids = await this.cacheManager.get(this.latest24L1TransactionsCacheKey); - if (txids) { - return txids as string[]; - } - return null; + public async getLatest24L1Transactions(leapDirection?: LeapDirection) { + const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); + const transactions = await this.prismaService.transaction.findMany({ + where: { + chainId: CKB_CHAIN_ID, + isRgbpp: true, + block: { + timestamp: { + gte: twentyFourHoursAgo, + }, + }, + ...(leapDirection ? { leapDirection } : {}), + }, + }); + return transactions; } public async getLatest24L2Transactions() { - const txhashes = await this.cacheManager.get(this.latest24L2TransactionsCacheKey); - if (txhashes) { - return txhashes as string[]; - } - return null; - } - - public async getLeapDirectionByBtcTxid(btcTxid: string) { - const leapDirection = await this.cacheManager.get( - `${this.transactionLeapDirectionCachePrefix}:${btcTxid}`, - ); - return leapDirection; - } - - public async collectLatest24HourRgbppTransactions() { - const tipBlockNumber = await this.ckbRpcService.getTipBlockNumber(); - this.logger.log( - `Collect latest 24 hours RGB++ transactions, tip block number: ${tipBlockNumber}`, - ); - - const blocks = await Promise.all( - Array.from({ length: CKB_24_HOURS_BLOCK_NUMBER }).map((_, index) => { - return limit(() => - this.getRgbppTxsByCkbBlockNumber(BI.from(tipBlockNumber).sub(index).toHexString()), - ); - }), - ); - const btcTxIds = blocks.flatMap((block) => block.btcTxIds); - const ckbTxHashes = blocks.flatMap((block) => block.ckbTxHashes); - await this.cacheManager.set(this.latest24L1TransactionsCacheKey, btcTxIds, ONE_DAY_MS); - await this.cacheManager.set(this.latest24L2TransactionsCacheKey, ckbTxHashes, ONE_DAY_MS); - - const leapDirections = blocks.flatMap((block) => Array.from(block.leapDirectionMap.entries())); - await Promise.all( - leapDirections.map(async ([btcTxid, leapDirection]) => { - await this.cacheManager.set( - `${this.transactionLeapDirectionCachePrefix}:${btcTxid}`, - leapDirection, - ONE_DAY_MS, - ); - }), - ); - this.logger.log(`Collect latest 24 hours RGB++ transactions done`); - return { - btcTxIds, - ckbTxHashes, - leapDirectionMap: leapDirections, - }; - } - - private async getRgbppTxsByCkbBlockNumber(blockNumber: string) { - const block = await this.ckbRpcService.getBlockByNumber(blockNumber); - const rgbppL1TxIds: string[] = []; - const rgbppL2Txhashes: string[] = []; - const leapDirectionMap = new Map(); - - for (const tx of block.transactions) { - const rgbppCell = tx.outputs.find((output) => { - const lock: Script = { - codeHash: output.lock.code_hash, - hashType: output.lock.hash_type as HashType, - args: output.lock.args, - }; - return ( - this.rgbppService.isRgbppLockScript(lock) || this.rgbppService.isBtcTimeLockScript(lock) - ); - }); - if (rgbppCell) { - try { - const { btcTxid } = this.rgbppService.parseRgbppLockArgs(rgbppCell.lock.args); - rgbppL1TxIds.push(btcTxid); - - // Get leap direction and cache it - const leapDirection = await this.rgbppTransactionService.getLeapDirectionByCkbTx(tx); - if (leapDirection) { - leapDirectionMap.set(btcTxid, leapDirection); - } - } catch (err) { - this.logger.error(err); - } - continue; - } - - const isRgbppL2Tx = tx.outputs.some((output) => { - if (!output.type) { - return false; - } - return this.rgbppAssetsTypeScripts.some((script) => - isScriptEqual(script, { - codeHash: output.type!.code_hash, - hashType: output.type!.hash_type as HashType, - args: '0x', - }), - ); - }); - if (isRgbppL2Tx) { - rgbppL2Txhashes.push(tx.hash); - } - } - - return { - btcTxIds: rgbppL1TxIds, - ckbTxHashes: rgbppL2Txhashes, - leapDirectionMap, - }; + const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); + const transactions = await this.prismaService.transaction.findMany({ + where: { + isRgbpp: false, + block: { + timestamp: { + gte: twentyFourHoursAgo, + }, + }, + }, + }); + return transactions; } public async getRgbppAssetsHoldersCount(isLayer1: boolean): Promise { diff --git a/backend/src/modules/rgbpp/transaction/transaction.model.ts b/backend/src/modules/rgbpp/transaction/transaction.model.ts index d3806158..b0918b6c 100644 --- a/backend/src/modules/rgbpp/transaction/transaction.model.ts +++ b/backend/src/modules/rgbpp/transaction/transaction.model.ts @@ -1,13 +1,8 @@ import { Field, Int, ObjectType, registerEnumType } from '@nestjs/graphql'; +import { Block, LeapDirection, Transaction } from '@prisma/client'; import { toNumber } from 'lodash'; import * as CkbExplorer from 'src/core/ckb-explorer/ckb-explorer.interface'; -export enum LeapDirection { - LeapIn = 'leap_in', - LeapOut = 'leap_out', - Within = 'within', -} - registerEnumType(LeapDirection, { name: 'LeapDirection', }); @@ -26,7 +21,7 @@ export class RgbppTransaction { @Field(() => Date, { nullable: true }) blockTime: Date | null; - public static from(tx: CkbExplorer.RgbppTransaction) { + public static fromRgbppTransaction(tx: CkbExplorer.RgbppTransaction) { return { ckbTxHash: tx.tx_hash, btcTxid: tx.rgb_txid, @@ -43,6 +38,15 @@ export class RgbppTransaction { blockTime: tx.block_timestamp ? new Date(toNumber(tx.block_timestamp)) : null, }; } + + public static from(tx: Transaction & { block: Block }) { + return { + ckbTxHash: tx.hash, + btcTxid: tx.btcTxid, + blockNumber: tx.blockNumber, + blockTime: tx.block.timestamp ? new Date(tx.block.timestamp) : null, + }; + } } @ObjectType({ description: 'RGB++ latest transaction list' }) diff --git a/backend/src/modules/rgbpp/transaction/transaction.resolver.ts b/backend/src/modules/rgbpp/transaction/transaction.resolver.ts index 03c4c777..8f60cdf6 100644 --- a/backend/src/modules/rgbpp/transaction/transaction.resolver.ts +++ b/backend/src/modules/rgbpp/transaction/transaction.resolver.ts @@ -11,10 +11,11 @@ import { BitcoinTransactionLoader, BitcoinTransactionLoaderType, } from 'src/modules/bitcoin/transaction/transaction.dataloader'; -import { RgbppTransaction, RgbppLatestTransactionList, LeapDirection } from './transaction.model'; +import { RgbppTransaction, RgbppLatestTransactionList } from './transaction.model'; import { RgbppTransactionLoader, RgbppTransactionLoaderType } from './transaction.dataloader'; import { BitcoinApiService } from 'src/core/bitcoin-api/bitcoin-api.service'; import { BI } from '@ckb-lumos/bi'; +import { LeapDirection } from '@prisma/client'; @Resolver(() => RgbppTransaction) export class RgbppTransactionResolver { @@ -27,31 +28,37 @@ export class RgbppTransactionResolver { public async getRecentTransactions( @Args('limit', { type: () => Int, nullable: true }) limit: number = 10, ): Promise { - const { txs: l1Txs } = await this.rgbppTransactionService.getLatestTransactions(1, limit); - const l2Txs = await this.rgbppTransactionService.getLatestL2Transactions(limit); - const txs = [...l1Txs, ...l2Txs.txs].sort((a, b) => b.blockNumber - a.blockNumber); + const transactions = await this.rgbppTransactionService.getLatestTransactions(limit); return { - txs: txs.slice(0, limit), - total: txs.length, - pageSize: limit, + txs: transactions, + total: transactions.length, + pageSize: 1, }; } @Query(() => RgbppLatestTransactionList, { name: 'rgbppLatestL1Transactions' }) public async getLatestL1Transactions( - @Args('page', { type: () => Int, nullable: true }) page: number = 1, - @Args('pageSize', { type: () => Int, nullable: true }) pageSize: number = 10, + @Args('limit', { type: () => Int, nullable: true }) limit: number = 10, ): Promise { - const txs = await this.rgbppTransactionService.getLatestTransactions(page, pageSize); - return txs; + const transactions = await this.rgbppTransactionService.getLatestL1Transactions(limit); + return { + txs: transactions, + total: transactions.length, + pageSize: 1, + }; } @Query(() => RgbppLatestTransactionList, { name: 'rgbppLatestL2Transactions' }) public async getLatestL2Transactions( @Args('limit', { type: () => Int, nullable: true }) limit: number = 10, ): Promise { - return this.rgbppTransactionService.getLatestL2Transactions(limit); + const transactions = await this.rgbppTransactionService.getLatestL2Transactions(limit); + return { + txs: transactions, + total: transactions.length, + pageSize: 1, + }; } @Query(() => RgbppTransaction, { name: 'rgbppTransaction', nullable: true }) diff --git a/backend/src/modules/rgbpp/transaction/transaction.service.ts b/backend/src/modules/rgbpp/transaction/transaction.service.ts index 39bccf79..01ae041c 100644 --- a/backend/src/modules/rgbpp/transaction/transaction.service.ts +++ b/backend/src/modules/rgbpp/transaction/transaction.service.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { BitcoinApiService } from 'src/core/bitcoin-api/bitcoin-api.service'; import { CkbExplorerService } from 'src/core/ckb-explorer/ckb-explorer.service'; -import { RgbppTransaction, RgbppLatestTransactionList, LeapDirection } from './transaction.model'; +import { RgbppTransaction, RgbppLatestTransactionList } from './transaction.model'; import { ConfigService } from '@nestjs/config'; import { Env } from 'src/env'; import { CkbRpcWebsocketService } from 'src/core/ckb-rpc/ckb-rpc-websocket.service'; @@ -13,6 +13,9 @@ import { BI, HashType } from '@ckb-lumos/lumos'; import { Cacheable } from 'src/decorators/cacheable.decorator'; import { ONE_MONTH_MS } from 'src/common/date'; import { CkbScriptService } from 'src/modules/ckb/script/script.service'; +import { LeapDirection } from '@prisma/client'; +import { PrismaService } from 'src/core/database/prisma/prisma.service'; +import { CKB_CHAIN_ID } from 'src/constants'; @Injectable() export class RgbppTransactionService { @@ -21,72 +24,51 @@ export class RgbppTransactionService { constructor( private ckbExplorerService: CkbExplorerService, private ckbRpcService: CkbRpcWebsocketService, - private ckbScriptService: CkbScriptService, + private prismaService: PrismaService, private rgbppService: RgbppService, private bitcoinApiService: BitcoinApiService, private configService: ConfigService, ) { } - public async getLatestTransactions( - page: number, - pageSize: number, - ): Promise { + public async getLatestTransactions(limit: number) { + const transactions = await this.prismaService.transaction.findMany({ + where: { + chainId: CKB_CHAIN_ID, + }, + orderBy: { + blockNumber: 'desc', + }, + include: { + block: true, + }, + take: limit, + }); + return transactions.map(RgbppTransaction.from); + } + + public async getLatestL1Transactions(limit: number) { const response = await this.ckbExplorerService.getRgbppTransactions({ - page, - pageSize, + page: 1, + pageSize: limit, }); - return { - txs: response.data.ckb_transactions.map((tx) => RgbppTransaction.from(tx)), - total: response.meta.total, - pageSize: response.meta.page_size, - }; + return response.data.ckb_transactions.map((tx) => RgbppTransaction.fromRgbppTransaction(tx)); } public async getLatestL2Transactions(limit: number) { - const rgbppL2Txs: RgbppTransaction[] = []; - const tipBlockNumber = await this.ckbRpcService.getTipBlockNumber(); - let blockNumber = BI.from(tipBlockNumber); - while (rgbppL2Txs.length < limit) { - const txss = await Promise.all( - Array({ length: limit }).map(async (_, index) => { - const block = await this.ckbRpcService.getBlockByNumber( - blockNumber.sub(index).toHexString(), - ); - - const ckbTxs = block.transactions.filter((tx) => { - return tx.outputs.some((output) => { - if (!output.type) { - return false; - } - return this.ckbScriptService.matchScript({ - codeHash: output.type.code_hash, - hashType: output.type.hash_type as HashType, - args: output.type.args, - }); - }); - }); - - const txs = await Promise.all( - ckbTxs.map((tx) => this.ckbExplorerService.getTransaction(tx.hash)), - ); - return txs; - }), - ); - const flatTxs = txss.flat().filter(res => { - const attr = res?.data?.attributes; - if (!attr) return false; - return attr.is_rgb_transaction !== true && attr.is_btc_time_lock !== true; - }).map((tx) => RgbppTransaction.fromCkbTransaction(tx.data.attributes)); - - rgbppL2Txs.push(...flatTxs); - blockNumber = blockNumber.sub(limit); - } - - return { - txs: rgbppL2Txs.slice(0, limit), - total: limit, - pageSize: limit, - }; + const transactions = await this.prismaService.transaction.findMany({ + where: { + chainId: CKB_CHAIN_ID, + isRgbpp: false, + }, + orderBy: { + blockNumber: 'desc', + }, + include: { + block: true, + }, + take: limit, + }); + return transactions.map(RgbppTransaction.from); } public async getTransactionByCkbTxHash(txHash: string): Promise { @@ -123,8 +105,7 @@ export class RgbppTransactionService { @Cacheable({ namespace: 'RgbppTransactionService', - key: (tx: CkbRpcInterface.Transaction) => - `getLeapDirectionByCkbTx:${tx.hash}`, + key: (tx: CkbRpcInterface.Transaction) => `getLeapDirectionByCkbTx:${tx.hash}`, ttl: ONE_MONTH_MS, }) public async getLeapDirectionByCkbTx(ckbTx: CkbRpcInterface.Transaction) { diff --git a/backend/src/schema.gql b/backend/src/schema.gql index f1599b10..d5ba5043 100644 --- a/backend/src/schema.gql +++ b/backend/src/schema.gql @@ -332,7 +332,7 @@ type Query { btcBlock(hashOrHeight: String!): BitcoinBlock btcTransaction(txid: String!): BitcoinTransaction rgbppLatestTransactions(limit: Int): RgbppLatestTransactionList! - rgbppLatestL1Transactions(page: Int, pageSize: Int): RgbppLatestTransactionList! + rgbppLatestL1Transactions(limit: Int): RgbppLatestTransactionList! rgbppLatestL2Transactions(limit: Int): RgbppLatestTransactionList! rgbppTransaction(txidOrTxHash: String!): RgbppTransaction rgbppAddress(address: String!): RgbppAddress diff --git a/frontend/src/app/[lang]/explorer/btc/page.tsx b/frontend/src/app/[lang]/explorer/btc/page.tsx index 0bc502c5..b2f51af0 100644 --- a/frontend/src/app/[lang]/explorer/btc/page.tsx +++ b/frontend/src/app/[lang]/explorer/btc/page.tsx @@ -11,9 +11,9 @@ import { graphQLClient } from '@/lib/graphql' export const revalidate = 5 -const queryRgbppLatestTransactions = graphql(` - query RgbppLatestL1Transactions($page: Int!, $pageSize: Int!) { - rgbppLatestL1Transactions(page: $page, pageSize: $pageSize) { +const query = graphql(` + query RgbppLatestL1Transactions($limit: Int!) { + rgbppLatestL1Transactions(limit: $limit) { txs { ckbTxHash btcTxid @@ -52,10 +52,7 @@ const queryRgbppLatestTransactions = graphql(` export default async function Page() { const i18n = getI18nFromHeaders() - const { rgbppLatestL1Transactions } = await graphQLClient.request(queryRgbppLatestTransactions, { - page: 1, - pageSize: 10, - }) + const { rgbppLatestL1Transactions } = await graphQLClient.request(query, { limit: 10 }) return ( diff --git a/frontend/src/gql/gql.ts b/frontend/src/gql/gql.ts index 3b9b04d7..2c6efecd 100644 --- a/frontend/src/gql/gql.ts +++ b/frontend/src/gql/gql.ts @@ -25,7 +25,7 @@ const documents = { "\n query CkbBlock($hashOrHeight: String!) {\n ckbBlock(heightOrHash: $hashOrHeight) {\n version\n hash\n number\n timestamp\n transactionsCount\n totalFee\n miner {\n address\n shannon\n transactionsCount\n }\n reward\n size\n confirmations\n }\n }\n": types.CkbBlockDocument, "\n query CkbBlockTransactions($hashOrHeight: String!) {\n ckbBlock(heightOrHash: $hashOrHeight) {\n timestamp\n transactions {\n isCellbase\n blockNumber\n hash\n fee\n size\n feeRate\n confirmations\n outputs {\n txHash\n index\n capacity\n lock {\n codeHash\n hashType\n args\n }\n type {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n status {\n consumed\n txHash\n index\n }\n }\n inputs {\n status {\n consumed\n txHash\n index\n }\n txHash\n index\n capacity\n lock {\n codeHash\n hashType\n args\n }\n type {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n }\n }\n }\n }\n": types.CkbBlockTransactionsDocument, "\n query BtcChainInfo {\n btcChainInfo {\n tipBlockHeight\n tipBlockHash\n difficulty\n transactionsCountIn24Hours\n fees {\n fastest\n halfHour\n hour\n economy\n minimum\n }\n }\n rgbppStatistic {\n latest24HoursL1TransactionsCount\n holdersCount(layer: L1)\n }\n }\n ": types.BtcChainInfoDocument, - "\n query RgbppLatestL1Transactions($page: Int!, $pageSize: Int!) {\n rgbppLatestL1Transactions(page: $page, pageSize: $pageSize) {\n txs {\n ckbTxHash\n btcTxid\n leapDirection\n blockNumber\n timestamp\n ckbTransaction {\n outputs {\n txHash\n index\n capacity\n cellType\n lock {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n }\n status {\n consumed\n txHash\n index\n }\n }\n }\n }\n total\n pageSize\n }\n }\n": types.RgbppLatestL1TransactionsDocument, + "\n query RgbppLatestL1Transactions($limit: Int!) {\n rgbppLatestL1Transactions(limit: $limit) {\n txs {\n ckbTxHash\n btcTxid\n leapDirection\n blockNumber\n timestamp\n ckbTransaction {\n outputs {\n txHash\n index\n capacity\n cellType\n lock {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n }\n status {\n consumed\n txHash\n index\n }\n }\n }\n }\n total\n pageSize\n }\n }\n": types.RgbppLatestL1TransactionsDocument, "\n query CkbChainInfo {\n ckbChainInfo {\n tipBlockNumber\n fees {\n fast\n slow\n average\n }\n }\n rgbppStatistic {\n latest24HoursL2TransactionsCount\n holdersCount(layer: L2)\n }\n }\n ": types.CkbChainInfoDocument, "\n query RgbppLatestL2Transactions($limit: Int!) {\n rgbppLatestL2Transactions(limit: $limit) {\n txs {\n ckbTxHash\n leapDirection\n timestamp\n ckbTransaction {\n outputs {\n txHash\n index\n capacity\n cellType\n lock {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n }\n status {\n consumed\n txHash\n index\n }\n }\n }\n }\n total\n pageSize\n }\n }\n": types.RgbppLatestL2TransactionsDocument, "\n query RgbppTransaction($txidOrTxHash: String!) {\n rgbppTransaction(txidOrTxHash: $txidOrTxHash) {\n ckbTxHash\n btcTxid\n leapDirection\n blockNumber\n timestamp\n btcTransaction {\n txid\n blockHeight\n blockHash\n size\n fee\n feeRate\n confirmed\n confirmations\n vin {\n txid\n vout\n isCoinbase\n prevout {\n txid\n vout\n value\n address {\n address\n }\n status {\n spent\n txid\n vin\n }\n }\n }\n vout {\n txid\n vout\n value\n address {\n address\n }\n status {\n spent\n txid\n vin\n }\n }\n }\n ckbTransaction {\n isCellbase\n blockNumber\n hash\n fee\n feeRate\n outputs {\n txHash\n index\n capacity\n cellType\n type {\n codeHash\n hashType\n args\n }\n lock {\n codeHash\n hashType\n args\n }\n status {\n consumed\n txHash\n index\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n }\n inputs {\n txHash\n index\n capacity\n cellType\n type {\n codeHash\n hashType\n args\n }\n lock {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n status {\n consumed\n txHash\n index\n }\n }\n block {\n timestamp\n hash\n }\n }\n }\n }\n": types.RgbppTransactionDocument, @@ -103,7 +103,7 @@ export function graphql(source: "\n query BtcChainInfo {\n btcChainI /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query RgbppLatestL1Transactions($page: Int!, $pageSize: Int!) {\n rgbppLatestL1Transactions(page: $page, pageSize: $pageSize) {\n txs {\n ckbTxHash\n btcTxid\n leapDirection\n blockNumber\n timestamp\n ckbTransaction {\n outputs {\n txHash\n index\n capacity\n cellType\n lock {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n }\n status {\n consumed\n txHash\n index\n }\n }\n }\n }\n total\n pageSize\n }\n }\n"): (typeof documents)["\n query RgbppLatestL1Transactions($page: Int!, $pageSize: Int!) {\n rgbppLatestL1Transactions(page: $page, pageSize: $pageSize) {\n txs {\n ckbTxHash\n btcTxid\n leapDirection\n blockNumber\n timestamp\n ckbTransaction {\n outputs {\n txHash\n index\n capacity\n cellType\n lock {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n }\n status {\n consumed\n txHash\n index\n }\n }\n }\n }\n total\n pageSize\n }\n }\n"]; +export function graphql(source: "\n query RgbppLatestL1Transactions($limit: Int!) {\n rgbppLatestL1Transactions(limit: $limit) {\n txs {\n ckbTxHash\n btcTxid\n leapDirection\n blockNumber\n timestamp\n ckbTransaction {\n outputs {\n txHash\n index\n capacity\n cellType\n lock {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n }\n status {\n consumed\n txHash\n index\n }\n }\n }\n }\n total\n pageSize\n }\n }\n"): (typeof documents)["\n query RgbppLatestL1Transactions($limit: Int!) {\n rgbppLatestL1Transactions(limit: $limit) {\n txs {\n ckbTxHash\n btcTxid\n leapDirection\n blockNumber\n timestamp\n ckbTransaction {\n outputs {\n txHash\n index\n capacity\n cellType\n lock {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n }\n status {\n consumed\n txHash\n index\n }\n }\n }\n }\n total\n pageSize\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index c59494fc..4f00ac66 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -372,8 +372,7 @@ export type QueryRgbppCoinsArgs = { export type QueryRgbppLatestL1TransactionsArgs = { - page?: InputMaybe; - pageSize?: InputMaybe; + limit?: InputMaybe; }; @@ -444,7 +443,7 @@ export type RgbppCoinAmountArgs = { /** RGB++ Coin */ export type RgbppCoinHoldersArgs = { - layer: Layer; + layer?: InputMaybe; order?: InputMaybe; page?: InputMaybe; pageSize?: InputMaybe; @@ -453,7 +452,7 @@ export type RgbppCoinHoldersArgs = { /** RGB++ Coin */ export type RgbppCoinHoldersCountArgs = { - layer: Layer; + layer?: InputMaybe; }; @@ -641,8 +640,7 @@ export type BtcChainInfoQueryVariables = Exact<{ [key: string]: never; }>; export type BtcChainInfoQuery = { __typename?: 'Query', btcChainInfo: { __typename?: 'BitcoinChainInfo', tipBlockHeight: number, tipBlockHash: string, difficulty: number, transactionsCountIn24Hours: number, fees: { __typename?: 'BitcoinFees', fastest: number, halfHour: number, hour: number, economy: number, minimum: number } }, rgbppStatistic: { __typename?: 'RgbppStatistic', latest24HoursL1TransactionsCount?: number | null, holdersCount: number } }; export type RgbppLatestL1TransactionsQueryVariables = Exact<{ - page: Scalars['Int']['input']; - pageSize: Scalars['Int']['input']; + limit: Scalars['Int']['input']; }>; @@ -723,7 +721,7 @@ export const BtcBlockTransactionDocument = {"kind":"Document","definitions":[{"k export const CkbBlockDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CkbBlock"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"hashOrHeight"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ckbBlock"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"heightOrHash"},"value":{"kind":"Variable","name":{"kind":"Name","value":"hashOrHeight"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"number"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"transactionsCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalFee"}},{"kind":"Field","name":{"kind":"Name","value":"miner"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"address"}},{"kind":"Field","name":{"kind":"Name","value":"shannon"}},{"kind":"Field","name":{"kind":"Name","value":"transactionsCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"reward"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"confirmations"}}]}}]}}]} as unknown as DocumentNode; export const CkbBlockTransactionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CkbBlockTransactions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"hashOrHeight"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ckbBlock"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"heightOrHash"},"value":{"kind":"Variable","name":{"kind":"Name","value":"hashOrHeight"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"transactions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"isCellbase"}},{"kind":"Field","name":{"kind":"Name","value":"blockNumber"}},{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"fee"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"feeRate"}},{"kind":"Field","name":{"kind":"Name","value":"confirmations"}},{"kind":"Field","name":{"kind":"Name","value":"outputs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"capacity"}},{"kind":"Field","name":{"kind":"Name","value":"lock"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"type"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"xudtInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"symbol"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"decimal"}},{"kind":"Field","name":{"kind":"Name","value":"typeHash"}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"consumed"}},{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"inputs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"consumed"}},{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}}]}},{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"capacity"}},{"kind":"Field","name":{"kind":"Name","value":"lock"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"type"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"xudtInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"symbol"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"decimal"}},{"kind":"Field","name":{"kind":"Name","value":"typeHash"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const BtcChainInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BtcChainInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"btcChainInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tipBlockHeight"}},{"kind":"Field","name":{"kind":"Name","value":"tipBlockHash"}},{"kind":"Field","name":{"kind":"Name","value":"difficulty"}},{"kind":"Field","name":{"kind":"Name","value":"transactionsCountIn24Hours"}},{"kind":"Field","name":{"kind":"Name","value":"fees"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fastest"}},{"kind":"Field","name":{"kind":"Name","value":"halfHour"}},{"kind":"Field","name":{"kind":"Name","value":"hour"}},{"kind":"Field","name":{"kind":"Name","value":"economy"}},{"kind":"Field","name":{"kind":"Name","value":"minimum"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"rgbppStatistic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"latest24HoursL1TransactionsCount"}},{"kind":"Field","name":{"kind":"Name","value":"holdersCount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"layer"},"value":{"kind":"EnumValue","value":"L1"}}]}]}}]}}]} as unknown as DocumentNode; -export const RgbppLatestL1TransactionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"RgbppLatestL1Transactions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"page"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rgbppLatestL1Transactions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"page"}}},{"kind":"Argument","name":{"kind":"Name","value":"pageSize"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ckbTxHash"}},{"kind":"Field","name":{"kind":"Name","value":"btcTxid"}},{"kind":"Field","name":{"kind":"Name","value":"leapDirection"}},{"kind":"Field","name":{"kind":"Name","value":"blockNumber"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"ckbTransaction"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"outputs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"capacity"}},{"kind":"Field","name":{"kind":"Name","value":"cellType"}},{"kind":"Field","name":{"kind":"Name","value":"lock"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"xudtInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"symbol"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"decimal"}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"consumed"}},{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"pageSize"}}]}}]}}]} as unknown as DocumentNode; +export const RgbppLatestL1TransactionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"RgbppLatestL1Transactions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rgbppLatestL1Transactions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ckbTxHash"}},{"kind":"Field","name":{"kind":"Name","value":"btcTxid"}},{"kind":"Field","name":{"kind":"Name","value":"leapDirection"}},{"kind":"Field","name":{"kind":"Name","value":"blockNumber"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"ckbTransaction"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"outputs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"capacity"}},{"kind":"Field","name":{"kind":"Name","value":"cellType"}},{"kind":"Field","name":{"kind":"Name","value":"lock"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"xudtInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"symbol"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"decimal"}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"consumed"}},{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"pageSize"}}]}}]}}]} as unknown as DocumentNode; export const CkbChainInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CkbChainInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ckbChainInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tipBlockNumber"}},{"kind":"Field","name":{"kind":"Name","value":"fees"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fast"}},{"kind":"Field","name":{"kind":"Name","value":"slow"}},{"kind":"Field","name":{"kind":"Name","value":"average"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"rgbppStatistic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"latest24HoursL2TransactionsCount"}},{"kind":"Field","name":{"kind":"Name","value":"holdersCount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"layer"},"value":{"kind":"EnumValue","value":"L2"}}]}]}}]}}]} as unknown as DocumentNode; export const RgbppLatestL2TransactionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"RgbppLatestL2Transactions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rgbppLatestL2Transactions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ckbTxHash"}},{"kind":"Field","name":{"kind":"Name","value":"leapDirection"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"ckbTransaction"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"outputs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"capacity"}},{"kind":"Field","name":{"kind":"Name","value":"cellType"}},{"kind":"Field","name":{"kind":"Name","value":"lock"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"xudtInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"symbol"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"decimal"}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"consumed"}},{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"pageSize"}}]}}]}}]} as unknown as DocumentNode; export const RgbppTransactionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"RgbppTransaction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"txidOrTxHash"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rgbppTransaction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"txidOrTxHash"},"value":{"kind":"Variable","name":{"kind":"Name","value":"txidOrTxHash"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ckbTxHash"}},{"kind":"Field","name":{"kind":"Name","value":"btcTxid"}},{"kind":"Field","name":{"kind":"Name","value":"leapDirection"}},{"kind":"Field","name":{"kind":"Name","value":"blockNumber"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"btcTransaction"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txid"}},{"kind":"Field","name":{"kind":"Name","value":"blockHeight"}},{"kind":"Field","name":{"kind":"Name","value":"blockHash"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"fee"}},{"kind":"Field","name":{"kind":"Name","value":"feeRate"}},{"kind":"Field","name":{"kind":"Name","value":"confirmed"}},{"kind":"Field","name":{"kind":"Name","value":"confirmations"}},{"kind":"Field","name":{"kind":"Name","value":"vin"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txid"}},{"kind":"Field","name":{"kind":"Name","value":"vout"}},{"kind":"Field","name":{"kind":"Name","value":"isCoinbase"}},{"kind":"Field","name":{"kind":"Name","value":"prevout"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txid"}},{"kind":"Field","name":{"kind":"Name","value":"vout"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"address"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"address"}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"spent"}},{"kind":"Field","name":{"kind":"Name","value":"txid"}},{"kind":"Field","name":{"kind":"Name","value":"vin"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"vout"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txid"}},{"kind":"Field","name":{"kind":"Name","value":"vout"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"address"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"address"}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"spent"}},{"kind":"Field","name":{"kind":"Name","value":"txid"}},{"kind":"Field","name":{"kind":"Name","value":"vin"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"ckbTransaction"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"isCellbase"}},{"kind":"Field","name":{"kind":"Name","value":"blockNumber"}},{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"fee"}},{"kind":"Field","name":{"kind":"Name","value":"feeRate"}},{"kind":"Field","name":{"kind":"Name","value":"outputs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"capacity"}},{"kind":"Field","name":{"kind":"Name","value":"cellType"}},{"kind":"Field","name":{"kind":"Name","value":"type"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lock"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"consumed"}},{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}}]}},{"kind":"Field","name":{"kind":"Name","value":"xudtInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"symbol"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"decimal"}},{"kind":"Field","name":{"kind":"Name","value":"typeHash"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"inputs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"capacity"}},{"kind":"Field","name":{"kind":"Name","value":"cellType"}},{"kind":"Field","name":{"kind":"Name","value":"type"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lock"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"xudtInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"symbol"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"decimal"}},{"kind":"Field","name":{"kind":"Name","value":"typeHash"}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"consumed"}},{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"block"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"hash"}}]}}]}}]}}]}}]} as unknown as DocumentNode;