Liquid: add indexing process of Federation utxos
This commit is contained in:
parent
6671746fc5
commit
4b10e32e73
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
|||||||
import { RowDataPacket } from 'mysql2';
|
import { RowDataPacket } from 'mysql2';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 66;
|
private static currentVersion = 67;
|
||||||
private queryTimeout = 3600_000;
|
private queryTimeout = 3600_000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@ -558,6 +558,21 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `statistics` ADD min_fee FLOAT UNSIGNED DEFAULT NULL');
|
await this.$executeQuery('ALTER TABLE `statistics` ADD min_fee FLOAT UNSIGNED DEFAULT NULL');
|
||||||
await this.updateToSchemaVersion(66);
|
await this.updateToSchemaVersion(66);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 67 && config.MEMPOOL.NETWORK === "liquid") {
|
||||||
|
// Drop and re-create the elements_pegs table
|
||||||
|
await this.$executeQuery('DROP table IF EXISTS elements_pegs;');
|
||||||
|
await this.$executeQuery(this.getCreateElementsTableQuery(), await this.$checkIfTableExists('elements_pegs'));
|
||||||
|
await this.$executeQuery(`UPDATE state SET number = 0 WHERE name = 'last_elements_block';`);
|
||||||
|
// Create the federation_addresses table and add the two Liquid Federation change addresses in
|
||||||
|
await this.$executeQuery(this.getCreateFederationAddressesTableQuery(), await this.$checkIfTableExists('federation_addresses'));
|
||||||
|
await this.$executeQuery(`INSERT INTO federation_addresses (bitcoinaddress) VALUES ('bc1qxvay4an52gcghxq5lavact7r6qe9l4laedsazz8fj2ee2cy47tlqff4aj4')`); // Federation change address
|
||||||
|
await this.$executeQuery(`INSERT INTO federation_addresses (bitcoinaddress) VALUES ('3EiAcrzq1cELXScc98KeCswGWZaPGceT1d')`); // Federation change address
|
||||||
|
// Create the federation_txos table that uses the federation_addresses table as a foreign key
|
||||||
|
await this.$executeQuery(this.getCreateFederationTxosTableQuery(), await this.$checkIfTableExists('federation_txos'));
|
||||||
|
await this.$executeQuery(`INSERT INTO state VALUES('last_bitcoin_block_audit', 0, NULL);`);
|
||||||
|
await this.updateToSchemaVersion(67);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -801,7 +816,31 @@ class DatabaseMigration {
|
|||||||
bitcoinaddress varchar(100) NOT NULL,
|
bitcoinaddress varchar(100) NOT NULL,
|
||||||
bitcointxid varchar(65) NOT NULL,
|
bitcointxid varchar(65) NOT NULL,
|
||||||
bitcoinindex int(11) NOT NULL,
|
bitcoinindex int(11) NOT NULL,
|
||||||
final_tx int(11) NOT NULL
|
final_tx int(11) NOT NULL,
|
||||||
|
PRIMARY KEY (txid, txindex)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCreateFederationAddressesTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS federation_addresses (
|
||||||
|
bitcoinaddress varchar(100) NOT NULL,
|
||||||
|
PRIMARY KEY (bitcoinaddress)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCreateFederationTxosTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS federation_txos (
|
||||||
|
txid varchar(65) NOT NULL,
|
||||||
|
txindex int(11) NOT NULL,
|
||||||
|
bitcoinaddress varchar(100) NOT NULL,
|
||||||
|
amount bigint(20) unsigned NOT NULL,
|
||||||
|
blocknumber int(11) unsigned NOT NULL,
|
||||||
|
blocktime int(11) unsigned NOT NULL,
|
||||||
|
unspent tinyint(1) NOT NULL,
|
||||||
|
lastblockupdate int(11) unsigned NOT NULL,
|
||||||
|
lasttimeupdate int(11) unsigned NOT NULL,
|
||||||
|
PRIMARY KEY (txid, txindex),
|
||||||
|
FOREIGN KEY (bitcoinaddress) REFERENCES federation_addresses (bitcoinaddress)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,8 +5,12 @@ import { Common } from '../common';
|
|||||||
import DB from '../../database';
|
import DB from '../../database';
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
|
|
||||||
|
const federationChangeAddresses = ['bc1qxvay4an52gcghxq5lavact7r6qe9l4laedsazz8fj2ee2cy47tlqff4aj4', '3EiAcrzq1cELXScc98KeCswGWZaPGceT1d'];
|
||||||
|
const auditBlockOffsetWithTip = 1; // Wait for 1 block confirmation before processing the block in the audit process to reduce the risk of reorgs
|
||||||
|
|
||||||
class ElementsParser {
|
class ElementsParser {
|
||||||
private isRunning = false;
|
private isRunning = false;
|
||||||
|
private isUtxosUpdatingRunning = false;
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
@ -22,22 +26,19 @@ class ElementsParser {
|
|||||||
for (let height = latestBlockHeight + 1; height <= tip; height++) {
|
for (let height = latestBlockHeight + 1; height <= tip; height++) {
|
||||||
const blockHash: IBitcoinApi.ChainTips = await bitcoinClient.getBlockHash(height);
|
const blockHash: IBitcoinApi.ChainTips = await bitcoinClient.getBlockHash(height);
|
||||||
const block: IBitcoinApi.Block = await bitcoinClient.getBlock(blockHash, 2);
|
const block: IBitcoinApi.Block = await bitcoinClient.getBlock(blockHash, 2);
|
||||||
|
await DB.query('START TRANSACTION;');
|
||||||
await this.$parseBlock(block);
|
await this.$parseBlock(block);
|
||||||
await this.$saveLatestBlockToDatabase(block.height);
|
await this.$saveLatestBlockToDatabase(block.height);
|
||||||
|
await DB.query('COMMIT;');
|
||||||
}
|
}
|
||||||
this.isRunning = false;
|
this.isRunning = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
await DB.query('ROLLBACK;');
|
||||||
this.isRunning = false;
|
this.isRunning = false;
|
||||||
throw new Error(e instanceof Error ? e.message : 'Error');
|
throw new Error(e instanceof Error ? e.message : 'Error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getPegDataByMonth(): Promise<any> {
|
|
||||||
const query = `SELECT SUM(amount) AS amount, DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y-%m-01') AS date FROM elements_pegs GROUP BY DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y%m')`;
|
|
||||||
const [rows] = await DB.query(query);
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async $parseBlock(block: IBitcoinApi.Block) {
|
protected async $parseBlock(block: IBitcoinApi.Block) {
|
||||||
for (const tx of block.tx) {
|
for (const tx of block.tx) {
|
||||||
await this.$parseInputs(tx, block);
|
await this.$parseInputs(tx, block);
|
||||||
@ -55,29 +56,30 @@ class ElementsParser {
|
|||||||
|
|
||||||
protected async $parsePegIn(input: IBitcoinApi.Vin, vindex: number, txid: string, block: IBitcoinApi.Block) {
|
protected async $parsePegIn(input: IBitcoinApi.Vin, vindex: number, txid: string, block: IBitcoinApi.Block) {
|
||||||
const bitcoinTx: IBitcoinApi.Transaction = await bitcoinSecondClient.getRawTransaction(input.txid, true);
|
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 prevout = bitcoinTx.vout[input.vout || 0];
|
||||||
const outputAddress = prevout.scriptPubKey.address || (prevout.scriptPubKey.addresses && prevout.scriptPubKey.addresses[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,
|
await this.$savePegToDatabase(block.height, block.time, prevout.value * 100000000, txid, vindex,
|
||||||
outputAddress, bitcoinTx.txid, prevout.n, 1);
|
outputAddress, bitcoinTx.txid, prevout.n, bitcoinBlock.height, bitcoinBlock.time, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async $parseOutputs(tx: IBitcoinApi.Transaction, block: IBitcoinApi.Block) {
|
protected async $parseOutputs(tx: IBitcoinApi.Transaction, block: IBitcoinApi.Block) {
|
||||||
for (const output of tx.vout) {
|
for (const output of tx.vout) {
|
||||||
if (output.scriptPubKey.pegout_chain) {
|
if (output.scriptPubKey.pegout_chain) {
|
||||||
await this.$savePegToDatabase(block.height, block.time, 0 - output.value * 100000000, tx.txid, output.n,
|
await this.$savePegToDatabase(block.height, block.time, 0 - output.value * 100000000, tx.txid, output.n,
|
||||||
(output.scriptPubKey.pegout_addresses && output.scriptPubKey.pegout_addresses[0] || ''), '', 0, 0);
|
(output.scriptPubKey.pegout_addresses && output.scriptPubKey.pegout_addresses[0] || ''), '', 0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
if (!output.scriptPubKey.pegout_chain && output.scriptPubKey.type === 'nulldata'
|
if (!output.scriptPubKey.pegout_chain && output.scriptPubKey.type === 'nulldata'
|
||||||
&& output.value && output.value > 0 && output.asset && output.asset === Common.nativeAssetId) {
|
&& output.value && output.value > 0 && output.asset && output.asset === Common.nativeAssetId) {
|
||||||
await this.$savePegToDatabase(block.height, block.time, 0 - output.value * 100000000, tx.txid, output.n,
|
await this.$savePegToDatabase(block.height, block.time, 0 - output.value * 100000000, tx.txid, output.n,
|
||||||
(output.scriptPubKey.pegout_addresses && output.scriptPubKey.pegout_addresses[0] || ''), '', 0, 1);
|
(output.scriptPubKey.pegout_addresses && output.scriptPubKey.pegout_addresses[0] || ''), '', 0, 0, 0, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async $savePegToDatabase(height: number, blockTime: number, amount: number, txid: string,
|
protected async $savePegToDatabase(height: number, blockTime: number, amount: number, txid: string,
|
||||||
txindex: number, bitcoinaddress: string, bitcointxid: string, bitcoinindex: number, final_tx: number): Promise<void> {
|
txindex: number, bitcoinaddress: string, bitcointxid: string, bitcoinindex: number, bitcoinblock: number, bitcoinBlockTime: number, final_tx: number): Promise<void> {
|
||||||
const query = `INSERT INTO elements_pegs(
|
const query = `INSERT IGNORE INTO elements_pegs(
|
||||||
block, datetime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx
|
block, datetime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||||
|
|
||||||
@ -85,7 +87,23 @@ class ElementsParser {
|
|||||||
height, blockTime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx
|
height, blockTime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx
|
||||||
];
|
];
|
||||||
await DB.query(query, params);
|
await DB.query(query, params);
|
||||||
logger.debug(`Saved L-BTC peg from block height #${height} with TXID ${txid}.`);
|
logger.debug(`Saved L-BTC peg from Liquid block height #${height} with TXID ${txid}.`);
|
||||||
|
|
||||||
|
if (amount > 0) { // Peg-in
|
||||||
|
|
||||||
|
// Add the address to the federation addresses table
|
||||||
|
await DB.query(`INSERT IGNORE INTO federation_addresses (bitcoinaddress) VALUES (?)`, [bitcoinaddress]);
|
||||||
|
logger.debug(`Saved new Federation address ${bitcoinaddress} to federation addresses.`);
|
||||||
|
|
||||||
|
// Add the UTXO to the federation txos table
|
||||||
|
const query_utxos = `INSERT IGNORE INTO federation_txos (txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, unspent, lastblockupdate, lasttimeupdate) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||||
|
const params_utxos: (string | number)[] = [bitcointxid, bitcoinindex, bitcoinaddress, amount, bitcoinblock, bitcoinBlockTime, 1, bitcoinblock - 1, 0];
|
||||||
|
await DB.query(query_utxos, params_utxos);
|
||||||
|
const [minBlockUpdate] = await DB.query(`SELECT MIN(lastblockupdate) AS lastblockupdate FROM federation_txos WHERE unspent = 1`)
|
||||||
|
await this.$saveLastBlockAuditToDatabase(minBlockUpdate[0]['lastblockupdate']);
|
||||||
|
logger.debug(`Saved new Federation UTXO ${bitcointxid}:${bitcoinindex} belonging to ${bitcoinaddress} to federation txos.`);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async $getLatestBlockHeightFromDatabase(): Promise<number> {
|
protected async $getLatestBlockHeightFromDatabase(): Promise<number> {
|
||||||
@ -98,6 +116,269 @@ class ElementsParser {
|
|||||||
const query = `UPDATE state SET number = ? WHERE name = 'last_elements_block'`;
|
const query = `UPDATE state SET number = ? WHERE name = 'last_elements_block'`;
|
||||||
await DB.query(query, [blockHeight]);
|
await DB.query(query, [blockHeight]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////// FEDERATION AUDIT //////////////
|
||||||
|
|
||||||
|
public async $updateFederationUtxos() {
|
||||||
|
if (this.isUtxosUpdatingRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isUtxosUpdatingRunning = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let auditProgress = await this.$getAuditProgress();
|
||||||
|
// If no peg in transaction was found in the database, return
|
||||||
|
if (!auditProgress.lastBlockAudit) {
|
||||||
|
logger.debug(`No Federation UTXOs found in the database. Waiting for some to be confirmed before starting the Federation UTXOs audit.`);
|
||||||
|
this.isUtxosUpdatingRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bitcoinBlocksToSync = await this.$getBitcoinBlockchainState();
|
||||||
|
// If the bitcoin blockchain is not synced yet, return
|
||||||
|
if (bitcoinBlocksToSync.bitcoinHeaders > bitcoinBlocksToSync.bitcoinBlocks + 1) {
|
||||||
|
logger.debug(`Bitcoin client is not synced yet. ${bitcoinBlocksToSync.bitcoinHeaders - bitcoinBlocksToSync.bitcoinBlocks} blocks remaining to sync before the Federation audit process can start.`);
|
||||||
|
this.isUtxosUpdatingRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auditProgress.lastBlockAudit++;
|
||||||
|
|
||||||
|
while (auditProgress.lastBlockAudit <= auditProgress.confirmedTip) {
|
||||||
|
// First, get the current UTXOs that need to be scanned in the block
|
||||||
|
const utxos = await this.$getFederationUtxosToScan(auditProgress.lastBlockAudit);
|
||||||
|
logger.debug(`Found ${utxos.length} Federation UTXOs to scan in block ${auditProgress.lastBlockAudit} / ${auditProgress.confirmedTip}`);
|
||||||
|
|
||||||
|
// The fast way: check if these UTXOs are still unspent as of the current block with gettxout
|
||||||
|
let spentAsTip: any[];
|
||||||
|
let unspentAsTip: any[];
|
||||||
|
if (auditProgress.confirmedTip - auditProgress.lastBlockAudit <= 150) { // If the audit status is not too far in the past, we can use gettxout (fast way)
|
||||||
|
const utxosToParse = await this.$getFederationUtxosToParse(utxos);
|
||||||
|
spentAsTip = utxosToParse.spentAsTip;
|
||||||
|
unspentAsTip = utxosToParse.unspentAsTip;
|
||||||
|
logger.debug(`${unspentAsTip.length} / ${utxos.length} Federation UTXOs are unspent as of tip`);
|
||||||
|
} else { // If the audit status is too far in the past, it is useless to look for still unspent txos since they will all be spent as of the tip
|
||||||
|
spentAsTip = utxos;
|
||||||
|
unspentAsTip = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The slow way: parse the block to look for the spending tx
|
||||||
|
logger.debug(`${spentAsTip.length} / ${utxos.length} Federation UTXOs are spent as of tip`);
|
||||||
|
|
||||||
|
const blockHash: IBitcoinApi.ChainTips = await bitcoinSecondClient.getBlockHash(auditProgress.lastBlockAudit);
|
||||||
|
const block: IBitcoinApi.Block = await bitcoinSecondClient.getBlock(blockHash, 2);
|
||||||
|
const nbUtxos = spentAsTip.length;
|
||||||
|
await DB.query('START TRANSACTION;');
|
||||||
|
await this.$parseBitcoinBlock(block, spentAsTip, unspentAsTip, auditProgress.confirmedTip);
|
||||||
|
await DB.query(`COMMIT;`);
|
||||||
|
logger.debug(`Watched for spending of ${nbUtxos} Federation UTXOs in block ${auditProgress.lastBlockAudit} / ${auditProgress.confirmedTip}`);
|
||||||
|
|
||||||
|
// Finally, update the lastblockupdate of the remaining UTXOs and save to the database
|
||||||
|
const [minBlockUpdate] = await DB.query(`SELECT MIN(lastblockupdate) AS lastblockupdate FROM federation_txos WHERE unspent = 1`)
|
||||||
|
await this.$saveLastBlockAuditToDatabase(minBlockUpdate[0]['lastblockupdate']);
|
||||||
|
|
||||||
|
auditProgress = await this.$getAuditProgress();
|
||||||
|
auditProgress.lastBlockAudit++;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isUtxosUpdatingRunning = false;
|
||||||
|
} catch (e) {
|
||||||
|
await DB.query('ROLLBACK;');
|
||||||
|
this.isUtxosUpdatingRunning = false;
|
||||||
|
throw new Error(e instanceof Error ? e.message : 'Error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the UTXOs that need to be scanned in block height (UTXOs that were last updated in the block height - 1)
|
||||||
|
protected async $getFederationUtxosToScan(height: number) {
|
||||||
|
const query = `SELECT txid, txindex, bitcoinaddress, amount FROM federation_txos WHERE lastblockupdate = ? AND unspent = 1`;
|
||||||
|
const [rows] = await DB.query(query, [height - 1]);
|
||||||
|
return rows as any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the UTXOs that are spent as of tip and need to be scanned
|
||||||
|
protected async $getFederationUtxosToParse(utxos: any[]): Promise<any> {
|
||||||
|
const spentAsTip: any[] = [];
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {spentAsTip, unspentAsTip};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async $parseBitcoinBlock(block: IBitcoinApi.Block, spentAsTip: any[], unspentAsTip: any[], confirmedTip: number) {
|
||||||
|
for (const tx of block.tx) {
|
||||||
|
// Check if the Federation UTXOs that was spent as of tip are spent in this block
|
||||||
|
for (const input of tx.vin) {
|
||||||
|
const txo = spentAsTip.find(txo => txo.txid === input.txid && txo.txindex === input.vout);
|
||||||
|
if (txo) {
|
||||||
|
await DB.query(`UPDATE federation_txos SET unspent = 0, lastblockupdate = ?, lasttimeupdate = ? WHERE txid = ? AND txindex = ?`, [block.height, block.time, txo.txid, txo.txindex]);
|
||||||
|
// Remove the TXO from the utxo array
|
||||||
|
spentAsTip.splice(spentAsTip.indexOf(txo), 1);
|
||||||
|
logger.debug(`Federation UTXO ${txo.txid}:${txo.txindex} (${txo.amount} sats) was spent in block ${block.height}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check if an output is sent to a change address of the federation
|
||||||
|
for (const output of tx.vout) {
|
||||||
|
if (output.scriptPubKey.address && federationChangeAddresses.includes(output.scriptPubKey.address)) {
|
||||||
|
// Check that the UTXO was not already added in the DB by previous scans
|
||||||
|
const [rows_check] = await DB.query(`SELECT txid FROM federation_txos WHERE txid = ? AND txindex = ?`, [tx.txid, output.n]) as any[];
|
||||||
|
if (rows_check.length === 0) {
|
||||||
|
const query_utxos = `INSERT INTO federation_txos (txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, unspent, lastblockupdate, lasttimeupdate) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||||
|
const params_utxos: (string | number)[] = [tx.txid, output.n, output.scriptPubKey.address, output.value * 100000000, block.height, block.time, 1, block.height, 0];
|
||||||
|
await DB.query(query_utxos, params_utxos);
|
||||||
|
// Add the UTXO to the utxo array
|
||||||
|
spentAsTip.push({
|
||||||
|
txid: tx.txid,
|
||||||
|
txindex: output.n,
|
||||||
|
bitcoinaddress: output.scriptPubKey.address,
|
||||||
|
amount: output.value * 100000000
|
||||||
|
});
|
||||||
|
logger.debug(`Added new Federation UTXO ${tx.txid}:${output.n} of ${output.value * 100000000} sats belonging to ${output.scriptPubKey.address} (Federation change address).`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const utxo of spentAsTip) {
|
||||||
|
await DB.query(`UPDATE federation_txos SET lastblockupdate = ? WHERE txid = ? AND txindex = ?`, [block.height, utxo.txid, utxo.txindex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const utxo of unspentAsTip) {
|
||||||
|
await DB.query(`UPDATE federation_txos SET lastblockupdate = ? WHERE txid = ? AND txindex = ?`, [confirmedTip, utxo.txid, utxo.txindex]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async $saveLastBlockAuditToDatabase(blockHeight: number) {
|
||||||
|
const query = `UPDATE state SET number = ? WHERE name = 'last_bitcoin_block_audit'`;
|
||||||
|
await DB.query(query, [blockHeight]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
return {
|
||||||
|
lastBlockAudit: lastblockaudit,
|
||||||
|
confirmedTip: bitcoinBlocksToSync.bitcoinBlocks - auditBlockOffsetWithTip,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the bitcoin blocks remaining to be synced
|
||||||
|
protected async $getBitcoinBlockchainState(): Promise<any> {
|
||||||
|
const result = await bitcoinSecondClient.getBlockchainInfo();
|
||||||
|
return {
|
||||||
|
bitcoinBlocks: result.blocks,
|
||||||
|
bitcoinHeaders: result.headers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async $getLastBlockAudit(): Promise<number> {
|
||||||
|
const query = `SELECT number FROM state WHERE name = 'last_bitcoin_block_audit'`;
|
||||||
|
const [rows] = await DB.query(query);
|
||||||
|
return rows[0]['number'];
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////// DATA QUERY //////////////
|
||||||
|
|
||||||
|
public async $getAuditStatus(): Promise<any> {
|
||||||
|
const lastBlockAudit = await this.$getLastBlockAudit();
|
||||||
|
const bitcoinBlocksToSync = await this.$getBitcoinBlockchainState();
|
||||||
|
return {
|
||||||
|
bitcoinBlocks: bitcoinBlocksToSync.bitcoinBlocks,
|
||||||
|
bitcoinHeaders: bitcoinBlocksToSync.bitcoinHeaders,
|
||||||
|
lastBlockAudit: lastBlockAudit,
|
||||||
|
isAuditSynced: bitcoinBlocksToSync.bitcoinHeaders - bitcoinBlocksToSync.bitcoinBlocks <= 2 && bitcoinBlocksToSync.bitcoinBlocks - lastBlockAudit <= 3,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getPegDataByMonth(): Promise<any> {
|
||||||
|
const query = `SELECT SUM(amount) AS amount, DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y-%m-01') AS date FROM elements_pegs GROUP BY DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y%m')`;
|
||||||
|
const [rows] = await DB.query(query);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getFederationReservesByMonth(): Promise<any> {
|
||||||
|
const query = `
|
||||||
|
SELECT SUM(amount) AS amount, DATE_FORMAT(FROM_UNIXTIME(blocktime), '%Y-%m-01') AS date FROM federation_txos
|
||||||
|
WHERE
|
||||||
|
(blocktime > UNIX_TIMESTAMP(LAST_DAY(FROM_UNIXTIME(blocktime) - INTERVAL 1 MONTH) + INTERVAL 1 DAY))
|
||||||
|
AND
|
||||||
|
((unspent = 1) OR (unspent = 0 AND lasttimeupdate > UNIX_TIMESTAMP(LAST_DAY(FROM_UNIXTIME(blocktime)) + INTERVAL 1 DAY)))
|
||||||
|
GROUP BY
|
||||||
|
date;`;
|
||||||
|
const [rows] = await DB.query(query);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current L-BTC pegs and the last Liquid block it was updated
|
||||||
|
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);
|
||||||
|
return {
|
||||||
|
amount: rows[0]['LBTC_supply'],
|
||||||
|
lastBlockUpdate: lastblockupdate,
|
||||||
|
hash: hash
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current reserves of the federation and the last Bitcoin block it was updated
|
||||||
|
public async $getCurrentFederationReserves(): Promise<any> {
|
||||||
|
const [rows] = await DB.query(`SELECT SUM(amount) AS total_balance FROM federation_txos WHERE unspent = 1;`);
|
||||||
|
const lastblockaudit = await this.$getLastBlockAudit();
|
||||||
|
const hash = await bitcoinSecondClient.getBlockHash(lastblockaudit);
|
||||||
|
return {
|
||||||
|
amount: rows[0]['total_balance'],
|
||||||
|
lastBlockUpdate: lastblockaudit,
|
||||||
|
hash: hash
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all of the federation addresses, most balances first
|
||||||
|
public async $getFederationAddresses(): Promise<any> {
|
||||||
|
const query = `SELECT bitcoinaddress, SUM(amount) AS balance FROM federation_txos WHERE unspent = 1 GROUP BY bitcoinaddress ORDER BY balance DESC;`;
|
||||||
|
const [rows] = await DB.query(query);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all of the UTXOs held by the federation, most recent first
|
||||||
|
public async $getFederationUtxos(): Promise<any> {
|
||||||
|
const query = `SELECT txid, txindex, bitcoinaddress, amount, blocknumber, blocktime FROM federation_txos WHERE unspent = 1 ORDER BY blocktime DESC;`;
|
||||||
|
const [rows] = await DB.query(query);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all of the federation addresses one month ago, most balances first
|
||||||
|
public async $getFederationAddressesOneMonthAgo(): Promise<any> {
|
||||||
|
const query = `
|
||||||
|
SELECT bitcoinaddress, SUM(amount) AS balance FROM federation_txos
|
||||||
|
WHERE
|
||||||
|
(blocktime < UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP())))
|
||||||
|
AND
|
||||||
|
((unspent = 1) OR (unspent = 0 AND lasttimeupdate > UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP()))))
|
||||||
|
GROUP BY bitcoinaddress ORDER BY balance DESC;`;
|
||||||
|
const [rows] = await DB.query(query);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all of the UTXOs held by the federation one month ago, most recent first
|
||||||
|
public async $getFederationUtxosOneMonthAgo(): Promise<any> {
|
||||||
|
const query = `
|
||||||
|
SELECT txid, txindex, amount, blocknumber, blocktime FROM federation_txos
|
||||||
|
WHERE
|
||||||
|
(blocktime < UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP())))
|
||||||
|
AND
|
||||||
|
((unspent = 1) OR (unspent = 0 AND lasttimeupdate > UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP()))))
|
||||||
|
ORDER BY blocktime DESC;`;
|
||||||
|
const [rows] = await DB.query(query);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ElementsParser();
|
export default new ElementsParser();
|
||||||
|
@ -15,7 +15,15 @@ class LiquidRoutes {
|
|||||||
|
|
||||||
if (config.DATABASE.ENABLED) {
|
if (config.DATABASE.ENABLED) {
|
||||||
app
|
app
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs', this.$getElementsPegs)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', this.$getElementsPegsByMonth)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', this.$getElementsPegsByMonth)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves', this.$getFederationReserves)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/month', this.$getFederationReservesByMonth)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/addresses', this.$getFederationAddresses)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/addresses/previous-month', this.$getFederationAddressesOneMonthAgo)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos', this.$getFederationUtxos)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos/previous-month', this.$getFederationUtxosOneMonthAgo)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/status', this.$getFederationAuditStatus)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,11 +71,111 @@ class LiquidRoutes {
|
|||||||
private async $getElementsPegsByMonth(req: Request, res: Response) {
|
private async $getElementsPegsByMonth(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const pegs = await elementsParser.$getPegDataByMonth();
|
const pegs = await elementsParser.$getPegDataByMonth();
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
|
||||||
res.json(pegs);
|
res.json(pegs);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async $getFederationReservesByMonth(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const reserves = await elementsParser.$getFederationReservesByMonth();
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
|
||||||
|
res.json(reserves);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getElementsPegs(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const currentSupply = await elementsParser.$getCurrentLbtcSupply();
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
|
res.json(currentSupply);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getFederationReserves(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const currentReserves = await elementsParser.$getCurrentFederationReserves();
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
|
res.json(currentReserves);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getFederationAuditStatus(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const auditStatus = await elementsParser.$getAuditStatus();
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
|
res.json(auditStatus);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getFederationAddresses(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const federationAddresses = await elementsParser.$getFederationAddresses();
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
|
res.json(federationAddresses);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getFederationAddressesOneMonthAgo(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const federationAddresses = await elementsParser.$getFederationAddressesOneMonthAgo();
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60 * 24).toUTCString());
|
||||||
|
res.json(federationAddresses);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getFederationUtxos(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const federationUtxos = await elementsParser.$getFederationUtxos();
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
|
res.json(federationUtxos);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getFederationUtxosOneMonthAgo(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const federationUtxos = await elementsParser.$getFederationUtxosOneMonthAgo();
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60 * 24).toUTCString());
|
||||||
|
res.json(federationUtxos);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new LiquidRoutes();
|
export default new LiquidRoutes();
|
||||||
|
@ -266,6 +266,7 @@ class Server {
|
|||||||
blocks.setNewBlockCallback(async () => {
|
blocks.setNewBlockCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await elementsParser.$parse();
|
await elementsParser.$parse();
|
||||||
|
await elementsParser.$updateFederationUtxos();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn('Elements parsing error: ' + (e instanceof Error ? e.message : e));
|
logger.warn('Elements parsing error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user