Skip to content

Commit

Permalink
Merge pull request #23 from router-protocol/refactor/bitcoin-balance
Browse files Browse the repository at this point in the history
update: bitcoin balance and transaction status fetcher
  • Loading branch information
jayeshbhole-rp authored Nov 13, 2024
2 parents 5d9169c + ce4d79c commit b45deaa
Show file tree
Hide file tree
Showing 10 changed files with 514 additions and 172 deletions.
7 changes: 7 additions & 0 deletions example-next/src/components/Tokens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ const tokens: TokenMetadata[] = [
symbol: 'USDT',
chainId: '-239',
},
{
address: ETH_ADDRESS,
decimals: 8,
name: 'Bitcoin',
symbol: 'BTC',
chainId: 'bitcoin',
},
{
address: ETH_ADDRESS,
decimals: 24,
Expand Down
111 changes: 111 additions & 0 deletions packages/react/src/actions/bitcoin/balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {
BalanceApiResponse,
BlockchainInfoBalanceResponse,
BlockcypherBalanceResponse,
BtcScanBalanceResponse,
} from '../../types/bitcoin.js';
import { tryAPI } from '../../utils/index.js';
import { APIs, CACHE_EXPIRATION_TIME } from './bitcoinApiConfig.js';

export const getBalance = async (address: string): Promise<BalanceApiResponse> => {
const lastUsedApiData = localStorage.getItem('lastUsedApi-bitcoin');
if (lastUsedApiData) {
const { apiName, timestamp } = JSON.parse(lastUsedApiData);

if (timestamp + CACHE_EXPIRATION_TIME > Date.now()) {
try {
const api = APIs[apiName];
if (api) {
const apiUrl = api.url.balance(address);
let response: BalanceApiResponse;
switch (apiName) {
case 'btcscan': {
const apiResponse = await tryAPI<BtcScanBalanceResponse>(apiName, apiUrl);
response = { source: 'btcscan', data: apiResponse.data };
break;
}
case 'blockchain.info': {
const apiResponse = await tryAPI<BlockchainInfoBalanceResponse>(apiName, apiUrl);
response = { source: 'blockchain.info', data: apiResponse.data };
break;
}
case 'blockcypher': {
const apiResponse = await tryAPI<BlockcypherBalanceResponse>(apiName, apiUrl);
response = { source: 'blockcypher', data: apiResponse.data };
break;
}
default: {
throw new Error(`Unknown API: ${apiName}`);
}
}
return response;
}
} catch (error) {
console.warn(`Last used API ${apiName} failed, trying all APIs...`);
}
}
}

let lastError: Error | null = null;
for (const api of Object.values(APIs)) {
const apiUrl = api.url.balance(address);
try {
let response: BalanceApiResponse;
switch (api.name) {
case 'btcscan': {
const apiResponse = await tryAPI<BtcScanBalanceResponse>(api.name, apiUrl);
response = { source: 'btcscan', data: apiResponse.data };
break;
}
case 'blockchain.info': {
const apiResponse = await tryAPI<BlockchainInfoBalanceResponse>(api.name, apiUrl);
response = { source: 'blockchain.info', data: apiResponse.data };
break;
}
case 'blockcypher': {
const apiResponse = await tryAPI<BlockcypherBalanceResponse>(api.name, apiUrl);
response = { source: 'blockcypher', data: apiResponse.data };
break;
}
}

localStorage.setItem(
'lastUsedApi-bitcoin',
JSON.stringify({
apiName: api.name,
timestamp: Date.now(),
}),
);

return response;
} catch (error) {
lastError = error as Error;
continue;
}
}

throw new Error(`All APIs failed to fetch balance. Last error: ${lastError?.message}`);
};

export const getFormattedBalance = async (address: string): Promise<number> => {
const response = await getBalance(address);

switch (response.source) {
case 'btcscan': {
const data = response.data as BtcScanBalanceResponse;
const confirmedBalance = data.chain_stats.funded_txo_sum - data.chain_stats.spent_txo_sum;
const unconfirmedBalance = data.mempool_stats.funded_txo_sum - data.mempool_stats.spent_txo_sum;
return confirmedBalance + unconfirmedBalance;
}

case 'blockchain.info': {
const data = response.data as BlockchainInfoBalanceResponse;
return data.confirmed + data.unconfirmed;
}

case 'blockcypher': {
const data = response.data as BlockcypherBalanceResponse;
return data.balance + data.unconfirmed_balance;
}
}
};
58 changes: 34 additions & 24 deletions packages/react/src/actions/bitcoin/bitcoinApiConfig.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
export interface BitcoinApiConfig {
mainnet: string;
testnet: string;
name: string;
}
import { ApiConfig } from '../../types/bitcoin.js';

export const bitcoinApiConfig: Record<string, BitcoinApiConfig> = {
blockstream: {
mainnet: 'https://blockstream.info',
testnet: 'https://blockstream.info/testnet',
name: 'blockstream',
export const APIs: Record<string, ApiConfig> = {
btcscan: {
name: 'btcscan',
url: {
balance: (address: string) => `https://btcscan.org/api/address/${address}`,
transaction: (txHash: string) => `https://btcscan.org/api/tx/${txHash}/status`,
},
},
blockchain: {
name: 'blockchain.info',
url: {
balance: (address: string) => `https://api.blockchain.info/haskoin-store/btc/address/${address}/balance`,
transaction: (txHash: string) => `https://api.blockchain.info/haskoin-store/btc/transaction/${txHash}`,
},
},
mempool: {
mainnet: 'https://mempool.space',
testnet: 'https://mempool.space/testnet',
name: 'mempool',
blockcypher: {
name: 'blockcypher',
url: {
balance: (address: string) => `https://api.blockcypher.com/v1/btc/main/addrs/${address}/balance`,
transaction: (txHash: string) => `https://api.blockcypher.com/v1/btc/main/txs/${txHash}`,
},
},
};

export interface BitcoinApiConfigResult {
baseUrl: string;
name: string;
}
export const CACHE_EXPIRATION_TIME = 30 * 60 * 1000; // 30 minutes
export const REQUEST_TIMEOUT = 5000; // 5 seconds
export const MAX_RETRIES = 3;
export const RETRY_DELAY = 1000;

export function getBitcoinApiConfig(isTestnet: boolean, apiName: string = 'blockstream'): BitcoinApiConfigResult {
const config = bitcoinApiConfig[apiName] || bitcoinApiConfig.blockstream;
return {
baseUrl: isTestnet ? config.testnet : config.mainnet,
name: config.name,
};
export class FetchError extends Error {
constructor(
message: string,
public readonly status?: number,
public readonly statusText?: string,
) {
super(message);
this.name = 'FetchError';
}
}
Loading

0 comments on commit b45deaa

Please sign in to comment.