mirror of
https://github.com/mempool/mempool.git
synced 2026-08-13 12:33:11 +02:00
[liquid] use bitcoin esplora for peg parsing queries
This commit is contained in:
parent
27ade6c20e
commit
d1ed7f4429
6 changed files with 135 additions and 13 deletions
|
|
@ -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,
|
||||
|
|
|
|||
89
backend/src/api/bitcoin/esplora-second-api.ts
Normal file
89
backend/src/api/bitcoin/esplora-second-api.ts
Normal 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();
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue