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

feat(bitcoin): post btc transaction to multi electrs/btc nodes #127

Merged
merged 4 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
10 changes: 10 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ const envSchema = z
* https://github.com/ckb-cell/ckb-bitcoin-spv-service
*/
BITCOIN_SPV_SERVICE_URL: z.string(),

/**
* Bitcoin additional broadcast electrs URL list
* broadcast transaction to multiple electrs API when receive bitcoin transaction from users
*/
BITCOIN_ADDITIONAL_BROADCAST_ELECTRS_URL_LIST: z
.string()
.transform((value) => value.split(','))
.optional(),

/**
* The URL of the CKB JSON-RPC server.
*/
Expand Down
7 changes: 3 additions & 4 deletions src/services/bitcoin/electrs.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import axios, { AxiosInstance } from 'axios';
import { Cradle } from '../../container';
import { IBitcoinDataProvider } from './interface';
import { Block, RecommendedFees, Transaction, UTXO } from './schema';

export class ElectrsClient implements IBitcoinDataProvider {
private request: AxiosInstance;

constructor(cradle: Cradle) {
if (!cradle.env.BITCOIN_ELECTRS_API_URL) {
constructor(baseURL: string) {
if (!baseURL) {
throw new Error('BITCOIN_ELECTRS_API_URL is required');
ShookLyngs marked this conversation as resolved.
Show resolved Hide resolved
}
this.request = axios.create({
baseURL: cradle.env.BITCOIN_ELECTRS_API_URL,
baseURL,
});
}

Expand Down
25 changes: 19 additions & 6 deletions src/services/bitcoin/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AxiosError, HttpStatusCode } from 'axios';
import * as Sentry from '@sentry/node';
import { Cradle } from '../../container';
import { IBitcoinDataProvider } from './interface';
import { IBitcoinBroadcastBackuper, IBitcoinDataProvider } from './interface';
import { MempoolClient } from './mempool';
import { ElectrsClient } from './electrs';
import { NetworkType } from '../../constants';
Expand Down Expand Up @@ -54,6 +54,7 @@ export default class BitcoinClient implements IBitcoinClient {
private cradle: Cradle;
private source: IBitcoinDataProvider;
private fallback?: IBitcoinDataProvider;
private backupers: IBitcoinBroadcastBackuper[] = [];

constructor(cradle: Cradle) {
this.cradle = cradle;
Expand All @@ -62,23 +63,33 @@ export default class BitcoinClient implements IBitcoinClient {
switch (env.BITCOIN_DATA_PROVIDER) {
case 'mempool':
this.cradle.logger.info('Using Mempool.space API as the bitcoin data provider');
this.source = new MempoolClient(cradle);
this.source = new MempoolClient(env.BITCOIN_MEMPOOL_SPACE_API_URL, cradle);
if (env.BITCOIN_ELECTRS_API_URL) {
this.cradle.logger.info('Using Electrs API as the fallback bitcoin data provider');
this.fallback = new ElectrsClient(cradle);
this.fallback = new ElectrsClient(env.BITCOIN_ELECTRS_API_URL);
}
break;
case 'electrs':
this.cradle.logger.info('Using Electrs API as the bitcoin data provider');
this.source = new ElectrsClient(cradle);
this.source = new ElectrsClient(env.BITCOIN_ELECTRS_API_URL);
if (env.BITCOIN_MEMPOOL_SPACE_API_URL) {
this.cradle.logger.info('Using Mempool.space API as the fallback bitcoin data provider');
this.fallback = new MempoolClient(cradle);
this.fallback = new MempoolClient(env.BITCOIN_MEMPOOL_SPACE_API_URL, cradle);
}
break;
default:
throw new Error('Invalid bitcoin data provider');
}

if (this.fallback) {
this.backupers.push(this.fallback);
}
if (
env.BITCOIN_ADDITIONAL_BROADCAST_ELECTRS_URL_LIST &&
env.BITCOIN_ADDITIONAL_BROADCAST_ELECTRS_URL_LIST.length > 0
) {
this.backupers = env.BITCOIN_ADDITIONAL_BROADCAST_ELECTRS_URL_LIST.map((url) => new ElectrsClient(url));
}
Flouse marked this conversation as resolved.
Show resolved Hide resolved
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -152,7 +163,9 @@ export default class BitcoinClient implements IBitcoinClient {
}

public async postTx({ txhex }: { txhex: string }) {
return this.call('postTx', [{ txhex }]);
const txid = await this.call('postTx', [{ txhex }]);
Promise.all(this.backupers.map((backuper) => backuper.postTx({ txhex })));
return txid;
}

public async getAddressTxsUtxo({ address }: { address: string }) {
Expand Down
2 changes: 2 additions & 0 deletions src/services/bitcoin/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ export interface IBitcoinDataProvider {
getBlockTxids({ hash }: { hash: string }): Promise<string[]>;
getBlocksTipHash(): Promise<string>;
}

export type IBitcoinBroadcastBackuper = Pick<IBitcoinDataProvider, 'postTx'>;
6 changes: 3 additions & 3 deletions src/services/bitcoin/mempool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ export class MempoolClient implements IBitcoinDataProvider {
private mempool: ReturnType<typeof mempoolJS>;
private defaultFee = 1;

constructor(cradle: Cradle) {
if (!cradle.env.BITCOIN_MEMPOOL_SPACE_API_URL) {
constructor(baseURL: string, cradle: Cradle) {
if (!baseURL) {
throw new Error('BITCOIN_MEMPOOL_SPACE_API_URL is required');
}
const url = new URL(cradle.env.BITCOIN_MEMPOOL_SPACE_API_URL);
const url = new URL(baseURL);
this.mempool = mempoolJS({
hostname: url.hostname,
network: cradle.env.NETWORK,
Expand Down
Loading