Changeset View
Changeset View
Standalone View
Standalone View
apps/ecash-herald/src/parse.js
// Copyright (c) 2023 The Bitcoin developers | // Copyright (c) 2023 The Bitcoin developers | ||||
// Distributed under the MIT software license, see the accompanying | // Distributed under the MIT software license, see the accompanying | ||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php. | // file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
'use strict'; | 'use strict'; | ||||
const config = require('../config'); | const config = require('../config'); | ||||
const cashaddr = require('ecashaddrjs'); | const cashaddr = require('ecashaddrjs'); | ||||
const BigNumber = require('bignumber.js'); | const BigNumber = require('bignumber.js'); | ||||
const { | const { | ||||
prepareStringForTelegramHTML, | prepareStringForTelegramHTML, | ||||
splitOverflowTgMsg, | splitOverflowTgMsg, | ||||
} = require('./telegram'); | } = require('./telegram'); | ||||
const { formatPrice, returnAddressPreview } = require('./utils'); | const { | ||||
formatPrice, | |||||
returnAddressPreview, | |||||
getOutputScriptFromAddress, | |||||
} = require('./utils'); | |||||
module.exports = { | module.exports = { | ||||
parseBlock: function (chronikBlockResponse) { | parseBlock: function (chronikBlockResponse) { | ||||
const { blockInfo, txs } = chronikBlockResponse; | const { blockInfo, txs } = chronikBlockResponse; | ||||
const { hash } = blockInfo; | const { hash } = blockInfo; | ||||
const { height, numTxs } = blockInfo; | const { height, numTxs } = blockInfo; | ||||
// Parse coinbase string | // Parse coinbase string | ||||
const coinbaseScript = txs[0].inputs[0].inputScript; | const coinbaseTx = txs[0]; | ||||
const miner = module.exports.getMinerFromCoinbase(coinbaseScript); | const miner = module.exports.getMinerFromCoinbaseTx(coinbaseTx); | ||||
// Start with i=1 to skip Coinbase tx | // Start with i=1 to skip Coinbase tx | ||||
const parsedTxs = []; | const parsedTxs = []; | ||||
for (let i = 1; i < txs.length; i += 1) { | for (let i = 1; i < txs.length; i += 1) { | ||||
parsedTxs.push(module.exports.parseTx(txs[i])); | parsedTxs.push(module.exports.parseTx(txs[i])); | ||||
} | } | ||||
// Collect token info needed to parse token send txs | // Collect token info needed to parse token send txs | ||||
const tokenIds = new Set(); // we only need each tokenId once | const tokenIds = new Set(); // we only need each tokenId once | ||||
for (let i = 0; i < parsedTxs.length; i += 1) { | for (let i = 0; i < parsedTxs.length; i += 1) { | ||||
const thisParsedTx = parsedTxs[i]; | const thisParsedTx = parsedTxs[i]; | ||||
if (thisParsedTx.tokenSendInfo) { | if (thisParsedTx.tokenSendInfo) { | ||||
tokenIds.add(thisParsedTx.tokenSendInfo.tokenId); | tokenIds.add(thisParsedTx.tokenSendInfo.tokenId); | ||||
} | } | ||||
} | } | ||||
return { hash, height, miner, numTxs, parsedTxs, tokenIds }; | return { hash, height, miner, numTxs, parsedTxs, tokenIds }; | ||||
}, | }, | ||||
getMinerFromCoinbase: function (coinbaseHexString) { | getMinerFromCoinbaseTx: function (coinbaseTx) { | ||||
// get coinbase inputScript | |||||
const testedCoinbaseScript = coinbaseTx.inputs[0].inputScript; | |||||
const knownMiners = config.knownMiners; | |||||
Fabien: You should pass this as a parameter. That makes the test easier as you can craft any tx/pattern… | |||||
let miner = 'unknown'; | |||||
// First, iterate over known miners to find a match by coinbaseScript fragment | |||||
let thisCoinbaseFragment; | |||||
for (let i = 0; i < knownMiners.length; i += 1) { | |||||
const testedMiner = knownMiners[i]; | |||||
const { coinbaseHexFragment } = testedMiner; | |||||
if (testedCoinbaseScript.includes(coinbaseHexFragment)) { | |||||
miner = testedMiner.miner; | |||||
thisCoinbaseFragment = coinbaseHexFragment; | |||||
// Stop looking | |||||
break; | |||||
} | |||||
} | |||||
if (miner === 'unknown') { | |||||
// If you have not found a miner, check by address | |||||
const { outputs } = coinbaseTx; | |||||
for (let i = 0; i < outputs.length; i += 1) { | |||||
const thisOutputScript = outputs[i].outputScript; | |||||
// Check outputScript against ifp outputScript | |||||
const ifpOutputScript = getOutputScriptFromAddress( | |||||
config.ifpAddress, | |||||
); | |||||
if (thisOutputScript !== ifpOutputScript) { | |||||
FabienUnsubmitted Not Done Inline ActionsThis whole IFP match is totally useless. You're losing more time with this branch than it takes to just fail matching the IFP script. Fabien: This whole IFP match is totally useless. You're losing more time with this branch than it takes… | |||||
/* If this output payment was not sent to the IFP address, | |||||
* it was sent to the miner | |||||
* Get miner by payout address | |||||
*/ | |||||
miner = | |||||
module.exports.getMinerFromOutputScript( | |||||
thisOutputScript, | |||||
); | |||||
/* In this case, thisCoinbaseFragment is still undefined | |||||
* Dev note, you may want to get it for parsing some future miner info | |||||
* For now, we have no known miners with "extra" coinbase info but no | |||||
* always-repeated coinbase info | |||||
*/ | |||||
//Stop looking | |||||
break; | |||||
} | |||||
} | |||||
} | |||||
// Some pool miners have additional info available in the coinbase script | |||||
// Parse for this | |||||
const MINERS_WITH_PARSABLE_COINBASE = ['ViaBTC', 'CK Pool']; | |||||
if (MINERS_WITH_PARSABLE_COINBASE.includes(miner)) { | |||||
/* Note: Parsing methods below are customized based on manual review | |||||
* of how each miner produces Coinbase strings | |||||
*/ | |||||
switch (miner) { | |||||
case 'ViaBTC': { | |||||
// Slice the hex string to get what's after ViaBTC | |||||
let infoHex = testedCoinbaseScript.slice( | |||||
testedCoinbaseScript.indexOf(thisCoinbaseFragment) + | |||||
thisCoinbaseFragment.length, | |||||
); | |||||
/* For ViaBTC, the interesting info is between '/' characters | |||||
* i.e. /Mined by 260786/ | |||||
* In ascii, these are encoded with '2f' | |||||
*/ | |||||
// Remove the first preceding '/' | |||||
infoHex = infoHex.slice( | |||||
infoHex.indexOf('2f') + '2f'.length, | |||||
); | |||||
// Remove the last preceding '/' | |||||
infoHex = infoHex.slice(0, infoHex.indexOf('2f')); | |||||
// Convert to ascii | |||||
const infoAscii = Buffer.from(infoHex, 'hex').toString( | |||||
'ascii', | |||||
); | |||||
// Return your improved 'miner' info | |||||
// ViaBTC, Mined by 260786 | |||||
if (infoAscii.length === 0) { | |||||
// If you did not find anything interesting, just return the miner | |||||
return miner; | |||||
} | |||||
return `${miner}, ${infoAscii}`; | |||||
} | |||||
case 'CK Pool': { | |||||
/* For CK Pool, the interesting info is between '/' characters | |||||
* CK Pool seems to be a solo miner identified as "IceBerg" | |||||
* However, some blocks do not have this identifier | |||||
* i.e. /Mined by IceBerg/ | |||||
* In ascii, these are encoded with '2f' | |||||
*/ | |||||
// Slice the hex string to get what's after 'CK Pool' | |||||
let infoHex = testedCoinbaseScript.slice( | |||||
testedCoinbaseScript.indexOf(thisCoinbaseFragment) + | |||||
thisCoinbaseFragment.length, | |||||
); | |||||
/* For 'CK Pool', the interesting info is between '/' characters | |||||
* i.e. /Mined by 260786/ | |||||
* In ascii, these are encoded with '2f' | |||||
*/ | |||||
// Remove the first preceding '/' | |||||
infoHex = infoHex.slice( | |||||
infoHex.indexOf('2f') + '2f'.length, | |||||
); | |||||
// Remove the last preceding '/' | |||||
infoHex = infoHex.slice(0, infoHex.indexOf('2f')); | |||||
// Convert to ascii | |||||
const infoAscii = Buffer.from(infoHex, 'hex').toString( | |||||
'ascii', | |||||
); | |||||
// Return your improved 'miner' info | |||||
// CK Pool, mined by IceBerg | |||||
// If this is IceBerg, identify uniquely | |||||
if (infoAscii === 'mined by IceBerg') { | |||||
return `IceBerg`; | |||||
} | |||||
if (infoAscii.length === 0) { | |||||
// If you did not find anything interesting, just return the miner | |||||
return miner; | |||||
} | |||||
return `${miner}, ${infoAscii}`; | |||||
} | |||||
} | |||||
} else { | |||||
// If no good info is available from Coinbase, return the miner | |||||
return miner; | |||||
} | |||||
}, | |||||
getMinerFromOutputScript: function (outputScript) { | |||||
FabienUnsubmitted Not Done Inline ActionsIf that's not going to be reused anywhere it's not worth making it a function. It makes testing less efficient as you don't supply a list of the know miners Fabien: If that's not going to be reused anywhere it's not worth making it a function. It makes testing… | |||||
/* Input params | |||||
* outputScript - string, the outputScript at the payout output of a coinbase tx | |||||
* | |||||
* Output | |||||
* A known miner if a match is found | |||||
* 'unknown' if no match found * | |||||
*/ | |||||
const knownMiners = config.knownMiners; | const knownMiners = config.knownMiners; | ||||
let miner = 'unknown'; | let miner = 'unknown'; | ||||
// Iterate over known miners to find a match | // Iterate over known miners to find a match | ||||
for (let i = 0; i < knownMiners.length; i += 1) { | for (let i = 0; i < knownMiners.length; i += 1) { | ||||
const testedMiner = knownMiners[i]; | const testedMiner = knownMiners[i]; | ||||
const { coinbaseScript } = testedMiner; | const { payoutOutputScript } = testedMiner; | ||||
if (coinbaseHexString.includes(coinbaseScript)) { | if (outputScript === payoutOutputScript) { | ||||
miner = testedMiner.miner; | miner = testedMiner.miner; | ||||
} | } | ||||
} | } | ||||
return miner; | return miner; | ||||
}, | }, | ||||
parseTx: function (tx) { | parseTx: function (tx) { | ||||
/* Parse an eCash tx as returned by chronik for newsworthy information | /* Parse an eCash tx as returned by chronik for newsworthy information | ||||
* returns | * returns | ||||
▲ Show 20 Lines • Show All 615 Lines • Show Last 20 Lines |
You should pass this as a parameter. That makes the test easier as you can craft any tx/pattern you want and make sure you have coverage for all the detection methods.