Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(backend): ensure data loaders and queries are nullable #19

Merged
merged 3 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions backend/src/constants.ts
Original file line number Diff line number Diff line change
@@ -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, RgbppBtc.NetworkType> = {
[NetworkType.mainnet]: RgbppBtc.NetworkType.MAINNET,
[NetworkType.testnet]: RgbppBtc.NetworkType.TESTNET,
[NetworkType.signet]: RgbppBtc.NetworkType.TESTNET,
};
4 changes: 2 additions & 2 deletions backend/src/core/ckb-rpc/ckb-rpc.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
19 changes: 11 additions & 8 deletions backend/src/modules/bitcoin/address/address.dataloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Address> {
export class BitcoinAddressLoader implements NestDataLoader<string, Address | null> {
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<string, Address>;
export type BitcoinAddressLoaderType = DataLoader<string, Address | null>;
export type BitcoinAddressLoaderResponse = DataLoaderResponse<BitcoinAddressLoader>;

export interface BitcoinAddressTransactionsLoaderParams {
Expand All @@ -31,16 +32,17 @@ export interface BitcoinAddressTransactionsLoaderParams {

@Injectable()
export class BitcoinAddressTransactionsLoader
implements NestDataLoader<BitcoinAddressTransactionsLoaderParams, BitcoinBaseTransaction[]>
implements
NestDataLoader<BitcoinAddressTransactionsLoaderParams, BitcoinBaseTransaction[] | null>
{
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,
Expand All @@ -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<BitcoinAddressTransactionsLoader>;
15 changes: 13 additions & 2 deletions backend/src/modules/bitcoin/address/address.resolver.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<Env>) {}

@Query(() => BitcoinAddress, { name: 'btcAddress', nullable: true })
public async getBtcAddress(@Args('address') address: string): Promise<BitcoinBaseAddress> {
public async getBtcAddress(@Args('address') address: string): Promise<BitcoinBaseAddress | null> {
// 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);
}

Expand Down
18 changes: 10 additions & 8 deletions backend/src/modules/bitcoin/block/block.dataloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, BitcoinApi.Block> {
export class BitcoinBlockLoader implements NestDataLoader<string, BitcoinApi.Block | null> {
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')) {
Expand All @@ -23,10 +23,11 @@ export class BitcoinBlockLoader implements NestDataLoader<string, BitcoinApi.Blo
return this.bitcoinApiService.getBlock({ hash });
}),
);
return results.map((result) => (result.status === 'fulfilled' ? result.value : null));
};
}
}
export type BitcoinBlockLoaderType = DataLoader<string, BitcoinApi.Block>;
export type BitcoinBlockLoaderType = DataLoader<string, BitcoinApi.Block | null>;
export type BitcoinBlockLoaderResponse = DataLoaderResponse<BitcoinBlockLoader>;

export interface BitcoinBlockTransactionsLoaderParams {
Expand All @@ -36,29 +37,30 @@ export interface BitcoinBlockTransactionsLoaderParams {

@Injectable()
export class BitcoinBlockTransactionsLoader
implements NestDataLoader<BitcoinBlockTransactionsLoaderParams, BitcoinApi.Transaction[]>
implements NestDataLoader<BitcoinBlockTransactionsLoaderParams, BitcoinApi.Transaction[] | null>
{
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,
startIndex: props.startIndex,
}),
),
);
return results.map((result) => (result.status === 'fulfilled' ? result.value : null));
};
}
}
export type BitcoinBlockTransactionsLoaderType = DataLoader<
BitcoinBlockTransactionsLoaderParams,
BitcoinApi.Transaction[]
BitcoinApi.Transaction[] | null
>;
export type BitcoinBlockTransactionsLoaderResponse =
DataLoaderResponse<BitcoinBlockTransactionsLoader>;
7 changes: 5 additions & 2 deletions backend/src/modules/bitcoin/block/block.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<BitcoinBaseBlock> {
): Promise<BitcoinBaseBlock | null> {
const block = await blockLoader.load(hashOrHeight);
if (!block) {
return null;
}
return BitcoinBlock.from(block);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
};
}
}
Expand All @@ -33,7 +28,7 @@ export type BitcoinTransactionLoaderResponse = DataLoaderResponse<BitcoinTransac

@Injectable()
export class BitcoinTransactionOutSpendsLoader
implements NestDataLoader<string, BitcoinApi.OutSpend[]>
implements NestDataLoader<string, BitcoinApi.OutSpend[] | null>
{
private logger = new Logger(BitcoinTransactionLoader.name);

Expand All @@ -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<string, BitcoinApi.OutSpend[]>;
export type BitcoinTransactionOutSpendsLoaderType = DataLoader<
string,
BitcoinApi.OutSpend[] | null
>;
export type BitcoinTransactionOutSpendsLoaderResponse =
DataLoaderResponse<BitcoinTransactionOutSpendsLoader>;
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export class BitcoinTransactionResolver {
@Loader(BitcoinTransactionLoader) txLoader: BitcoinTransactionLoaderType,
): Promise<BitcoinBaseTransaction | null> {
const transaction = await txLoader.load(txid);
if (!transaction) {
return null;
}
return BitcoinTransaction.from(transaction);
}

Expand Down
18 changes: 10 additions & 8 deletions backend/src/modules/ckb/address/address.dataloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,26 @@ import {

@Injectable()
export class CkbAddressLoader
implements NestDataLoader<GetAddressParams, CkbExplorer.AddressInfo[]>
implements NestDataLoader<GetAddressParams, CkbExplorer.AddressInfo[] | null>
{
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<GetAddressParams, CkbExplorer.AddressInfo[]>;
export type CkbAddressLoaderType = DataLoader<GetAddressParams, CkbExplorer.AddressInfo[] | null>;
export type CkbAddressLoaderResponse = DataLoaderResponse<CkbAddressLoader>;

export interface CkbAddressTransactionLoaderResult {
Expand All @@ -39,16 +40,16 @@ export interface CkbAddressTransactionLoaderResult {

@Injectable()
export class CkbAddressTransactionsLoader
implements NestDataLoader<GetAddressTransactionsParams, CkbAddressTransactionLoaderResult>
implements NestDataLoader<GetAddressTransactionsParams, CkbAddressTransactionLoaderResult | null>
{
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 {
Expand All @@ -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<CkbAddressTransactionsLoader>;
3 changes: 1 addition & 2 deletions backend/src/modules/ckb/address/address.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}

Expand Down
Loading
Loading