diff --git a/backend/src/api/bitcoin/bitcoin-api.interface.ts b/backend/src/api/bitcoin/bitcoin-api.interface.ts index 04c158211..ad603835a 100644 --- a/backend/src/api/bitcoin/bitcoin-api.interface.ts +++ b/backend/src/api/bitcoin/bitcoin-api.interface.ts @@ -72,7 +72,7 @@ export namespace IBitcoinApi { time: number; // (numeric) Same as blocktime } - interface Vin { + export interface Vin { txid?: string; // (string) The transaction id vout?: number; // (string) scriptSig?: { // (json object) The script @@ -82,18 +82,22 @@ export namespace IBitcoinApi { sequence: number; // (numeric) The script sequence number txinwitness?: string[]; // (string) hex-encoded witness data coinbase?: string; + is_pegin?: boolean; // (boolean) Elements peg-in } - interface Vout { + export interface Vout { value: number; // (numeric) The value in BTC n: number; // (numeric) index + asset?: string; // (string) Elements asset id scriptPubKey: { // (json object) asm: string; // (string) the asm hex: string; // (string) the hex reqSigs?: number; // (numeric) The required sigs type: string; // (string) The type, eg 'pubkeyhash' - addresses?: string[]; // (string) bitcoin addresses address?: string; // (string) bitcoin address + addresses?: string[]; // (string) bitcoin addresses + pegout_chain?: string; // (string) Elements peg-out chain + pegout_addresses?: string[]; // (string) Elements peg-out addresses }; } diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 5c59582a1..0a4a210d1 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -1,6 +1,8 @@ import { CpfpInfo, TransactionExtended, TransactionStripped } from '../mempool.interfaces'; import config from '../config'; export class Common { + static nativeAssetId = '6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d'; + static median(numbers: number[]) { let medianNr = 0; const numsLen = numbers.length; diff --git a/backend/src/api/liquid/elements-parser.ts b/backend/src/api/liquid/elements-parser.ts new file mode 100644 index 000000000..b2618a66e --- /dev/null +++ b/backend/src/api/liquid/elements-parser.ts @@ -0,0 +1,105 @@ +import { IBitcoinApi } from '../bitcoin/bitcoin-api.interface'; +import bitcoinClient from '../bitcoin/bitcoin-client'; +import bitcoinSecondClient from '../bitcoin/bitcoin-second-client'; +import { Common } from '../common'; +import { DB } from '../../database'; +import logger from '../../logger'; + +class ElementsParser { + isRunning = false; + constructor() { } + + public async $parse() { + if (this.isRunning) { + return; + } + this.isRunning = true; + const result = await bitcoinClient.getChainTips(); + const tip = result[0].height; + const latestBlock = await this.$getLatestBlockFromDatabase(); + for (let height = latestBlock.block + 1; height <= tip; height++) { + const blockHash: IBitcoinApi.ChainTips = await bitcoinClient.getBlockHash(height); + const block: IBitcoinApi.Block = await bitcoinClient.getBlock(blockHash, 2); + await this.$parseBlock(block); + await this.$saveLatestBlockToDatabase(block.height, block.time, block.hash); + } + this.isRunning = false; + } + + public async $getPegDataByMonth(): Promise { + const connection = await DB.pool.getConnection(); + 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 connection.query(query); + connection.release(); + return rows; + } + + protected async $parseBlock(block: IBitcoinApi.Block) { + for (const tx of block.tx) { + await this.$parseInputs(tx, block); + await this.$parseOutputs(tx, block); + } + } + + protected async $parseInputs(tx: IBitcoinApi.Transaction, block: IBitcoinApi.Block) { + for (const [index, input] of tx.vin.entries()) { + if (input.is_pegin) { + await this.$parsePegIn(input, index, tx.txid, 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 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, 1); + } + + protected async $parseOutputs(tx: IBitcoinApi.Transaction, block: IBitcoinApi.Block) { + for (const output of tx.vout) { + if (output.scriptPubKey.pegout_chain) { + 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); + } + if (!output.scriptPubKey.pegout_chain && output.scriptPubKey.type === 'nulldata' + && 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, + (output.scriptPubKey.pegout_addresses && output.scriptPubKey.pegout_addresses[0] || ''), '', 0, 1); + } + } + } + + protected async $savePegToDatabase(height: number, blockTime: number, amount: number, txid: string, + txindex: number, bitcoinaddress: string, bitcointxid: string, bitcoinindex: number, final_tx: number): Promise { + const connection = await DB.pool.getConnection(); + const query = `INSERT INTO elements_pegs( + block, datetime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`; + + const params: (string | number)[] = [ + height, blockTime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx + ]; + await connection.query(query, params); + connection.release(); + logger.debug(`Saved L-BTC peg from block height #${height} with TXID ${txid}.`); + } + + protected async $getLatestBlockFromDatabase(): Promise { + const connection = await DB.pool.getConnection(); + const query = `SELECT block, datetime, block_hash FROM last_elements_block`; + const [rows] = await connection.query(query); + connection.release(); + return rows[0]; + } + + protected async $saveLatestBlockToDatabase(blockHeight: number, datetime: number, blockHash: string) { + const connection = await DB.pool.getConnection(); + const query = `UPDATE last_elements_block SET block = ?, datetime = ?, block_hash = ?`; + await connection.query(query, [blockHeight, datetime, blockHash]); + connection.release(); + } +} + +export default new ElementsParser(); diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 29b3b7d8f..7891a606a 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -14,7 +14,6 @@ import transactionUtils from './transaction-utils'; class WebsocketHandler { private wss: WebSocket.Server | undefined; - private nativeAssetId = '6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d'; private extraInitProperties = {}; constructor() { } @@ -308,7 +307,7 @@ class WebsocketHandler { newTransactions.forEach((tx) => { - if (client['track-asset'] === this.nativeAssetId) { + if (client['track-asset'] === Common.nativeAssetId) { if (tx.vin.some((vin) => !!vin.is_pegin)) { foundTransactions.push(tx); return; @@ -439,7 +438,7 @@ class WebsocketHandler { const foundTransactions: TransactionExtended[] = []; transactions.forEach((tx) => { - if (client['track-asset'] === this.nativeAssetId) { + if (client['track-asset'] === Common.nativeAssetId) { if (tx.vin && tx.vin.some((vin) => !!vin.is_pegin)) { foundTransactions.push(tx); return; diff --git a/backend/src/index.ts b/backend/src/index.ts index 9756f343a..a07903a7b 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -20,6 +20,7 @@ import logger from './logger'; import backendInfo from './api/backend-info'; import loadingIndicators from './api/loading-indicators'; import mempool from './api/mempool'; +import elementsParser from './api/liquid/elements-parser'; class Server { private wss: WebSocket.Server | undefined; @@ -141,6 +142,15 @@ class Server { if (this.wss) { websocketHandler.setWebsocketServer(this.wss); } + if (config.MEMPOOL.NETWORK === 'liquid') { + blocks.setNewBlockCallback(async () => { + try { + await elementsParser.$parse(); + } catch (e) { + logger.warn('Elements parsing error: ' + (e instanceof Error ? e.message : e)); + } + }); + } websocketHandler.setupConnectionHandling(); statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler)); blocks.setNewBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler)); @@ -254,6 +264,12 @@ class Server { .get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', routes.getAddressPrefix) ; } + + if (config.MEMPOOL.NETWORK === 'liquid') { + this.app + .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', routes.$getElementsPegsByMonth) + ; + } } } diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 26d88eade..8db3e4b89 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -18,6 +18,7 @@ import blocks from './api/blocks'; import loadingIndicators from './api/loading-indicators'; import { Common } from './api/common'; import bitcoinClient from './api/bitcoin/bitcoin-client'; +import elementsParser from './api/liquid/elements-parser'; class Routes { constructor() {} @@ -754,6 +755,15 @@ class Routes { res.status(500).send(e instanceof Error ? e.message : e); } } + + public async $getElementsPegsByMonth(req: Request, res: Response) { + try { + const pegs = await elementsParser.$getPegDataByMonth(); + res.json(pegs); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } } export default new Routes(); diff --git a/mariadb-structure.sql b/mariadb-structure.sql index 4d567ed91..f652a0a54 100644 --- a/mariadb-structure.sql +++ b/mariadb-structure.sql @@ -84,3 +84,23 @@ ALTER TABLE `transactions` ALTER TABLE `statistics` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; + +CREATE TABLE `last_elements_block` ( + `block` int(11) NOT NULL, + `datetime` int(11) NOT NULL, + `block_hash` varchar(65) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +INSERT INTO `last_elements_block` VALUES(0, 0, ''); + +CREATE TABLE `elements_pegs` ( + `block` int(11) NOT NULL, + `datetime` int(11) NOT NULL, + `amount` bigint(20) NOT NULL, + `txid` varchar(65) NOT NULL, + `txindex` int(11) NOT NULL, + `bitcoinaddress` varchar(100) NOT NULL, + `bitcointxid` varchar(65) NOT NULL, + `bitcoinindex` int(11) NOT NULL, + `final_tx` int(11) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8;