diff --git a/backend/src/constants.ts b/backend/src/constants.ts index d295f02f..2676143c 100644 --- a/backend/src/constants.ts +++ b/backend/src/constants.ts @@ -1,5 +1,13 @@ +import * as RgbppBtc from '@rgbpp-sdk/btc'; + export enum NetworkType { mainnet = 'mainnet', testnet = 'testnet', signet = 'signet', } + +export const BtcNetworkTypeMap: Record = { + [NetworkType.mainnet]: RgbppBtc.NetworkType.MAINNET, + [NetworkType.testnet]: RgbppBtc.NetworkType.TESTNET, + [NetworkType.signet]: RgbppBtc.NetworkType.TESTNET, +}; diff --git a/backend/src/core/ckb-rpc/ckb-rpc.interface.ts b/backend/src/core/ckb-rpc/ckb-rpc.interface.ts index ddb965fb..b141641e 100644 --- a/backend/src/core/ckb-rpc/ckb-rpc.interface.ts +++ b/backend/src/core/ckb-rpc/ckb-rpc.interface.ts @@ -46,8 +46,8 @@ export interface TransactionWithStatusResponse { time_added_to_pool: string | null; transaction: Transaction; tx_status: { - block_hash: string; - block_number: string; + block_hash: string | null; + block_number: string | null; reason: string | null; status: string; }; diff --git a/backend/src/modules/bitcoin/address/address.dataloader.ts b/backend/src/modules/bitcoin/address/address.dataloader.ts index faab155a..fb4d598d 100644 --- a/backend/src/modules/bitcoin/address/address.dataloader.ts +++ b/backend/src/modules/bitcoin/address/address.dataloader.ts @@ -7,21 +7,22 @@ import { BitcoinApiService } from 'src/core/bitcoin-api/bitcoin-api.service'; import { BitcoinBaseTransaction, BitcoinTransaction } from '../transaction/transaction.model'; @Injectable() -export class BitcoinAddressLoader implements NestDataLoader { +export class BitcoinAddressLoader implements NestDataLoader { private logger = new Logger(BitcoinAddressLoader.name); constructor(private bitcoinApiService: BitcoinApiService) {} public getBatchFunction() { - return (addresses: string[]) => { + return async (addresses: string[]) => { this.logger.debug(`Loading bitcoin addresses stats: ${addresses.join(', ')}`); - return Promise.all( + const results = await Promise.allSettled( addresses.map((address) => this.bitcoinApiService.getAddress({ address })), ); + return results.map((result) => (result.status === 'fulfilled' ? result.value : null)); }; } } -export type BitcoinAddressLoaderType = DataLoader; +export type BitcoinAddressLoaderType = DataLoader; export type BitcoinAddressLoaderResponse = DataLoaderResponse; export interface BitcoinAddressTransactionsLoaderParams { @@ -31,16 +32,17 @@ export interface BitcoinAddressTransactionsLoaderParams { @Injectable() export class BitcoinAddressTransactionsLoader - implements NestDataLoader + implements + NestDataLoader { private logger = new Logger(BitcoinAddressTransactionsLoader.name); constructor(private bitcoinApiService: BitcoinApiService) {} public getBatchFunction() { - return (batchProps: BitcoinAddressTransactionsLoaderParams[]) => { + return async (batchProps: BitcoinAddressTransactionsLoaderParams[]) => { this.logger.debug(`Loading bitcoin addresses txs: ${batchProps}`); - return Promise.all( + const results = await Promise.allSettled( batchProps.map(async (props) => { const txs = await this.bitcoinApiService.getAddressTxs({ address: props.address, @@ -49,12 +51,13 @@ export class BitcoinAddressTransactionsLoader return txs.map((tx) => BitcoinTransaction.from(tx)); }), ); + return results.map((result) => (result.status === 'fulfilled' ? result.value : null)); }; } } export type BitcoinAddressTransactionsLoaderType = DataLoader< BitcoinAddressTransactionsLoaderParams, - BitcoinBaseTransaction[] + BitcoinBaseTransaction[] | null >; 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 607da0b9..4982dfc1 100644 --- a/backend/src/modules/bitcoin/address/address.resolver.ts +++ b/backend/src/modules/bitcoin/address/address.resolver.ts @@ -1,5 +1,10 @@ +import { ConfigService } from '@nestjs/config'; +import { isValidAddress } from '@rgbpp-sdk/btc'; import { Loader } from '@applifting-io/nestjs-dataloader'; import { Args, Float, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'; +import { Env } from 'src/env'; +import { BtcNetworkTypeMap } from 'src/constants'; +import { RgbppAddress, RgbppBaseAddress } from 'src/modules/rgbpp/address/address.model'; import { BitcoinBaseTransaction, BitcoinTransaction } from '../transaction/transaction.model'; import { BitcoinAddress, BitcoinBaseAddress } from './address.model'; import { @@ -8,12 +13,18 @@ import { BitcoinAddressTransactionsLoader, BitcoinAddressTransactionsLoaderType, } from './address.dataloader'; -import { RgbppAddress, RgbppBaseAddress } from 'src/modules/rgbpp/address/address.model'; @Resolver(() => BitcoinAddress) export class BitcoinAddressResolver { + constructor(private configService: ConfigService) {} + @Query(() => BitcoinAddress, { name: 'btcAddress', nullable: true }) - public async getBtcAddress(@Args('address') address: string): Promise { + public async getBtcAddress(@Args('address') address: string): Promise { + // TODO: replace with decorator/interceptor? + const network = this.configService.get('NETWORK'); + if (!isValidAddress(address, BtcNetworkTypeMap[network])) { + throw new Error('Invalid bitcoin address'); + } return BitcoinAddress.from(address); } diff --git a/backend/src/modules/bitcoin/block/block.dataloader.ts b/backend/src/modules/bitcoin/block/block.dataloader.ts index f89bf71b..3740be75 100644 --- a/backend/src/modules/bitcoin/block/block.dataloader.ts +++ b/backend/src/modules/bitcoin/block/block.dataloader.ts @@ -6,15 +6,15 @@ import { BitcoinApiService } from 'src/core/bitcoin-api/bitcoin-api.service'; import * as BitcoinApi from 'src/core/bitcoin-api/bitcoin-api.schema'; @Injectable() -export class BitcoinBlockLoader implements NestDataLoader { +export class BitcoinBlockLoader implements NestDataLoader { private logger = new Logger(BitcoinBlockLoader.name); constructor(private bitcoinApiService: BitcoinApiService) {} public getBatchFunction() { - return (keys: string[]) => { + return async (keys: string[]) => { this.logger.debug(`Loading bitcoin blocks: ${keys.join(', ')}`); - return Promise.all( + const results = await Promise.allSettled( keys.map(async (key) => { let hash = key; if (!hash.startsWith('0')) { @@ -23,10 +23,11 @@ export class BitcoinBlockLoader implements NestDataLoader (result.status === 'fulfilled' ? result.value : null)); }; } } -export type BitcoinBlockLoaderType = DataLoader; +export type BitcoinBlockLoaderType = DataLoader; export type BitcoinBlockLoaderResponse = DataLoaderResponse; export interface BitcoinBlockTransactionsLoaderParams { @@ -36,16 +37,16 @@ export interface BitcoinBlockTransactionsLoaderParams { @Injectable() export class BitcoinBlockTransactionsLoader - implements NestDataLoader + implements NestDataLoader { private logger = new Logger(BitcoinBlockLoader.name); constructor(private bitcoinApiService: BitcoinApiService) {} public getBatchFunction() { - return (batchProps: BitcoinBlockTransactionsLoaderParams[]) => { + return async (batchProps: BitcoinBlockTransactionsLoaderParams[]) => { this.logger.debug(`Loading bitcoin block transactions: ${batchProps}`); - return Promise.all( + const results = await Promise.allSettled( batchProps.map((props) => this.bitcoinApiService.getBlockTxs({ hash: props.hash, @@ -53,12 +54,13 @@ export class BitcoinBlockTransactionsLoader }), ), ); + return results.map((result) => (result.status === 'fulfilled' ? result.value : null)); }; } } export type BitcoinBlockTransactionsLoaderType = DataLoader< BitcoinBlockTransactionsLoaderParams, - BitcoinApi.Transaction[] + BitcoinApi.Transaction[] | null >; export type BitcoinBlockTransactionsLoaderResponse = DataLoaderResponse; diff --git a/backend/src/modules/bitcoin/block/block.resolver.ts b/backend/src/modules/bitcoin/block/block.resolver.ts index 15ce31f4..919edf8e 100644 --- a/backend/src/modules/bitcoin/block/block.resolver.ts +++ b/backend/src/modules/bitcoin/block/block.resolver.ts @@ -15,12 +15,15 @@ import { export class BitcoinBlockResolver { private logger = new Logger(BitcoinBlockResolver.name); - @Query(() => BitcoinBlock, { name: 'btcBlock' }) + @Query(() => BitcoinBlock, { name: 'btcBlock', nullable: true }) public async getBlock( @Args('hashOrHeight', { type: () => String }) hashOrHeight: string, @Loader(BitcoinBlockLoader) blockLoader: BitcoinBlockLoaderType, - ): Promise { + ): Promise { const block = await blockLoader.load(hashOrHeight); + if (!block) { + return null; + } return BitcoinBlock.from(block); } diff --git a/backend/src/modules/bitcoin/transaction/transaction.dataloader.ts b/backend/src/modules/bitcoin/transaction/transaction.dataloader.ts index 7798d9f7..25adcb16 100644 --- a/backend/src/modules/bitcoin/transaction/transaction.dataloader.ts +++ b/backend/src/modules/bitcoin/transaction/transaction.dataloader.ts @@ -19,12 +19,7 @@ export class BitcoinTransactionLoader const results = await Promise.allSettled( ids.map((key) => this.bitcoinApiService.getTx({ txid: key })), ); - return results.map((result) => { - if (result.status === 'fulfilled') { - return result.value; - } - return null; - }); + return results.map((result) => (result.status === 'fulfilled' ? result.value : null)); }; } } @@ -33,7 +28,7 @@ export type BitcoinTransactionLoaderResponse = DataLoaderResponse + implements NestDataLoader { private logger = new Logger(BitcoinTransactionLoader.name); @@ -42,12 +37,16 @@ export class BitcoinTransactionOutSpendsLoader public getBatchFunction() { return async (txids: string[]) => { this.logger.debug(`Loading bitcoin transactions: ${txids.join(', ')}`); - return Promise.all( + const results = await Promise.allSettled( txids.map(async (txid) => this.bitcoinApiService.getTxOutSpends({ txid: txid })), ); + return results.map((result) => (result.status === 'fulfilled' ? result.value : null)); }; } } -export type BitcoinTransactionOutSpendsLoaderType = DataLoader; +export type BitcoinTransactionOutSpendsLoaderType = DataLoader< + string, + BitcoinApi.OutSpend[] | null +>; export type BitcoinTransactionOutSpendsLoaderResponse = DataLoaderResponse; diff --git a/backend/src/modules/bitcoin/transaction/transaction.resolver.ts b/backend/src/modules/bitcoin/transaction/transaction.resolver.ts index 9b421fd2..769735ce 100644 --- a/backend/src/modules/bitcoin/transaction/transaction.resolver.ts +++ b/backend/src/modules/bitcoin/transaction/transaction.resolver.ts @@ -22,6 +22,9 @@ export class BitcoinTransactionResolver { @Loader(BitcoinTransactionLoader) txLoader: BitcoinTransactionLoaderType, ): Promise { const transaction = await txLoader.load(txid); + if (!transaction) { + return null; + } return BitcoinTransaction.from(transaction); } diff --git a/backend/src/modules/ckb/address/address.dataloader.ts b/backend/src/modules/ckb/address/address.dataloader.ts index 40fa2a03..53e7445b 100644 --- a/backend/src/modules/ckb/address/address.dataloader.ts +++ b/backend/src/modules/ckb/address/address.dataloader.ts @@ -11,25 +11,26 @@ import { @Injectable() export class CkbAddressLoader - implements NestDataLoader + implements NestDataLoader { private logger = new Logger(CkbAddressLoader.name); constructor(private ckbExplorerService: CkbExplorerService) {} public getBatchFunction() { - return (batchParams: GetAddressParams[]) => { + return async (batchParams: GetAddressParams[]) => { this.logger.debug(`Loading CKB addresses info: ${batchParams}`); - return Promise.all( + const results = await Promise.allSettled( batchParams.map(async (params) => { const response = await this.ckbExplorerService.getAddress(params); return response.data.map((data) => data.attributes); }), ); + return results.map((result) => (result.status === 'fulfilled' ? result.value : null)); }; } } -export type CkbAddressLoaderType = DataLoader; +export type CkbAddressLoaderType = DataLoader; export type CkbAddressLoaderResponse = DataLoaderResponse; export interface CkbAddressTransactionLoaderResult { @@ -39,16 +40,16 @@ export interface CkbAddressTransactionLoaderResult { @Injectable() export class CkbAddressTransactionsLoader - implements NestDataLoader + implements NestDataLoader { private logger = new Logger(CkbAddressTransactionsLoader.name); constructor(private ckbExplorerService: CkbExplorerService) {} public getBatchFunction() { - return (batchParams: GetAddressParams[]) => { + return async (batchParams: GetAddressParams[]) => { this.logger.debug(`Loading CKB address transactions: ${batchParams}`); - return Promise.all( + const results = await Promise.allSettled( batchParams.map(async (params) => { const response = await this.ckbExplorerService.getAddressTransactions(params); return { @@ -57,11 +58,12 @@ export class CkbAddressTransactionsLoader }; }), ); + return results.map((result) => (result.status === 'fulfilled' ? result.value : null)); }; } } export type CkbAddressTransactionsLoaderType = DataLoader< GetAddressTransactionsParams, - CkbAddressTransactionLoaderResult + CkbAddressTransactionLoaderResult | null >; export type CkbAddressTransactionsLoaderResponse = DataLoaderResponse; diff --git a/backend/src/modules/ckb/address/address.resolver.ts b/backend/src/modules/ckb/address/address.resolver.ts index 8942e33b..0faa8c5e 100644 --- a/backend/src/modules/ckb/address/address.resolver.ts +++ b/backend/src/modules/ckb/address/address.resolver.ts @@ -33,8 +33,7 @@ export class CkbAddressResolver { address, }; } catch (e) { - this.logger.error(`getCkbAddress error: ${address} is not a valid CKB address`); - return null; + throw new Error('Invalid CKB address'); } } diff --git a/backend/src/modules/ckb/block/block.dataloader.ts b/backend/src/modules/ckb/block/block.dataloader.ts index c0833b9f..500551be 100644 --- a/backend/src/modules/ckb/block/block.dataloader.ts +++ b/backend/src/modules/ckb/block/block.dataloader.ts @@ -7,37 +7,39 @@ import * as CkbExplorer from 'src/core/ckb-explorer/ckb-explorer.interface'; import { CkbBlockService } from './block.service'; @Injectable() -export class CkbRpcBlockLoader implements NestDataLoader { +export class CkbRpcBlockLoader implements NestDataLoader { private logger = new Logger(CkbRpcBlockLoader.name); constructor(private blockService: CkbBlockService) {} public getBatchFunction() { - return (heightOrHashList: string[]) => { + return async (heightOrHashList: string[]) => { this.logger.debug(`Loading blocks from CkbRpc: ${heightOrHashList.join(', ')}`); - return Promise.all( + const results = await Promise.allSettled( heightOrHashList.map((heightOrHash) => this.blockService.getBlockFromRpc(heightOrHash)), ); + return results.map((result) => (result.status === 'fulfilled' ? result.value : null)); }; } } -export type CkbRpcBlockLoaderType = DataLoader; +export type CkbRpcBlockLoaderType = DataLoader; export type CkbRpcBlockLoaderResponse = DataLoaderResponse; @Injectable() -export class CkbExplorerBlockLoader implements NestDataLoader { +export class CkbExplorerBlockLoader implements NestDataLoader { private logger = new Logger(CkbRpcBlockLoader.name); constructor(private blockService: CkbBlockService) {} public getBatchFunction() { - return (heightOrHashList: string[]) => { + return async (heightOrHashList: string[]) => { this.logger.debug(`Loading blocks from CkbExplorer: ${heightOrHashList.join(', ')}`); - return Promise.all( + const results = await Promise.allSettled( heightOrHashList.map((heightOrHash) => this.blockService.getBlockFromExplorer(heightOrHash), ), ); + return results.map((result) => (result.status === 'fulfilled' ? result.value : null)); }; } } @@ -46,18 +48,21 @@ export type CkbExplorerBlockLoaderResponse = DataLoaderResponse + implements NestDataLoader { private logger = new Logger(CkbBlockEconomicStateLoader.name); constructor(private blockService: CkbBlockService) {} public getBatchFunction() { - return (hashes: string[]) => { + return async (hashes: string[]) => { this.logger.debug(`Loading economic state for blocks: ${hashes.join(', ')}`); - return Promise.all(hashes.map((key) => this.blockService.getBlockEconomicState(key))); + const results = await Promise.allSettled( + hashes.map((key) => this.blockService.getBlockEconomicState(key)), + ); + return results.map((result) => (result.status === 'fulfilled' ? result.value : null)); }; } } -export type CkbBlockEconomicStateLoaderType = DataLoader; +export type CkbBlockEconomicStateLoaderType = DataLoader; export type CkbBlockEconomicStateLoaderResponse = DataLoaderResponse; diff --git a/backend/src/modules/ckb/block/block.resolver.ts b/backend/src/modules/ckb/block/block.resolver.ts index cbe9916c..e4b392ec 100644 --- a/backend/src/modules/ckb/block/block.resolver.ts +++ b/backend/src/modules/ckb/block/block.resolver.ts @@ -20,12 +20,15 @@ import { @Resolver(() => CkbBlock) export class CkbBlockResolver { - @Query(() => CkbBlock, { name: 'ckbBlock' }) + @Query(() => CkbBlock, { name: 'ckbBlock', nullable: true }) public async getBlock( @Args('heightOrHash', { type: () => String }) heightOrHash: string, @Loader(CkbRpcBlockLoader) rpcBlockLoader: CkbRpcBlockLoaderType, - ): Promise { + ): Promise { const block = await rpcBlockLoader.load(heightOrHash); + if (!block) { + return null; + } return CkbBlock.from(block); } diff --git a/backend/src/modules/ckb/transaction/transaction.dataloader.ts b/backend/src/modules/ckb/transaction/transaction.dataloader.ts index 118b524a..700fb093 100644 --- a/backend/src/modules/ckb/transaction/transaction.dataloader.ts +++ b/backend/src/modules/ckb/transaction/transaction.dataloader.ts @@ -8,44 +8,48 @@ import { CkbTransactionService } from './transaction.service'; @Injectable() export class CkbRpcTransactionLoader - implements NestDataLoader + implements NestDataLoader { private logger = new Logger(CkbRpcTransactionLoader.name); constructor(private transactionService: CkbTransactionService) {} public getBatchFunction() { - return (hashes: string[]) => { + return async (hashes: string[]) => { this.logger.debug(`Loading CKB transactions from CkbRpcService: ${hashes.join(', ')}`); - return Promise.all(hashes.map((key) => this.transactionService.getTransactionFromRpc(key))); + const results = await Promise.allSettled( + hashes.map((key) => this.transactionService.getTransactionFromRpc(key)), + ); + return results.map((result) => (result.status === 'fulfilled' ? result.value : null)); }; } } export type CkbRpcTransactionLoaderType = DataLoader< string, - CkbRpcInterface.TransactionWithStatusResponse + CkbRpcInterface.TransactionWithStatusResponse | null >; export type CkbRpcTransactionLoaderResponse = DataLoaderResponse; @Injectable() export class CkbExplorerTransactionLoader - implements NestDataLoader + implements NestDataLoader { private logger = new Logger(CkbExplorerTransactionLoader.name); constructor(private transactionService: CkbTransactionService) {} public getBatchFunction() { - return (hashes: string[]) => { + return async (hashes: string[]) => { this.logger.debug(`Loading CKB transactions from CkbExplorerService: ${hashes.join(', ')}`); - return Promise.all( + const results = await Promise.allSettled( hashes.map((key) => this.transactionService.getTransactionFromExplorer(key)), ); + return results.map((result) => (result.status === 'fulfilled' ? result.value : null)); }; } } export type CkbExplorerTransactionLoaderType = DataLoader< string, - CkbExplorerInterface.DetailTransaction + CkbExplorerInterface.DetailTransaction | null >; export type CkbExplorerTransactionLoaderResponse = DataLoaderResponse; diff --git a/backend/src/modules/ckb/transaction/transaction.model.ts b/backend/src/modules/ckb/transaction/transaction.model.ts index b02ba2cb..2a814046 100644 --- a/backend/src/modules/ckb/transaction/transaction.model.ts +++ b/backend/src/modules/ckb/transaction/transaction.model.ts @@ -50,6 +50,10 @@ export class CkbTransaction { transactionWithStatus: CkbRpc.TransactionWithStatusResponse, ): CkbBaseTransaction { const { transaction, tx_status } = transactionWithStatus; + if (!transaction || tx_status?.status === 'unknown') { + return null; + } + const isCellbase = transaction.inputs[0].previous_output.tx_hash.endsWith('0'.repeat(64)); const resultTx = ResultFormatter.toTransaction(transaction as RPCTypes.Transaction); diff --git a/backend/src/modules/rgbpp/transaction/transaction.dataloader.ts b/backend/src/modules/rgbpp/transaction/transaction.dataloader.ts index efba69f8..bdfdb26c 100644 --- a/backend/src/modules/rgbpp/transaction/transaction.dataloader.ts +++ b/backend/src/modules/rgbpp/transaction/transaction.dataloader.ts @@ -6,11 +6,10 @@ import { RgbppTransactionService } from './transaction.service'; import { RgbppBaseTransaction } from './transaction.model'; @Injectable() -export class RgbppTransactionLoader - implements NestDataLoader { +export class RgbppTransactionLoader implements NestDataLoader { private logger = new Logger(RgbppTransactionLoader.name); - constructor(private transactionService: RgbppTransactionService) { } + constructor(private transactionService: RgbppTransactionService) {} public getBatchFunction() { return async (ids: string[]) => { diff --git a/backend/src/schema.gql b/backend/src/schema.gql index b7c7da40..a9d7b2bb 100644 --- a/backend/src/schema.gql +++ b/backend/src/schema.gql @@ -257,11 +257,11 @@ type BitcoinBlock { type Query { ckbChainInfo: CkbChainInfo! - ckbBlock(heightOrHash: String!): CkbBlock! + ckbBlock(heightOrHash: String!): CkbBlock ckbTransaction(txHash: String!): CkbTransaction ckbAddress(address: String!): CkbAddress btcChainInfo: BitcoinChainInfo! - btcBlock(hashOrHeight: String!): BitcoinBlock! + btcBlock(hashOrHeight: String!): BitcoinBlock btcTransaction(txid: String!): BitcoinTransaction rgbppLatestTransactions(page: Int, pageSize: Int): RgbppLatestTransactionList! rgbppTransaction(txidOrTxHash: String!): RgbppTransaction