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,
});
});
});
});