[liquid] use bitcoin esplora for peg parsing queries

This commit is contained in:
Mononaut 2026-01-27 00:54:56 +00:00
parent 27ade6c20e
commit d1ed7f4429
No known key found for this signature in database
GPG key ID: A3F058E41374C04E
6 changed files with 135 additions and 13 deletions

View file

@ -64,6 +64,9 @@
"FALLBACK": [],
"MAX_BEHIND_TIP": 2
},
"SECOND_ESPLORA": {
"UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet"
},
"SECOND_CORE_RPC": {
"HOST": "127.0.0.1",
"PORT": 8332,

View file

@ -0,0 +1,89 @@
import config from '../../config';
import axios from 'axios';
import http from 'http';
import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger';
class EsploraSecondApi {
private requestConnection = axios.create({
httpAgent: new http.Agent({ keepAlive: true })
});
private async $query<T>(method: 'get' | 'post', path: string, data?: any, responseType = 'json'): Promise<T> {
let axiosConfig: any;
let url: string;
if (config.SECOND_ESPLORA.UNIX_SOCKET_PATH) {
axiosConfig = { socketPath: config.SECOND_ESPLORA.UNIX_SOCKET_PATH, timeout: config.SECOND_ESPLORA.REQUEST_TIMEOUT, responseType };
url = 'http://api' + path;
} else {
axiosConfig = { timeout: config.SECOND_ESPLORA.REQUEST_TIMEOUT, responseType };
url = config.SECOND_ESPLORA.REST_API_URL + path;
}
try {
const response = method === 'post'
? await this.requestConnection.post<T>(url, data, axiosConfig)
: await this.requestConnection.get<T>(url, axiosConfig);
return response.data;
} catch (e: any) {
logger.warn(`Second esplora request failed: ${url}`);
logger.warn(e instanceof Error ? e.message : e);
throw e;
}
}
async $getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
return this.$query<IEsploraApi.Transaction>('get', '/tx/' + txId);
}
async $getTransactionHex(txId: string): Promise<string> {
return this.$query<string>('get', '/tx/' + txId + '/hex');
}
async $getBlockHeightTip(): Promise<number> {
return this.$query<number>('get', '/blocks/tip/height');
}
async $getBlockHashTip(): Promise<string> {
return this.$query<string>('get', '/blocks/tip/hash');
}
async $getBlockHash(height: number): Promise<string> {
return this.$query<string>('get', '/block-height/' + height);
}
async $getBlock(hash: string): Promise<IEsploraApi.Block> {
return this.$query<IEsploraApi.Block>('get', '/block/' + hash);
}
async $getBlockHeader(hash: string): Promise<string> {
return this.$query<string>('get', '/block/' + hash + '/header');
}
async $getTxIdsForBlock(hash: string): Promise<string[]> {
return this.$query<string[]>('get', '/block/' + hash + '/txids');
}
async $getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
return this.$query<IEsploraApi.Transaction[]>('get', '/internal/block/' + hash + '/txs');
}
async $getAddress(address: string): Promise<IEsploraApi.Address> {
return this.$query<IEsploraApi.Address>('get', '/address/' + address);
}
async $getAddressUtxos(address: string): Promise<IEsploraApi.UTXO[]> {
return this.$query<IEsploraApi.UTXO[]>('get', '/address/' + address + '/utxo');
}
async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
return this.$query<IEsploraApi.Outspend>('get', '/tx/' + txId + '/outspend/' + vout);
}
async $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
return this.$query<IEsploraApi.Outspend[]>('get', '/tx/' + txId + '/outspends');
}
}
export default new EsploraSecondApi();

View file

@ -1,9 +1,11 @@
import { IBitcoinApi } from '../bitcoin/bitcoin-api.interface';
import bitcoinClient from '../bitcoin/bitcoin-client';
import bitcoinSecondClient from '../bitcoin/bitcoin-second-client';
import bitcoinEsploraApi from '../bitcoin/esplora-second-api';
import { Common } from '../common';
import DB from '../../database';
import logger from '../../logger';
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
const federationChangeAddresses = ['bc1qxvay4an52gcghxq5lavact7r6qe9l4laedsazz8fj2ee2cy47tlqff4aj4', '3EiAcrzq1cELXScc98KeCswGWZaPGceT1d', '3G6neksSBMp51kHJ2if8SeDUrzT8iVETWT', 'bc1qwnevjp8nsq7adu3hxlvdvslrf242q4vuavfg0y929jp2zntp3vgq7cq6z2'];
const auditBlockOffsetWithTip = 1; // Wait for 1 block confirmation before processing the block in the audit process to reduce the risk of reorgs
@ -52,12 +54,18 @@ class ElementsParser {
}
protected async $parsePegIn(input: IBitcoinApi.Vin, vindex: number, txid: string, block: IBitcoinApi.Block) {
const bitcoinTx: IBitcoinApi.Transaction = await bitcoinSecondClient.getRawTransaction(input.txid, true);
const bitcoinBlock: IBitcoinApi.Block = await bitcoinSecondClient.getBlock(bitcoinTx.blockhash);
const prevout = bitcoinTx.vout[input.vout || 0];
const outputAddress = prevout.scriptPubKey.address || (prevout.scriptPubKey.addresses && prevout.scriptPubKey.addresses[0]) || '';
await this.$savePegToDatabase(block.height, block.time, prevout.value * 100000000, txid, vindex,
outputAddress, bitcoinTx.txid, prevout.n, bitcoinBlock.height, bitcoinBlock.time, 1);
if (!input.txid || input.vout == null) {
return;
}
const bitcoinTx: IEsploraApi.Transaction = await bitcoinEsploraApi.$getRawTransaction(input.txid);
if (!bitcoinTx.status.block_hash) {
return;
}
const bitcoinBlock: IEsploraApi.Block = await bitcoinEsploraApi.$getBlock(bitcoinTx.status.block_hash);
const prevout = bitcoinTx.vout[input.vout];
const outputAddress = prevout.scriptpubkey_address || '';
await this.$savePegToDatabase(bitcoinBlock.height, bitcoinBlock.timestamp, prevout.value * 100000000, txid, vindex,
outputAddress, bitcoinTx.txid, input.vout, bitcoinBlock.height, bitcoinBlock.timestamp, 1);
}
protected async $parseOutputs(tx: IBitcoinApi.Transaction, block: IBitcoinApi.Block) {
@ -184,7 +192,7 @@ class ElementsParser {
}
// The slow way: parse the block to look for the spending tx
const blockHash: IBitcoinApi.ChainTips = await bitcoinSecondClient.getBlockHash(auditProgress.lastBlockAudit);
const blockHash: string = await bitcoinEsploraApi.$getBlockHash(auditProgress.lastBlockAudit);
const block: IBitcoinApi.Block = await bitcoinSecondClient.getBlock(blockHash, 2);
await this.$parseBitcoinBlock(block, spentAsTip, unspentAsTip, auditProgress.confirmedTip, redeemAddresses);
@ -217,8 +225,12 @@ class ElementsParser {
const unspentAsTip: any[] = [];
for (const utxo of utxos) {
const result = await bitcoinSecondClient.getTxOut(utxo.txid, utxo.txindex, false);
result ? unspentAsTip.push(utxo) : spentAsTip.push(utxo);
const outspend = await bitcoinEsploraApi.$getOutspend(utxo.txid, utxo.txindex);
if (outspend?.spent) {
spentAsTip.push(utxo);
} else {
unspentAsTip.push(utxo);
}
}
return {spentAsTip, unspentAsTip};
@ -323,10 +335,10 @@ class ElementsParser {
// Get the bitcoin block where the audit process was last updated
protected async $getAuditProgress(): Promise<any> {
const lastblockaudit = await this.$getLastBlockAudit();
const bitcoinBlocksToSync = await this.$getBitcoinBlockchainState();
const bitcoinHeight = await bitcoinEsploraApi.$getBlockHeightTip();
return {
lastBlockAudit: lastblockaudit,
confirmedTip: bitcoinBlocksToSync.bitcoinBlocks - auditBlockOffsetWithTip,
confirmedTip: bitcoinHeight - auditBlockOffsetWithTip,
};
}
@ -393,7 +405,7 @@ class ElementsParser {
public async $getCurrentLbtcSupply(): Promise<any> {
const [rows] = await DB.query(`SELECT SUM(amount) AS LBTC_supply FROM elements_pegs;`);
const lastblockupdate = await this.$getLatestBlockHeightFromDatabase();
const hash = await bitcoinClient.getBlockHash(lastblockupdate);
const hash = await bitcoinEsploraApi.$getBlockHash(lastblockupdate);
return {
amount: rows[0]['LBTC_supply'],
lastBlockUpdate: lastblockupdate,
@ -405,7 +417,7 @@ class ElementsParser {
public async $getCurrentFederationReserves(): Promise<any> {
const [rows] = await DB.query(`SELECT SUM(amount) AS total_balance FROM federation_txos WHERE unspent = 1 AND expiredAt = 0;`);
const lastblockaudit = await this.$getLastBlockAudit();
const hash = await bitcoinSecondClient.getBlockHash(lastblockaudit);
const hash = await bitcoinEsploraApi.$getBlockHash(lastblockaudit);
return {
amount: rows[0]['total_balance'],
lastBlockUpdate: lastblockaudit,

View file

@ -54,6 +54,11 @@ interface IConfig {
FALLBACK: string[];
MAX_BEHIND_TIP: number;
};
SECOND_ESPLORA: {
REST_API_URL: string;
UNIX_SOCKET_PATH: string | void | null;
REQUEST_TIMEOUT: number;
};
LIGHTNING: {
ENABLED: boolean;
BACKEND: 'lnd' | 'cln' | 'ldk';
@ -225,6 +230,11 @@ const defaults: IConfig = {
'FALLBACK': [],
'MAX_BEHIND_TIP': 2,
},
'SECOND_ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000',
'UNIX_SOCKET_PATH': null,
'REQUEST_TIMEOUT': 10000,
},
'ELECTRUM': {
'HOST': '127.0.0.1',
'PORT': 3306,
@ -347,6 +357,7 @@ const defaults: IConfig = {
class Config implements IConfig {
MEMPOOL: IConfig['MEMPOOL'];
ESPLORA: IConfig['ESPLORA'];
SECOND_ESPLORA: IConfig['SECOND_ESPLORA'];
ELECTRUM: IConfig['ELECTRUM'];
CORE_RPC: IConfig['CORE_RPC'];
SECOND_CORE_RPC: IConfig['SECOND_CORE_RPC'];
@ -370,6 +381,7 @@ class Config implements IConfig {
const configs = this.merge(configFromFile, defaults);
this.MEMPOOL = configs.MEMPOOL;
this.ESPLORA = configs.ESPLORA;
this.SECOND_ESPLORA = configs.SECOND_ESPLORA;
this.ELECTRUM = configs.ELECTRUM;
this.CORE_RPC = configs.CORE_RPC;
this.SECOND_CORE_RPC = configs.SECOND_CORE_RPC;

View file

@ -70,6 +70,9 @@
"http://node206.tk7.mempool.space:3001"
]
},
"SECOND_ESPLORA": {
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet"
},
"DATABASE": {
"ENABLED": true,
"HOST": "127.0.0.1",

View file

@ -70,6 +70,9 @@
"http://node206.tk7.mempool.space:3004"
]
},
"SECOND_ESPLORA": {
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet"
},
"DATABASE": {
"ENABLED": true,
"HOST": "127.0.0.1",