Skip to content

Commit

Permalink
feat: fill backend todos
Browse files Browse the repository at this point in the history
  • Loading branch information
ShookLyngs committed Jul 24, 2024
1 parent e4724dd commit 8df1d39
Show file tree
Hide file tree
Showing 13 changed files with 233 additions and 33 deletions.
3 changes: 2 additions & 1 deletion backend/src/core/bitcoin-api/bitcoin-api.interface.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Block, RecommendedFees, Transaction, UTXO } from './bitcoin-api.schema';
import { Address, Block, RecommendedFees, Transaction, UTXO } from './bitcoin-api.schema';

export interface IBitcoinDataProvider {
getFeesRecommended(): Promise<RecommendedFees>;
getAddress({ address }: { address: string }): Promise<Address>;
getAddressTxsUtxo({ address }: { address: string }): Promise<UTXO[]>;
getAddressTxs({
address,
Expand Down
19 changes: 19 additions & 0 deletions backend/src/core/bitcoin-api/bitcoin-api.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,25 @@ export const Status = z.object({
});
export type Status = z.infer<typeof Status>;

export const Address = z.object({
address: z.string(),
chain_stats: z.object({
funded_txo_count: z.number(),
funded_txo_sum: z.number(),
spent_txo_count: z.number(),
spent_txo_sum: z.number(),
tx_count: z.number(),
}),
mempool_stats: z.object({
funded_txo_count: z.number(),
funded_txo_sum: z.number(),
spent_txo_count: z.number(),
spent_txo_sum: z.number(),
tx_count: z.number(),
}),
});
export type Address = z.infer<typeof Address>;

export const Balance = z.object({
address: z.string(),
satoshi: z.number(),
Expand Down
4 changes: 4 additions & 0 deletions backend/src/core/bitcoin-api/bitcoin-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ export class BitcoinApiService {
return this.call('getFeesRecommended');
}

public async getAddress({ address }: { address: string }) {
return this.call('getAddress', { address });
}

public async getAddressTxsUtxo({ address }: { address: string }) {
return this.call('getAddressTxsUtxo', { address });
}
Expand Down
7 changes: 6 additions & 1 deletion backend/src/core/bitcoin-api/provider/electrs.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import axios, { AxiosInstance } from 'axios';
import { Block, RecommendedFees, Transaction, UTXO } from '../bitcoin-api.schema';
import { Address, Block, RecommendedFees, Transaction, UTXO } from '../bitcoin-api.schema';
import { IBitcoinDataProvider } from '../bitcoin-api.interface';

export class ElectrsService implements IBitcoinDataProvider {
Expand All @@ -15,6 +15,11 @@ export class ElectrsService implements IBitcoinDataProvider {
throw new Error('Electrs: Recommended fees not available');
}

public async getAddress({ address }: { address: string }) {
const response = await this.request.get<Address>(`/address/${address}`);
return response.data;
}

public async getAddressTxsUtxo({ address }: { address: string }) {
const response = await this.request.get<UTXO[]>(`/address/${address}/utxo`);
return response.data;
Expand Down
5 changes: 5 additions & 0 deletions backend/src/core/bitcoin-api/provider/mempool.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ export class MempoolService implements IBitcoinDataProvider {
}
}

public async getAddress({ address }: { address: string }) {
const response = await this.mempool.bitcoin.addresses.getAddress({ address });
return response;
}

public async getAddressTxsUtxo({ address }: { address: string }) {
const response = await this.mempool.bitcoin.addresses.getAddressTxsUtxo({ address });
return response.map((utxo) => UTXO.parse(utxo));
Expand Down
91 changes: 91 additions & 0 deletions backend/src/modules/bitcoin/address/address.dataloader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Injectable, Logger } from '@nestjs/common';
import { NestDataLoader } from '@applifting-io/nestjs-dataloader';
import { Address } from 'src/core/bitcoin-api/bitcoin-api.schema';
import { BitcoinApiService } from 'src/core/bitcoin-api/bitcoin-api.service';
import { DataLoaderResponse } from 'src/common/type/dataloader';
import { BitcoinTransaction } from '../transaction/transaction.model';

@Injectable()
export class BitcoinAddressLoader implements NestDataLoader<string, Address> {
private logger = new Logger(BitcoinAddressLoader.name);

constructor(private bitcoinApiService: BitcoinApiService) {}

public getBatchFunction() {
return (addresses: string[]) => {
this.logger.debug(`Loading bitcoin addresses stats: ${addresses.join(', ')}`);
return Promise.all(
addresses.map((address) => this.bitcoinApiService.getAddress({ address })),
);
};
}
}
export type BitcoinAddressLoaderResponse = DataLoaderResponse<BitcoinAddressLoader>;

export interface BitcoinAddressBalance {
satoshi: number;
pendingSatoshi: number;
}

@Injectable()
export class BitcoinAddressBalanceLoader implements NestDataLoader<string, BitcoinAddressBalance> {
private logger = new Logger(BitcoinAddressBalanceLoader.name);

constructor(private bitcoinApiService: BitcoinApiService) {}

public getBatchFunction() {
return (addresses: string[]) => {
this.logger.debug(`Loading bitcoin addresses balance: ${addresses.join(', ')}`);
return Promise.all(
addresses.map(async (address) => {
// XXX: Miners has higher chance of getting this error: Too many unspent transaction outputs (>9000). Contact support to raise limits.
const utxos = await this.bitcoinApiService.getAddressTxsUtxo({ address });

let satoshi = 0;
let pendingSatoshi = 0;
utxos.forEach((utxo) => {
satoshi += utxo.value;
if (!utxo.status.confirmed) {
pendingSatoshi += utxo.value;
}
});
return {
satoshi,
pendingSatoshi,
};
}),
);
};
}
}
export type BitcoinAddressBalanceLoaderResponse = DataLoaderResponse<BitcoinAddressBalanceLoader>;

export interface BitcoinAddressTxsLoaderProps {
address: string;
afterTxid?: string;
}

@Injectable()
export class BitcoinAddressTxsLoader
implements NestDataLoader<BitcoinAddressTxsLoaderProps, BitcoinTransaction[]>
{
private logger = new Logger(BitcoinAddressTxsLoader.name);

constructor(private bitcoinApiService: BitcoinApiService) {}

public getBatchFunction() {
return (batchProps: BitcoinAddressTxsLoaderProps[]) => {
this.logger.debug(`Loading bitcoin addresses txs: ${batchProps}`);
return Promise.all(
batchProps.map(async (props) => {
const txs = await this.bitcoinApiService.getAddressTxs({
address: props.address,
after_txid: props.afterTxid,
});
return txs.map((tx) => BitcoinTransaction.from(tx));
}),
);
};
}
}
export type BitcoinAddressTxsLoaderResponse = DataLoaderResponse<BitcoinAddressTxsLoader>;
14 changes: 13 additions & 1 deletion backend/src/modules/bitcoin/address/address.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { Module } from '@nestjs/common';
import { BitcoinApiModule } from 'src/core/bitcoin-api/bitcoin-api.module';
import { BitcoinAddressResolver } from './address.resolver';
import {
BitcoinAddressLoader,
BitcoinAddressTxsLoader,
BitcoinAddressBalanceLoader,
} from './address.dataloader';

@Module({
providers: [BitcoinAddressResolver],
imports: [BitcoinApiModule],
providers: [
BitcoinAddressResolver,
BitcoinAddressLoader,
BitcoinAddressTxsLoader,
BitcoinAddressBalanceLoader,
],
})
export class AddressModule {}
61 changes: 46 additions & 15 deletions backend/src/modules/bitcoin/address/address.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,63 @@
import { Float, Parent, ResolveField, Resolver } from '@nestjs/graphql';
import DataLoader from 'dataloader';
import { Loader } from '@applifting-io/nestjs-dataloader';
import { Args, Float, Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { BitcoinApiService } from 'src/core/bitcoin-api/bitcoin-api.service';
import { BitcoinBaseTransaction, BitcoinTransaction } from '../transaction/transaction.model';
import { BitcoinAddress } from './address.model';
import {
BitcoinAddressBalanceLoader,
BitcoinAddressBalanceLoaderResponse,
BitcoinAddressLoader,
BitcoinAddressLoaderResponse,
BitcoinAddressTxsLoader,
BitcoinAddressTxsLoaderProps,
BitcoinAddressTxsLoaderResponse,
} from './address.dataloader';

@Resolver(() => BitcoinAddress)
export class BitcoinAddressResolver {
constructor(private bitcoinApiService: BitcoinApiService) {}

@ResolveField(() => Float)
public async satoshi(@Parent() address: BitcoinAddress): Promise<number> {
// TODO: Implement this resolver
// get satoshi/pendingSatoshi from the address UTXOs
return 0;
public async satoshi(
@Parent() address: BitcoinAddress,
@Loader(BitcoinAddressBalanceLoader)
addressBalanceLoader: DataLoader<string, BitcoinAddressBalanceLoaderResponse>,
): Promise<number> {
const { satoshi } = await addressBalanceLoader.load(address.address);
return satoshi;
}

@ResolveField(() => Float)
public async pendingSatoshi(@Parent() address: BitcoinAddress): Promise<number> {
// TODO: Implement this Resolver
return 0;
public async pendingSatoshi(
@Parent() address: BitcoinAddress,
@Loader(BitcoinAddressBalanceLoader)
addressBalanceLoader: DataLoader<string, BitcoinAddressBalanceLoaderResponse>,
): Promise<number> {
const { pendingSatoshi } = await addressBalanceLoader.load(address.address);
return pendingSatoshi;
}

@ResolveField(() => Float)
public async transactionCount(@Parent() address: BitcoinAddress): Promise<number> {
// TODO: Implement this resolver
return 0;
public async transactionCount(
@Parent() address: BitcoinAddress,
@Loader(BitcoinAddressLoader) addressLoader: DataLoader<string, BitcoinAddressLoaderResponse>,
): Promise<number> {
// XXX: addressInfo.mempool_stats.tx_count is not included in the response, not sure if it should be included
const stats = await addressLoader.load(address.address);
return stats.chain_stats.tx_count;
}

@ResolveField(() => [BitcoinTransaction])
public async transactions(@Parent() address: BitcoinAddress): Promise<BitcoinBaseTransaction[]> {
// TODO: Implement this resolver
// use dataloaders to fetch transactions
return [];
public async transactions(
@Parent() address: BitcoinAddress,
@Loader(BitcoinAddressTxsLoader)
addressTxsLoader: DataLoader<BitcoinAddressTxsLoaderProps, BitcoinAddressTxsLoaderResponse>,
@Args('afterTxid', { nullable: true }) afterTxid?: string,
): Promise<BitcoinBaseTransaction[]> {
return await addressTxsLoader.load({
address: address.address,
afterTxid: afterTxid,
});
}
}
3 changes: 2 additions & 1 deletion backend/src/modules/bitcoin/block/block.dataloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export class BitcoinBlockTransactionsLoader

public getBatchFunction() {
return (hashes: string[]) => {
this.logger.debug(`Loading bitcoin blocks: ${hashes.join(', ')}`);
// TODO: this method only returns the first 25 transactions of each block
this.logger.debug(`Loading bitcoin block transactions: ${hashes.join(', ')}`);
return Promise.all(hashes.map((hash) => this.bitcoinApiService.getBlockTxs({ hash })));
};
}
Expand Down
47 changes: 37 additions & 10 deletions backend/src/modules/bitcoin/block/block.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,52 @@ export class BitcoinBlockResolver {
}

@ResolveField(() => BitcoinAddress)
public async miner(@Parent() block: BitcoinBaseBlock): Promise<BitcoinBaseAddress> {
// TODO: Implement this resolver
public async miner(
@Parent() block: BitcoinBaseBlock,
@Loader(BitcoinBlockTransactionsLoader)
blockTxsLoader: DataLoader<string, BitcoinBlockTransactionsLoaderResponse>,
): Promise<BitcoinBaseAddress> {
const txs = await blockTxsLoader.load(block.id);
const coinbaseTx = BitcoinTransaction.from(txs[0]);
return {
address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa',
address: coinbaseTx.vout[0].scriptpubkeyAddress,
};
}

@ResolveField(() => Float)
public async totalFee(@Parent() block: BitcoinBaseBlock): Promise<number> {
// TODO: Implement this resolver
return 0;
public async totalFee(
@Parent() block: BitcoinBaseBlock,
@Loader(BitcoinBlockTransactionsLoader)
blockTxsLoader: DataLoader<string, BitcoinBlockTransactionsLoaderResponse>,
): Promise<number> {
const txs = await blockTxsLoader.load(block.id);
return txs.reduce((sum, tx) => sum + tx.fee, 0);
}

@ResolveField(() => FeeRateRange)
public async feeRateRange(@Parent() block: BitcoinBaseBlock): Promise<FeeRateRange> {
// TODO: Implement this resolver
public async feeRateRange(
@Parent() block: BitcoinBaseBlock,
@Loader(BitcoinBlockTransactionsLoader)
blockTxsLoader: DataLoader<string, BitcoinBlockTransactionsLoaderResponse>,
): Promise<FeeRateRange> {
// TODO: The BitcoinApiService.getBlockTxs() only returns the first 25 transactions
const txs = await blockTxsLoader.load(block.id);

let min = Infinity;
let max = 0;
for (const tx of txs.slice(1)) {
const vSize = Math.ceil(tx.weight / 4);
const feeRate = tx.fee / vSize;
if (feeRate < min) {
min = feeRate;
}
if (feeRate > max) {
max = feeRate;
}
}
return {
min: 0,
max: 1,
min,
max,
};
}

Expand Down
9 changes: 6 additions & 3 deletions backend/src/modules/bitcoin/output/output.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import { BitcoinOutput } from './output.model';
@Resolver(() => BitcoinOutput)
export class BitcoinOutputResolver {
@ResolveField(() => BitcoinAddress)
public async address(@Parent() output: BitcoinOutput): Promise<BitcoinBaseAddress> {
// TODO: Implement this resolver
public async address(@Parent() output: BitcoinOutput): Promise<BitcoinBaseAddress | null> {
// XXX: OP_RETURN outputs don't have address
if (!output.scriptpubkeyAddress) {
return null;
}
return {
address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa',
address: output.scriptpubkeyAddress,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class RgbppTransactionResolver {
public async getTransaction(
@Args('txidOrTxHash') txidOrTxHash: string,
): Promise<RgbppBaseTransaction | null> {
// TODO: not sure if the txidOrTxHash is a ckb txHash or a btc txid
return txidOrTxHash.startsWith('ck')
? this.transactionService.getTransactionByCkbTxHash(txidOrTxHash)
: this.transactionService.getTransactionByBtcTxid(txidOrTxHash);
Expand Down
2 changes: 1 addition & 1 deletion backend/src/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ type BitcoinAddress {
satoshi: Float!
pendingSatoshi: Float!
transactionCount: Float!
transactions: [BitcoinTransaction!]!
transactions(afterTxid: String): [BitcoinTransaction!]!
}

"""Bitcoin Output"""
Expand Down

0 comments on commit 8df1d39

Please sign in to comment.