diff --git a/apps/ecash-herald/config.js b/apps/ecash-herald/config.js index ec814e580..d4a547755 100644 --- a/apps/ecash-herald/config.js +++ b/apps/ecash-herald/config.js @@ -1,74 +1,25 @@ // Copyright (c) 2023 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. 'use strict'; module.exports = { chronik: 'https://chronik.fabien.cash', // URL of chronik instance blockExplorer: 'https://explorer.e.cash', priceApi: { apiBase: 'https://api.coingecko.com/api/v3/simple/price', cryptos: [ { coingeckoSlug: 'ecash', ticker: 'XEC' }, { coingeckoSlug: 'bitcoin', ticker: 'BTC' }, { coingeckoSlug: 'ethereum', ticker: 'ETH' }, ], fiat: 'usd', precision: 8, }, fiatReference: { usd: '$', jpy: '¥', eur: '€', gbp: '£' }, ifpAddress: 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07', tgMsgOptions: { parse_mode: 'HTML', disable_web_page_preview: true, }, - knownMiners: [ - { - coinbaseScript: '566961425443', - miner: 'ViaBTC', - }, - { - coinbaseScript: '4d696e696e672d4475746368', - miner: 'Mining-Dutch', - }, - ], - opReturn: { - opReturnPrefix: '6a', - opReturnAppPrefixLength: '04', - opPushDataOne: '4c', - appPrefixes: { - '00746162': 'Cashtab Msg', - '2e786563': 'Alias', - }, - memo: { - 'prefix': '026d', - 'app': 'memo', - '01': 'Set name', - '02': 'Post memo', - '03': 'Reply to memo', - '04': 'Like / tip memo', - '05': 'Set profile text', - '06': 'Follow user', - '07': 'Unfollow user', - '0a': 'Set profile picture', - '0b': 'Repost memo', - '0c': 'Post topic message', - '0d': 'Topic follow', - '0e': 'Topic unfollow', - '10': 'Create poll', - '13': 'Add poll option', - '14': 'Poll vote', - '16': 'Mute user', - '17': 'Unmute user', - '24': 'Send money', - '30': 'Sell tokens Spec', - '31': 'Token buy offer Spec', - '32': 'Attach token sale signature Spec', - '35': 'Pin token post', - '20': 'Link request', - '21': 'Link accept', - '22': 'Link revoke', - '26': 'Set address alias', - }, - }, }; diff --git a/apps/ecash-herald/constants/miners.js b/apps/ecash-herald/constants/miners.js new file mode 100644 index 000000000..8fe6c1834 --- /dev/null +++ b/apps/ecash-herald/constants/miners.js @@ -0,0 +1,18 @@ +// Copyright (c) 2023 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. +'use strict'; +/** + * miners.js + * Constants related to parsing for known miners of ecash blocks + */ +module.exports = [ + { + coinbaseScript: '566961425443', + miner: 'ViaBTC', + }, + { + coinbaseScript: '4d696e696e672d4475746368', + miner: 'Mining-Dutch', + }, +]; diff --git a/apps/ecash-herald/constants/op_return.js b/apps/ecash-herald/constants/op_return.js new file mode 100644 index 000000000..e1e3fa73b --- /dev/null +++ b/apps/ecash-herald/constants/op_return.js @@ -0,0 +1,48 @@ +// Copyright (c) 2023 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. +'use strict'; +/** + * op_return.js + * Constants related to OP_RETURN script + * https://en.bitcoin.it/wiki/Script + */ +module.exports = { + opReturnPrefix: '6a', + opReturnAppPrefixLength: '04', + opPushDataOne: '4c', + appPrefixes: { + '00746162': 'Cashtab Msg', + '2e786563': 'Alias', + }, + memo: { + 'prefix': '026d', + 'app': 'memo', + '01': 'Set name', + '02': 'Post memo', + '03': 'Reply to memo', + '04': 'Like / tip memo', + '05': 'Set profile text', + '06': 'Follow user', + '07': 'Unfollow user', + '0a': 'Set profile picture', + '0b': 'Repost memo', + '0c': 'Post topic message', + '0d': 'Topic follow', + '0e': 'Topic unfollow', + '10': 'Create poll', + '13': 'Add poll option', + '14': 'Poll vote', + '16': 'Mute user', + '17': 'Unmute user', + '24': 'Send money', + '30': 'Sell tokens Spec', + '31': 'Token buy offer Spec', + '32': 'Attach token sale signature Spec', + '35': 'Pin token post', + '20': 'Link request', + '21': 'Link accept', + '22': 'Link revoke', + '26': 'Set address alias', + }, +}; diff --git a/apps/ecash-herald/src/parse.js b/apps/ecash-herald/src/parse.js index 7be5c0bb1..3fca788ea 100644 --- a/apps/ecash-herald/src/parse.js +++ b/apps/ecash-herald/src/parse.js @@ -1,671 +1,667 @@ // Copyright (c) 2023 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. 'use strict'; const config = require('../config'); +const opReturn = require('../constants/op_return'); +const knownMiners = require('../constants/miners'); const cashaddr = require('ecashaddrjs'); const BigNumber = require('bignumber.js'); const { prepareStringForTelegramHTML, splitOverflowTgMsg, } = require('./telegram'); const { formatPrice, returnAddressPreview } = require('./utils'); module.exports = { parseBlock: function (chronikBlockResponse) { const { blockInfo, txs } = chronikBlockResponse; const { hash } = blockInfo; const { height, numTxs } = blockInfo; // Parse coinbase string const coinbaseScript = txs[0].inputs[0].inputScript; const miner = module.exports.getMinerFromCoinbase(coinbaseScript); // Start with i=1 to skip Coinbase tx const parsedTxs = []; for (let i = 1; i < txs.length; i += 1) { parsedTxs.push(module.exports.parseTx(txs[i])); } // Collect token info needed to parse token send txs const tokenIds = new Set(); // we only need each tokenId once for (let i = 0; i < parsedTxs.length; i += 1) { const thisParsedTx = parsedTxs[i]; if (thisParsedTx.tokenSendInfo) { tokenIds.add(thisParsedTx.tokenSendInfo.tokenId); } } return { hash, height, miner, numTxs, parsedTxs, tokenIds }; }, getMinerFromCoinbase: function (coinbaseHexString) { - const knownMiners = config.knownMiners; let miner = 'unknown'; // Iterate over known miners to find a match for (let i = 0; i < knownMiners.length; i += 1) { const testedMiner = knownMiners[i]; const { coinbaseScript } = testedMiner; if (coinbaseHexString.includes(coinbaseScript)) { miner = testedMiner.miner; } } return miner; }, parseTx: function (tx) { /* Parse an eCash tx as returned by chronik for newsworthy information * returns * { txid, genesisInfo, opReturnInfo } */ const { txid, inputs, outputs, size } = tx; let isTokenTx = false; let genesisInfo = false; let opReturnInfo = false; /* Token send parsing info * * Note that token send amounts received from chronik do not account for * token decimals. Decimalized amounts require token genesisInfo * decimals param to calculate */ /* tokenSendInfo * `false` for txs that are not etoken send txs * an object containing info about the token send for token send txs */ let tokenSendInfo = false; let tokenSendingOutputScripts = new Set(); let tokenReceivingOutputs = new Map(); let tokenChangeOutputs = new Map(); let undecimalizedTokenInputAmount = new BigNumber(0); /* Collect xecSendInfo for all txs, since all txs are XEC sends * You may later want to render xecSendInfo for tokenSends, appTxs, etc, * maybe on special conditions, e.g.a token send tx that also sends a bunch of xec */ // xecSend parsing variables let xecSendingOutputScripts = new Set(); let xecReceivingOutputs = new Map(); let xecChangeOutputs = new Map(); let xecInputAmountSats = 0; let xecOutputAmountSats = 0; if (tx.slpTxData !== null && typeof tx.slpTxData !== 'undefined') { isTokenTx = true; // Determine if this is an etoken genesis tx if ( tx.slpTxData.slpMeta !== null && typeof tx.slpTxData.slpMeta !== 'undefined' && tx.slpTxData.genesisInfo !== null && typeof tx.slpTxData.genesisInfo !== 'undefined' && tx.slpTxData.slpMeta.txType === 'GENESIS' ) { genesisInfo = tx.slpTxData.genesisInfo; } // Determine if this is an etoken send tx if ( tx.slpTxData.slpMeta !== null && typeof tx.slpTxData.slpMeta !== 'undefined' && tx.slpTxData.slpMeta.txType === 'SEND' ) { // Initialize tokenSendInfo as an object with the sent tokenId tokenSendInfo = { tokenId: tx.slpTxData.slpMeta.tokenId }; } } for (let i in inputs) { const thisInput = inputs[i]; xecSendingOutputScripts.add(thisInput.outputScript); xecInputAmountSats += parseInt(thisInput.value); // The input that sent the token utxos will have key 'slpToken' if (typeof thisInput.slpToken !== 'undefined') { // Add amount to undecimalizedTokenInputAmount undecimalizedTokenInputAmount = undecimalizedTokenInputAmount.plus( thisInput.slpToken.amount, ); // Collect the input outputScripts to identify change output tokenSendingOutputScripts.add(thisInput.outputScript); } } // Iterate over outputs to check for OP_RETURN msgs for (let i = 0; i < outputs.length; i += 1) { const thisOutput = outputs[i]; const value = parseInt(thisOutput.value); xecOutputAmountSats += value; // If this output script is the same as one of the sendingOutputScripts if (xecSendingOutputScripts.has(thisOutput.outputScript)) { // Then this XEC amount is change // Add outputScript and value to map // If this outputScript is already in xecChangeOutputs, increment its value xecChangeOutputs.set( thisOutput.outputScript, (xecChangeOutputs.get(thisOutput.outputScript) ?? 0) + value, ); } else { // Add an xecReceivingOutput // Add outputScript and value to map // If this outputScript is already in xecReceivingOutputs, increment its value xecReceivingOutputs.set( thisOutput.outputScript, (xecReceivingOutputs.get(thisOutput.outputScript) ?? 0) + value, ); } // Don't parse OP_RETURN values of etoken txs, this info is available from chronik if ( - thisOutput.outputScript.startsWith( - config.opReturn.opReturnPrefix, - ) && + thisOutput.outputScript.startsWith(opReturn.opReturnPrefix) && !isTokenTx ) { opReturnInfo = module.exports.parseOpReturn( thisOutput.outputScript, ); } // For etoken send txs, parse outputs for tokenSendInfo object if (typeof thisOutput.slpToken !== 'undefined') { // Check output script to confirm does not match tokenSendingOutputScript if (tokenSendingOutputScripts.has(thisOutput.outputScript)) { // change tokenChangeOutputs.set( thisOutput.outputScript, ( tokenChangeOutputs.get(thisOutput.outputScript) ?? new BigNumber(0) ).plus(thisOutput.slpToken.amount), ); } else { /* This is the sent token qty * * Add outputScript and undecimalizedTokenReceivedAmount to map * If this outputScript is already in tokenReceivingOutputs, increment undecimalizedTokenReceivedAmount * note that thisOutput.slpToken.amount is a string so you do not want to add it * BigNumber library is required for token calculations */ tokenReceivingOutputs.set( thisOutput.outputScript, ( tokenReceivingOutputs.get( thisOutput.outputScript, ) ?? new BigNumber(0) ).plus(thisOutput.slpToken.amount), ); } } } // Determine tx fee const txFee = xecInputAmountSats - xecOutputAmountSats; const satsPerByte = txFee / size; // If this is a token send tx, return token send parsing info and not 'false' for tokenSendInfo if (tokenSendInfo) { tokenSendInfo.tokenChangeOutputs = tokenChangeOutputs; tokenSendInfo.tokenReceivingOutputs = tokenReceivingOutputs; tokenSendInfo.tokenSendingOutputScripts = tokenSendingOutputScripts; } return { txid, genesisInfo, opReturnInfo, satsPerByte, xecSendingOutputScripts, xecChangeOutputs, xecReceivingOutputs, tokenSendInfo, }; }, parseOpReturn: function (outputScript) { // Initialize required vars let app; let msg; // Determine if this is an OP_RETURN field - const isOpReturn = - outputScript.slice(0, 2) === config.opReturn.opReturnPrefix; + const isOpReturn = outputScript.slice(0, 2) === opReturn.opReturnPrefix; if (!isOpReturn) { return false; } // Determine if this is a memo tx // Memo txs have a shorter prefix and require special processing - const isMemoTx = - outputScript.slice(2, 6) === config.opReturn.memo.prefix; + const isMemoTx = outputScript.slice(2, 6) === opReturn.memo.prefix; if (isMemoTx) { // memo txs require special processing // Send the unprocessed remainder of the string to a specialized function return module.exports.parseMemoOutputScript(outputScript); } // Parse for app prefix // See https://github.com/Bitcoin-ABC/bitcoin-abc/blob/master/web/standards/op_return-prefix-guideline.md const hasAppPrefix = - outputScript.slice(2, 4) === - config.opReturn.opReturnAppPrefixLength; + outputScript.slice(2, 4) === opReturn.opReturnAppPrefixLength; if (hasAppPrefix) { const appPrefix = outputScript.slice(4, 12); - if (Object.keys(config.opReturn.appPrefixes).includes(appPrefix)) { - app = config.opReturn.appPrefixes[appPrefix]; + if (Object.keys(opReturn.appPrefixes).includes(appPrefix)) { + app = opReturn.appPrefixes[appPrefix]; } else { app = 'unknown app'; } switch (app) { case 'Cashtab Msg': // For a Cashtab msg, the rest of the string will be parsed as an OP_RETURN msg msg = module.exports.hexOpReturnToUtf8( outputScript.slice(12), ); break; case 'Alias': // For an Alias Registration, the rest of the string will be parsed as an OP_RETURN msg msg = module.exports.hexOpReturnToUtf8( outputScript.slice(12), ); break; case 'unknown': // Parse the whole string less the 6a prefix, so we can see the unknown app prefix msg = module.exports.hexOpReturnToUtf8( outputScript.slice(2), ); break; default: // Parse the whole string less the 6a prefix, so we can see the unknown app prefix msg = module.exports.hexOpReturnToUtf8( outputScript.slice(2), ); } } else { app = 'no app'; msg = module.exports.hexOpReturnToUtf8(outputScript.slice(2)); } return { app, msg }; }, hexOpReturnToUtf8: function (hexStr) { /* * Accept as input an OP_RETURN hex string less the 6a prefix * String will have the form of 4c+bytelength+msg (? + 4c + bytelength + msg) */ let hexStrLength = hexStr.length; let opReturnMsgArray = []; for (let i = 0; hexStrLength !== 0; i++) { // Check first byte for the message length or 4c + message length let byteValue = hexStr.slice(0, 2); let msgByteSize = 0; - if (byteValue === config.opReturn.opPushDataOne) { + if (byteValue === opReturn.opPushDataOne) { // If this byte is 4c, then the next byte is the message byte size. // Retrieve the message byte size and convert from hex to decimal msgByteSize = parseInt(hexStr.substring(2, 4), 16); // Remove 4c + message byte size info from the beginning of hexStr hexStr = hexStr.slice(4); } else { // This byte is the length of an upcoming msg msgByteSize = parseInt(hexStr.substring(0, 2), 16); // Remove message byte size info from the beginning of hexStr hexStr = hexStr.slice(2); } // Use msgByteSize to parse the message const msgCharLength = 2 * msgByteSize; const message = hexStr.slice(0, msgCharLength); // Add to opReturnMsgArray opReturnMsgArray.push(Buffer.from(message, 'hex').toString('utf8')); // strip out the parsed message hexStr = hexStr.slice(msgCharLength); hexStrLength = hexStr.length; // Sometimes OP_RETURN will have a series of msgs // Return to beginning of loop with i=0 if there hexStr still has remaining unparsed characters } // If there are multiple messages, for example an unknown prefix, signify this with the | character return opReturnMsgArray.join('|'); }, parseMemoOutputScript: function (memoHexStr) { // Remove the memo prefix, already processed memoHexStr = memoHexStr.slice(6); // At the beginning of this function, we have already popped off '0d6d' - let app = config.opReturn.memo.app; + let app = opReturn.memo.app; let msg = ''; for (let i = 0; memoHexStr !== 0; i++) { // Get the memo action code // See https://memo.cash/protocol const actionCode = memoHexStr.slice(0, 2); // Remove actionCode from memoHexStr memoHexStr = memoHexStr.slice(2); switch (actionCode) { case '01' || '02' || '05' || '0a' || '0c' || '0d' || '0e': // Action codes where the entire string may be parsed to utf8 msg += `${ - config.opReturn.memo[actionCode] + opReturn.memo[actionCode] }|${module.exports.hexOpReturnToUtf8(memoHexStr)}`; break; default: // parse the rest of the string like a normal op_return utf8 string msg += `${ - Object.keys(config.opReturn.memo).includes(actionCode) - ? config.opReturn.memo[actionCode] + Object.keys(opReturn.memo).includes(actionCode) + ? opReturn.memo[actionCode] : '' }|${module.exports.hexOpReturnToUtf8(memoHexStr)}`; } return { app, msg }; } }, getBlockTgMessage: function (parsedBlock, coingeckoPrices, tokenInfoMap) { const { hash, height, miner, numTxs, parsedTxs } = parsedBlock; // Define newsworthy types of txs in parsedTxs // These arrays will be used to present txs in batches by type const genesisTxTgMsgLines = []; const tokenSendTxTgMsgLines = []; const opReturnTxTgMsgLines = []; const xecSendTxTgMsgLines = []; // Iterate over parsedTxs to find anything newsworthy for (let i = 0; i < parsedTxs.length; i += 1) { const thisParsedTx = parsedTxs[i]; const { txid, genesisInfo, opReturnInfo, satsPerByte, xecSendingOutputScripts, xecChangeOutputs, xecReceivingOutputs, tokenSendInfo, } = thisParsedTx; if (genesisInfo) { // The txid of a genesis tx is the tokenId const tokenId = txid; let { tokenTicker, tokenName, tokenDocumentUrl } = genesisInfo; // Make sure tokenName does not contain telegram html escape characters tokenName = prepareStringForTelegramHTML(tokenName); // Make sure tokenName does not contain telegram html escape characters tokenTicker = prepareStringForTelegramHTML(tokenTicker); // Do not apply this parsing to tokenDocumentUrl, as this could change the URL // If this breaks the msg, so be it // Would only happen for bad URLs genesisTxTgMsgLines.push( `${tokenName} (${tokenTicker}) [doc]`, ); // This parsed tx has a tg msg line. Move on to the next one. continue; } if (opReturnInfo) { let { app, msg } = opReturnInfo; // Make sure the OP_RETURN msg does not contain telegram html escape characters msg = prepareStringForTelegramHTML(msg); opReturnTxTgMsgLines.push( `${app}: ${msg}`, ); // This parsed tx has a tg msg line. Move on to the next one. continue; } // Only parse tokenSendInfo txs if you successfuly got tokenMapInfo from chronik if (tokenSendInfo && tokenInfoMap) { let { tokenId, tokenSendingOutputScripts, tokenChangeOutputs, tokenReceivingOutputs, } = tokenSendInfo; // Get token info from tokenInfoMap const thisTokenInfo = tokenInfoMap.get(tokenId); let { tokenTicker, tokenName, decimals } = thisTokenInfo; // Note: tokenDocumentUrl and tokenDocumentHash are also available from thisTokenInfo // Make sure tokenName does not contain telegram html escape characters tokenName = prepareStringForTelegramHTML(tokenName); // Make sure tokenName does not contain telegram html escape characters tokenTicker = prepareStringForTelegramHTML(tokenTicker); // Initialize tokenSendMsg let tokenSendMsg; // Parse token self-send txs if (tokenReceivingOutputs.size === 0) { // self send tx let undecimalizedTokenChangeAmount = new BigNumber(0); for (const tokenChangeAmount of tokenChangeOutputs.values()) { undecimalizedTokenChangeAmount = undecimalizedTokenChangeAmount.plus( tokenChangeAmount, ); } // Calculate true tokenChangeAmount using decimals // Use decimals to calculate the sent amount as string const decimalizedTokenChangeAmount = new BigNumber( undecimalizedTokenChangeAmount, ) .shiftedBy(-1 * decimals) .toString(); // Self send tokenSendMsg tokenSendMsg = `${tokenSendingOutputScripts.size} ${ tokenSendingOutputScripts.size > 1 ? 'addresses' : 'address' } sent ${decimalizedTokenChangeAmount} ${tokenTicker} to ${ tokenSendingOutputScripts.size > 1 ? 'themselves' : 'itself' }`; } else { // Normal token send tx let undecimalizedTokenReceivedAmount = new BigNumber(0); for (const tokenReceivedAmount of tokenReceivingOutputs.values()) { undecimalizedTokenReceivedAmount = undecimalizedTokenReceivedAmount.plus( tokenReceivedAmount, ); } // Calculate true tokenReceivedAmount using decimals // Use decimals to calculate the received amount as string const decimalizedTokenReceivedAmount = new BigNumber( undecimalizedTokenReceivedAmount, ) .shiftedBy(-1 * decimals) .toString(); tokenSendMsg = `${returnAddressPreview( cashaddr.encodeOutputScript( tokenSendingOutputScripts.values().next().value, ), )} sent ${decimalizedTokenReceivedAmount.toLocaleString( 'en-US', { minimumFractionDigits: decimals, }, )} ${tokenTicker} to ${returnAddressPreview( cashaddr.encodeOutputScript( tokenReceivingOutputs.keys().next().value, ), )}${ tokenReceivingOutputs.size > 1 ? ` and ${tokenReceivingOutputs.size - 1} others` : '' }`; } tokenSendTxTgMsgLines.push(tokenSendMsg); // This parsed tx has a tg msg line. Move on to the next one. continue; } // Txs not parsed above are parsed as xec send txs /* We do the totalSatsSent calculation here in getBlockTgMsg and not above in parseTx * as it is only necessary to do for rendered txs */ let totalSatsSent = 0; for (const satoshis of xecReceivingOutputs.values()) { totalSatsSent += satoshis; } // Convert sats to XEC. Round as decimals will not be rendered in msgs. const totalXecSent = parseFloat((totalSatsSent / 100).toFixed(0)); // Clone xecReceivingOutputs so that you don't modify unit test mocks let xecReceivingAddressOutputs = new Map(xecReceivingOutputs); // Throw out OP_RETURN outputs for txs parsed as XEC send txs xecReceivingAddressOutputs.forEach((value, key, map) => { - if (key.startsWith(config.opReturn.opReturnPrefix)) { + if (key.startsWith(opReturn.opReturnPrefix)) { map.delete(key); } }); let xecSendMsg; if (xecReceivingAddressOutputs.size === 0) { // self send tx let changeAmountSats = 0; for (const satoshis of xecChangeOutputs.values()) { changeAmountSats += satoshis; } // Convert sats to XEC. const changeAmountXec = parseFloat(changeAmountSats / 100); xecSendMsg = `${xecSendingOutputScripts.size} ${ xecSendingOutputScripts.size > 1 ? 'addresses' : 'address' } sent ${changeAmountXec} XEC to ${ xecSendingOutputScripts.size > 1 ? 'themselves' : 'itself' }`; } else { xecSendMsg = `${returnAddressPreview( cashaddr.encodeOutputScript( xecSendingOutputScripts.values().next().value, ), )} sent ${totalXecSent.toLocaleString('en-US', { minimumFractionDigits: 0, })} XEC to ${ xecReceivingAddressOutputs.keys().next().value === xecSendingOutputScripts.values().next().value ? 'itself' : returnAddressPreview( cashaddr.encodeOutputScript( xecReceivingAddressOutputs.keys().next() .value, ), ) }${ xecReceivingAddressOutputs.size > 1 ? ` and ${xecReceivingAddressOutputs.size - 1} others` : '' } | ${satsPerByte.toFixed(2)} sats per byte`; } xecSendTxTgMsgLines.push(xecSendMsg); } // Build up message as an array, with each line as an entry let tgMsg = []; // Header // | | tgMsg.push( `${height} | ${numTxs} tx${ numTxs > 1 ? `s` : '' } | ${miner}`, ); // Display prices as set in config.js if (coingeckoPrices) { // Iterate over prices and add a line for each price in the object for (let i = 0; i < coingeckoPrices.length; i += 1) { const { fiat, ticker, price } = coingeckoPrices[i]; const thisFormattedPrice = formatPrice(price, fiat); tgMsg.push(`1 ${ticker} = ${thisFormattedPrice}`); } } // Genesis txs if (genesisTxTgMsgLines.length > 0) { // Line break for new section tgMsg.push(''); // 1 new eToken created: // or // new eTokens created: tgMsg.push( `${genesisTxTgMsgLines.length} new eToken${ genesisTxTgMsgLines.length > 1 ? `s` : '' } created:`, ); tgMsg = tgMsg.concat(genesisTxTgMsgLines); } // eToken Send txs if (tokenSendTxTgMsgLines.length > 0) { // Line break for new section tgMsg.push(''); // 1 eToken send tx: // or // eToken send txs: tgMsg.push( `${tokenSendTxTgMsgLines.length} eToken send tx${ tokenSendTxTgMsgLines.length > 1 ? `s` : '' }`, ); tgMsg = tgMsg.concat(tokenSendTxTgMsgLines); } // OP_RETURN txs if (opReturnTxTgMsgLines.length > 0) { // Line break for new section tgMsg.push(''); // App txs: // or // App tx: tgMsg.push(`App tx${opReturnTxTgMsgLines.length > 1 ? `s` : ''}:`); // : // alias: newlyregisteredalias // Cashtab Msg: This is a Cashtab Msg tgMsg = tgMsg.concat(opReturnTxTgMsgLines); } // XEC txs if (xecSendTxTgMsgLines.length > 0) { // Line break for new section tgMsg.push(''); // App txs: // or // App tx: tgMsg.push( `${xecSendTxTgMsgLines.length} eCash tx${ xecSendTxTgMsgLines.length > 1 ? `s` : '' }:`, ); tgMsg = tgMsg.concat(xecSendTxTgMsgLines); } return splitOverflowTgMsg(tgMsg); }, }; diff --git a/apps/ecash-herald/test/mocks/memo.js b/apps/ecash-herald/test/mocks/memo.js index 3695a0749..b20808319 100644 --- a/apps/ecash-herald/test/mocks/memo.js +++ b/apps/ecash-herald/test/mocks/memo.js @@ -1,31 +1,31 @@ // Copyright (c) 2023 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. 'use strict'; -const config = require('../../config'); +const opReturn = require('../../constants/op_return'); module.exports = [ // Set name { txid: '753e29e81cdea12dc5fa30ca89049ca7d538d4062c4bb1b19ecf2a209a3ac8d9', - action: config.opReturn.memo['01'], + action: opReturn.memo['01'], outputScript: '6a026d0106746573742032', - parsed: `${config.opReturn.memo['01']}|test 2`, + parsed: `${opReturn.memo['01']}|test 2`, }, // Post memo { txid: 'c7e91099923a28cf86685c9683c74c8c029c8965a5039f84ad79886b42720f9b', - action: config.opReturn.memo['02'], + action: opReturn.memo['02'], outputScript: '6a026d02374c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e73656374657475722061646970697363696e6720656c6974', - parsed: `${config.opReturn.memo['02']}|Lorem ipsum dolor sit amet, consectetur adipiscing elit`, + parsed: `${opReturn.memo['02']}|Lorem ipsum dolor sit amet, consectetur adipiscing elit`, }, // Reply to memo, parsing not yet fully supported { txid: '28f3ec1f134dc8ea2e37a0645774fa2aa19e0bc2871b6edcc7e99cd86d77b1b6', - action: config.opReturn.memo['03'], + action: opReturn.memo['03'], outputScript: '6a026d0320965689bc694d816ab0745b501c0e9dc8dbe7994a185fe37a37b808dc6b05750a4c8546726f6d20776861742049276d20676174686572696e672c206974207365656d73207468617420746865206d656469612077656e742066726f6d207175657374696f6e696e6720617574686f7269747920746f20646f696e672074686569722062696464696e67206173206120636f6c6c656374697665204e504320686976656d696e6421', - parsed: `${config.opReturn.memo['03']}|�V��iM�j�t[P\u001c\u000e����J\u0018_�z7�\b�k\u0005u\n|From what I'm gathering, it seems that the media went from questioning authority to doing their bidding as a collective NPC hivemind!`, + parsed: `${opReturn.memo['03']}|�V��iM�j�t[P\u001c\u000e����J\u0018_�z7�\b�k\u0005u\n|From what I'm gathering, it seems that the media went from questioning authority to doing their bidding as a collective NPC hivemind!`, }, ]; diff --git a/apps/ecash-herald/test/parseTests.js b/apps/ecash-herald/test/parseTests.js index c1037783b..416ff0db4 100644 --- a/apps/ecash-herald/test/parseTests.js +++ b/apps/ecash-herald/test/parseTests.js @@ -1,47 +1,47 @@ // Copyright (c) 2023 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. 'use strict'; const assert = require('assert'); -const config = require('../config'); +const opReturn = require('../constants/op_return'); const unrevivedBlocks = require('./mocks/blocks'); const { jsonReviver } = require('../src/utils'); const blocks = JSON.parse(JSON.stringify(unrevivedBlocks), jsonReviver); const memoOutputScripts = require('./mocks/memo'); const { parseBlock, parseMemoOutputScript, getBlockTgMessage, } = require('../src/parse'); describe('parse.js functions', function () { it('All test blocks', function () { for (let i = 0; i < blocks.length; i += 1) { const thisBlock = blocks[i]; const { blockDetails, parsedBlock, coingeckoPrices, tokenInfoMap, blockSummaryTgMsgs, } = thisBlock; assert.deepEqual(parseBlock(blockDetails), parsedBlock); assert.deepEqual( getBlockTgMessage(parsedBlock, coingeckoPrices, tokenInfoMap), blockSummaryTgMsgs, ); } }); it(`parseMemoOutputScript correctly parses all tested memo actions in memo.js`, function () { memoOutputScripts.map(memoTestObj => { - const app = config.opReturn.memo.app; + const app = opReturn.memo.app; const { outputScript, parsed } = memoTestObj; assert.deepEqual(parseMemoOutputScript(outputScript), { app, msg: parsed, }); }); }); });