Skip to content

Commit

Permalink
feat: Implemented Retry logic for esplora and bitcoin_core providers
Browse files Browse the repository at this point in the history
Implemented custom retry logic for axios request using an exponential delay function

implemented retry logic for bitcoin core provider

cleaned up retry logic

cleaned up retry logic

cleaned up retry logic
  • Loading branch information
aruokhai committed Sep 10, 2024
1 parent c2cd174 commit da1aba3
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 119 deletions.
4 changes: 4 additions & 0 deletions config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ db:
app:
port: <port>
network: <network>
requestRetry:
delay: <number> # delay in Milliseconds
count: <number>
providerType: <providerType>
esplora:
url: <host>
Expand All @@ -14,3 +17,4 @@ bitcoinCore:
rpcPass: <password>
rpcUser: <username>
rpcPort: <port>

4 changes: 4 additions & 0 deletions config/dev.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ db:
app:
port: 3000
network: regtest
requestRetry:
delay: 3000
count: 3
providerType: ESPLORA
esplora:
url: https://blockstream.info
Expand All @@ -14,3 +17,4 @@ bitcoinCore:
rpcPass: polarpass
rpcUser: polaruser
rpcPort: 18445

3 changes: 3 additions & 0 deletions config/e2e.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ db:
app:
port: 3000
network: regtest
requestRetry:
delay: 500
count: 1
providerType: BITCOIN_CORE_RPC
bitcoinCore:
protocol: http
Expand Down
116 changes: 61 additions & 55 deletions src/block-data-providers/bitcoin-core/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ import {
Input,
NetworkInfo,
} from '@/block-data-providers/bitcoin-core/interfaces';
import axios from 'axios';
import { AxiosRequestConfig } from 'axios';
import * as currency from 'currency.js';
import { AxiosRetryConfig, makeRequest } from '@/common/request';

@Injectable()
export class BitcoinCoreProvider
Expand All @@ -37,16 +38,22 @@ export class BitcoinCoreProvider
protected readonly operationStateKey = 'bitcoincore-operation-state';
private readonly rpcUrl: string;
private isSyncing = false;
private retryConfig: AxiosRetryConfig;

public constructor(
private readonly configService: ConfigService,
indexerService: IndexerService,
operationStateService: OperationStateService,
) {
super(indexerService, operationStateService);

const { protocol, rpcPort, rpcHost } =
configService.get<BitcoinCoreConfig>('bitcoinCore');

this.rpcUrl = `${protocol}://${rpcHost}:${rpcPort}/`;

this.retryConfig =
this.configService.get<AxiosRetryConfig>('app.requestRetry');
}

async onApplicationBootstrap() {
Expand Down Expand Up @@ -82,41 +89,44 @@ export class BitcoinCoreProvider
throw new Error('State not found');
}

const tipHeight = await this.getTipHeight();
if (tipHeight <= state.indexedBlockHeight) {
this.logger.debug(
`No new blocks found. Current tip height: ${tipHeight}`,
);
this.isSyncing = false;
return;
}

const networkInfo = await this.getNetworkInfo();
const verbosityLevel = this.versionToVerbosity(networkInfo.version);
try {
const tipHeight = await this.getTipHeight();
if (tipHeight <= state.indexedBlockHeight) {
this.logger.debug(
`No new blocks found. Current tip height: ${tipHeight}`,
);
this.isSyncing = false;
return;
}

let height = state.indexedBlockHeight + 1;
for (height; height <= tipHeight; height++) {
const transactions = await this.processBlock(
height,
verbosityLevel,
);
const networkInfo = await this.getNetworkInfo();
const verbosityLevel = this.versionToVerbosity(networkInfo.version);

for (const transaction of transactions) {
const { txid, vin, vout, blockHeight, blockHash } = transaction;
await this.indexTransaction(
txid,
vin,
vout,
blockHeight,
blockHash,
let height = state.indexedBlockHeight + 1;
for (height; height <= tipHeight; height++) {
const transactions = await this.processBlock(
height,
verbosityLevel,
);
}

state.indexedBlockHeight = height;
await this.setState(state);
for (const transaction of transactions) {
const { txid, vin, vout, blockHeight, blockHash } =
transaction;
await this.indexTransaction(
txid,
vin,
vout,
blockHeight,
blockHash,
);
}

state.indexedBlockHeight = height;
await this.setState(state);
}
} finally {
this.isSyncing = false;
}

this.isSyncing = false;
}

private async getNetworkInfo(): Promise<NetworkInfo> {
Expand Down Expand Up @@ -241,31 +251,27 @@ export class BitcoinCoreProvider
const { rpcUser, rpcPass } =
this.configService.get<BitcoinCoreConfig>('bitcoinCore');

try {
const response = await axios.post(
this.rpcUrl,
{
...body,
jsonrpc: '1.0',
id: 'silent_payment_indexer',
},
{
auth: {
username: rpcUser,
password: rpcPass,
},
},
);
const request_config: AxiosRequestConfig = {
url: this.rpcUrl,
method: 'POST',
auth: {
username: rpcUser,
password: rpcPass,
},
data: {
...body,
jsonrpc: '1.0',
id: 'silent_payment_indexer',
},
};

return response.data.result;
} catch (error) {
this.logger.error(
`Request to BitcoinCore failed!\nRequest:\n${JSON.stringify(
body,
)}\nError:\n${error.message}`,
);
throw error;
}
const response = await makeRequest(
request_config,
this.retryConfig,
this.logger,
);

return response.result;
}

private convertToSatoshi(amount: number): number {
Expand Down
97 changes: 33 additions & 64 deletions src/block-data-providers/esplora.provider.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
import { OperationStateService } from '@/operation-state/operation-state.service';
import { BaseBlockDataProvider } from '@/block-data-providers/base-block-data-provider.abstract';
import axios, { AxiosError, AxiosRequestConfig } from 'axios';
import { TAPROOT_ACTIVATION_HEIGHT } from '@/common/constants';
import { ConfigService } from '@nestjs/config';
import { BitcoinNetwork } from '@/common/enum';
import { URL } from 'url';
import { Cron, CronExpression } from '@nestjs/schedule';
import { IndexerService, TransactionInput } from '@/indexer/indexer.service';
import { AxiosRetryConfig, makeRequest } from '@/common/request';
import { AxiosRequestConfig } from 'axios';

type EsploraOperationState = {
currentBlockHeight: number;
Expand Down Expand Up @@ -64,6 +65,7 @@ export class EsploraProvider
protected readonly logger = new Logger(EsploraProvider.name);
protected readonly operationStateKey = 'esplora-operation-state';
private readonly baseUrl: string;
private retryConfig: AxiosRetryConfig;
private isSyncing = false;
private readonly batchSize: number;

Expand Down Expand Up @@ -91,6 +93,9 @@ export class EsploraProvider
this.baseUrl = new URL(
`${this.configService.get<string>('esplora.url')}${pathPrefix}`,
).toString();

this.retryConfig =
this.configService.get<AxiosRetryConfig>('app.requestRetry');
}

async onApplicationBootstrap() {
Expand Down Expand Up @@ -124,41 +129,34 @@ export class EsploraProvider
if (!state) {
throw new Error('State not found');
}

const tipHeight = await this.getTipHeight();
if (tipHeight <= state.indexedBlockHeight) {
this.logger.log(
`No new blocks found. Current tip height: ${tipHeight}`,
);
this.isSyncing = false;
return;
}

for (
let height = state.indexedBlockHeight + 1;
height <= tipHeight;
height++
) {
const blockHash = await this.getBlockHash(height);
this.logger.log(
`Processing block at height ${height}, hash ${blockHash}`,
);

try {
await this.processBlock(height, blockHash);
} catch (error) {
this.logger.error(
`Error processing block at height ${height}, hash ${blockHash}: ${error.message}`,
try {
const tipHeight = await this.getTipHeight();
if (tipHeight <= state.indexedBlockHeight) {
this.logger.log(
`No new blocks found. Current tip height: ${tipHeight}`,
);
this.isSyncing = false;
break;
return;
}

state.indexedBlockHeight = height;
await this.setState(state);
}
for (
let height = state.indexedBlockHeight + 1;
height <= tipHeight;
height++
) {
const blockHash = await this.getBlockHash(height);
this.logger.log(
`Processing block at height ${height}, hash ${blockHash}`,
);

await this.processBlock(height, blockHash);

this.isSyncing = false;
state.indexedBlockHeight = height;
await this.setState(state);
}
} finally {
this.isSyncing = false;
}
}

private async processBlock(height: number, hash: string) {
Expand Down Expand Up @@ -191,39 +189,6 @@ export class EsploraProvider
}
}

async request(config: AxiosRequestConfig): Promise<any> {
try {
const response = await axios.request(config);
this.logger.debug(
`Request to Esplora succeeded:\nRequest:\n${JSON.stringify(
config,
null,
2,
)}\nResponse:\n${JSON.stringify(response.data, null, 2)}`,
);
return response.data;
} catch (error) {
this.logger.error(error);
if (error instanceof AxiosError) {
if (error.response) {
this.logger.error(
`Request to Esplora failed!\nStatus code ${
error.response.status
}\nRequest:\n${JSON.stringify(
config,
)}\nResponse:\n${JSON.stringify(error.response.data)}`,
);
}
} else {
this.logger.error(
`Request to Esplora failed!\nRequest:\n${JSON.stringify(
config,
)}\nError:\n${error.message}`,
);
}
}
}

private async getTipHeight(): Promise<number> {
return this.request({
method: 'GET',
Expand Down Expand Up @@ -258,4 +223,8 @@ export class EsploraProvider
url: `${this.baseUrl}/tx/${txid}`,
});
}

private async request(request_config: AxiosRequestConfig): Promise<any> {
return makeRequest(request_config, this.retryConfig, this.logger);
}
}
Loading

0 comments on commit da1aba3

Please sign in to comment.