Page MenuHomePhabricator

No OneTemporary

diff --git a/apps/ecash-herald/src/parse.js b/apps/ecash-herald/src/parse.js
index c76c8446e..1b29fec17 100644
--- a/apps/ecash-herald/src/parse.js
+++ b/apps/ecash-herald/src/parse.js
@@ -1,1653 +1,1652 @@
// 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 { consumeNextPush } = require('ecash-script');
const knownMinersJson = require('../constants/miners');
const { jsonReviver, bigNumberAmountToLocaleString } = require('../src/utils');
const miners = JSON.parse(JSON.stringify(knownMinersJson), jsonReviver);
const cashaddr = require('ecashaddrjs');
const BigNumber = require('bignumber.js');
const {
prepareStringForTelegramHTML,
splitOverflowTgMsg,
} = require('./telegram');
const {
formatPrice,
satsToFormattedValue,
returnAddressPreview,
} = require('./utils');
module.exports = {
parseBlock: function (chronikBlockResponse) {
const { blockInfo, txs } = chronikBlockResponse;
const { hash } = blockInfo;
const { height, numTxs } = blockInfo;
// Parse coinbase string
const coinbaseTx = txs[0];
const miner = module.exports.getMinerFromCoinbaseTx(coinbaseTx, miners);
// Start with i=1 to skip Coinbase tx
let parsedTxs = [];
for (let i = 1; i < txs.length; i += 1) {
parsedTxs.push(module.exports.parseTx(txs[i]));
}
// Sort parsedTxs by totalSatsSent, highest to lowest
parsedTxs = parsedTxs.sort((a, b) => {
return b.totalSatsSent - a.totalSatsSent;
});
// Collect token info needed to parse token send txs
const tokenIds = new Set(); // we only need each tokenId once
// Collect outputScripts seen in this block to parse for balance
let outputScripts = new Set();
for (let i = 0; i < parsedTxs.length; i += 1) {
const thisParsedTx = parsedTxs[i];
if (thisParsedTx.tokenSendInfo) {
tokenIds.add(thisParsedTx.tokenSendInfo.tokenId);
}
// Some OP_RETURN txs also have token IDs we need to parse
// SWaP txs, (TODO: airdrop txs)
if (
thisParsedTx.opReturnInfo &&
thisParsedTx.opReturnInfo.tokenId
) {
tokenIds.add(thisParsedTx.opReturnInfo.tokenId);
}
const { xecSendingOutputScripts, xecReceivingOutputs } =
thisParsedTx;
// Only add the first sending and receiving output script,
// As you will only render balance emojis for these
outputScripts.add(xecSendingOutputScripts.values().next().value);
// For receiving outputScripts, add the first that is not OP_RETURN
// So, get an array of the outputScripts first
const xecReceivingOutputScriptsArray = Array.from(
xecReceivingOutputs.keys(),
);
for (let j = 0; j < xecReceivingOutputScriptsArray.length; j += 1) {
if (
!xecReceivingOutputScriptsArray[j].startsWith(
opReturn.opReturnPrefix,
)
) {
outputScripts.add(xecReceivingOutputScriptsArray[j]);
// Exit loop after you've added the first non-OP_RETURN outputScript
break;
}
}
}
return {
hash,
height,
miner,
numTxs,
parsedTxs,
tokenIds,
outputScripts,
};
},
getMinerFromCoinbaseTx: function (coinbaseTx, knownMiners) {
// get coinbase inputScript
const testedCoinbaseScript = coinbaseTx.inputs[0].inputScript;
// When you find the miner, minerInfo will come from knownMiners
let minerInfo = false;
// First, check outputScripts for a known miner
const { outputs } = coinbaseTx;
for (let i = 0; i < outputs.length; i += 1) {
const thisOutputScript = outputs[i].outputScript;
if (knownMiners.has(thisOutputScript)) {
minerInfo = knownMiners.get(thisOutputScript);
break;
}
}
if (!minerInfo) {
// If you still haven't found minerInfo, test by known pattern of coinbase script
// Possibly a known miner is using a new address
knownMiners.forEach(knownMinerInfo => {
const { coinbaseHexFragment } = knownMinerInfo;
if (testedCoinbaseScript.includes(coinbaseHexFragment)) {
minerInfo = knownMinerInfo;
}
});
}
// At this point, if you haven't found the miner, you won't
if (!minerInfo) {
return 'unknown';
}
// If you have found the miner, parse coinbase hex for additional info
switch (minerInfo.miner) {
// This is available for ViaBTC and CK Pool
// Use a switch statement to easily support adding future miners
case 'ViaBTC':
// Intentional fall-through so ViaBTC and CKPool have same parsing
// es-lint ignore no-fallthrough
case 'CK Pool': {
/* For ViaBTC, the interesting info is between '/' characters
* i.e. /Mined by 260786/
* In ascii, these are encoded with '2f'
*/
const infoHexParts = testedCoinbaseScript.split('2f');
// Because the characters before and after the info we are looking for could also
// contain '2f', we need to find the right part
// The right part is the one that comes immediately after coinbaseHexFragment
let infoAscii = '';
for (let i = 0; i < infoHexParts.length; i += 1) {
if (
infoHexParts[i].includes(minerInfo.coinbaseHexFragment)
) {
// We want the next one, if it exists
if (i + 1 < infoHexParts.length) {
infoAscii = Buffer.from(
infoHexParts[i + 1],
'hex',
).toString('ascii');
}
break;
}
}
if (infoAscii === 'mined by IceBerg') {
// CK Pool, mined by IceBerg
// If this is IceBerg, identify uniquely
// Iceberg is probably a solo miner using CK Pool software
return `IceBerg`;
}
// 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 minerInfo.miner;
}
return `${minerInfo.miner}, ${infoAscii}`;
}
default: {
// Unless the miner has specific parsing rules defined above, no additional info is available
return minerInfo.miner;
}
}
},
parseTx: function (tx) {
/* Parse an eCash tx as returned by chronik for newsworthy information
* returns
* { txid, genesisInfo, opReturnInfo }
*/
const { txid, inputs, outputs } = 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);
// tokenBurn parsing variables
let tokenBurnInfo = false;
let undecimalizedTokenBurnAmount = 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 xecInputAmountSats = 0;
let xecOutputAmountSats = 0;
let totalSatsSent = 0;
let changeAmountSats = 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);
}
if (typeof thisInput.slpBurn !== 'undefined') {
undecimalizedTokenBurnAmount =
undecimalizedTokenBurnAmount.plus(
new BigNumber(thisInput.slpBurn.token.amount),
);
}
}
// 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
changeAmountSats += 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,
);
// Increment totalSatsSent
totalSatsSent += value;
}
// Don't parse OP_RETURN values of etoken txs, this info is available from chronik
if (
thisOutput.outputScript.startsWith(opReturn.opReturnPrefix) &&
!isTokenTx
) {
opReturnInfo = module.exports.parseOpReturn(
thisOutput.outputScript.slice(2),
);
}
// 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;
// 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;
}
// If this is a token burn tx, return token burn parsing info and not 'false' for tokenBurnInfo
// Check to make sure undecimalizedTokenBurnAmount is not zero
// Some txs e.g. ff314cae5d5daeabe44225f855bd54c7d07737b8458a8e8a49fc581025ee0a57 give
// an slpBurn field in chronik, even though not even a token tx
if (undecimalizedTokenBurnAmount.gt(0)) {
tokenBurnInfo = {
undecimalizedTokenBurnAmount:
undecimalizedTokenBurnAmount.toString(),
};
}
// If this tx sent XEC to itself, reassign changeAmountSats to totalSatsSent
// Need to do this to prevent self-send txs being sorted at the bottom of msgs
if (xecReceivingOutputs.size === 0) {
totalSatsSent = changeAmountSats;
}
return {
txid,
genesisInfo,
opReturnInfo,
txFee,
xecSendingOutputScripts,
xecReceivingOutputs,
totalSatsSent,
tokenSendInfo,
tokenBurnInfo,
};
},
/**
*
* @param {string} opReturnHex an OP_RETURN outputScript with '6a' removed
* @returns {object} {app, msg} an object with app and msg params used to generate msg
*/
parseOpReturn: function (opReturnHex) {
// Initialize required vars
let app;
let msg;
let tokenId = false;
// Get array of pushes
let stack = { remainingHex: opReturnHex };
let stackArray = [];
while (stack.remainingHex.length > 0) {
const thisPush = consumeNextPush(stack);
if (thisPush !== '') {
// You may have an empty push in the middle of a complicated tx for some reason
// Mb some libraries erroneously create these
// e.g. https://explorer.e.cash/tx/70c2842e1b2c7eb49ee69cdecf2d6f3cd783c307c4cbeef80f176159c5891484
// has 4c000100 for last characters. 4c00 is just nothing.
// But you want to know 00 and have the correct array index
stackArray.push(thisPush);
}
}
// Get the protocolIdentifier, the first push
const protocolIdentifier = stackArray[0];
// Test for memo
// Memo prefixes are special in that they are two bytes instead of the usual four
// Also, memo has many prefixes, in that the action is also encoded in these two bytes
if (
protocolIdentifier.startsWith(opReturn.memo.prefix) &&
protocolIdentifier.length === 4
) {
// If the protocol identifier is two bytes long (4 characters), parse for memo tx
// For now, send the same info to this function that it currently parses
// TODO parseMemoOutputScript needs to be refactored to use ecash-script
return module.exports.parseMemoOutputScript(stackArray);
}
// Test for other known apps with known msg processing methods
switch (protocolIdentifier) {
case opReturn.opReserved: {
// Parse for empp OP_RETURN
// Spec https://github.com/Bitcoin-ABC/bitcoin-abc/blob/master/chronik/bitcoinsuite-slp/src/empp/mod.rs
return module.exports.parseMultipushStack(stackArray);
}
case opReturn.knownApps.alias.prefix: {
app = opReturn.knownApps.alias.app;
/*
For now, parse and render alias txs by going through OP_RETURN
When aliases are live, refactor to use alias-server for validation
<protocolIdentifier> <version> <alias> <address type + hash>
Only parse the msg if the tx is constructed correctly
*/
msg =
stackArray.length === 4 && stackArray[1] === '00'
? prepareStringForTelegramHTML(
Buffer.from(stackArray[2], 'hex').toString(
'utf8',
),
)
: 'Invalid alias registration';
break;
}
case opReturn.knownApps.airdrop.prefix: {
app = opReturn.knownApps.airdrop.app;
// Initialize msg as empty string. Need tokenId info to complete.
msg = '';
// Airdrop tx has structure
// <prefix> <tokenId>
// Cashtab allows sending a cashtab msg with an airdrop
// These look like
// <prefix> <tokenId> <cashtabMsgPrefix> <msg>
if (stackArray.length >= 2 && stackArray[1].length === 64) {
tokenId = stackArray[1];
}
break;
}
case opReturn.knownApps.cashtabMsg.prefix: {
app = opReturn.knownApps.cashtabMsg.app;
// For a Cashtab msg, the next push on the stack is the Cashtab msg
// Cashtab msgs use utf8 encoding
// Valid Cashtab Msg
// <protocol identifier> <msg in utf8>
msg =
stackArray.length >= 2
? prepareStringForTelegramHTML(
Buffer.from(stackArray[1], 'hex').toString(
'utf8',
),
)
: `Invalid ${app}`;
break;
}
case opReturn.knownApps.cashtabMsgEncrypted.prefix: {
app = opReturn.knownApps.cashtabMsgEncrypted.app;
// For an encrypted cashtab msg, you can't parse and display the msg
msg = '';
// You will add info about the tx when you build the msg
break;
}
case opReturn.knownApps.fusionLegacy.prefix:
case opReturn.knownApps.fusion.prefix: {
/**
* Cash Fusion tx
* <protocolIdentifier> <sessionHash>
* https://github.com/cashshuffle/spec/blob/master/CASHFUSION.md
*/
app = opReturn.knownApps.fusion.app;
// The session hash is not particularly interesting to users
// Provide tx info in telegram prep function
msg = '';
break;
}
case opReturn.knownApps.swap.prefix: {
// Swap txs require special parsing that should be done in getSwapTgMsg
// We may need to get info about a token ID before we can
// create a good msg
app = opReturn.knownApps.swap.app;
msg = '';
if (
stackArray.length >= 3 &&
stackArray[1] === '01' &&
stackArray[2] === '01' &&
stackArray[3].length === 64
) {
// If this is a signal for buy or sell of a token, save the token id
// Ref https://github.com/vinarmani/swap-protocol/blob/master/swap-protocol-spec.md
// A buy or sell signal tx will have '01' at stackArray[1] and stackArray[2] and
// token id at stackArray[3]
tokenId = stackArray[3];
}
break;
}
default: {
/**
* If you don't recognize protocolIdentifier, just translate with ASCII
* Will be easy to spot these msgs in the bot and add special parsing rules *
*/
app = 'unknown';
msg = prepareStringForTelegramHTML(
Buffer.from(stackArray.join(''), 'hex').toString('ascii'),
);
break;
}
}
return { app, msg, stackArray, tokenId };
},
/**
* Parse an empp stack for a simplified slp v2 description
* TODO expand for parsing other types of empp txs as specs or examples are known
* @param {array} emppStackArray an array containing a hex string for every push of this memo OP_RETURN outputScript
* @returns {object} {app, msg} used to compose a useful telegram msg describing the transaction
*/
parseMultipushStack: function (emppStackArray) {
// Note that an empp push may not necessarily include traditionally parsed pushes
// i.e. consumeNextPush({remainingHex:<emppPush>}) may throw an error
// For example, SLPv2 txs do not include a push for their prefix
// So, parsing empp txs will require specific rules depending on the type of tx
let msgs = [];
// Start at i=1 because emppStackArray[0] is OP_RESERVED
for (let i = 1; i < emppStackArray.length; i += 1) {
if (
emppStackArray[i].slice(0, 8) === opReturn.knownApps.slp2.prefix
) {
// Parse string for slp v2
const thisMsg = module.exports.parseSlpTwo(
emppStackArray[i].slice(8),
);
msgs.push(`${opReturn.knownApps.slp2.app}:${thisMsg}`);
} else {
// Since we don't know any spec or parsing rules for other types of EMPP pushes,
// Just add an ASCII decode of the whole thing if you see one
msgs.push(
`${'Unknown App:'}${Buffer.from(
emppStackArray[i],
'hex',
).toString('ascii')}`,
);
}
// Do not parse any other empp (haven't seen any in the wild, no existing specs to follow)
}
if (msgs.length > 0) {
return { app: 'EMPP', msg: msgs.join('|') };
}
},
/**
* Stub method to parse slp two empps
* @param {string} slpTwoPush a string of hex characters in an empp tx representing an slp2 push
* @returns {string} For now, just the section type, if token type is correct
*/
parseSlpTwo: function (slpTwoPush) {
// Parse an empp push hex string with the SLP protocol identifier removed per SLP v2 spec
// https://ecashbuilders.notion.site/SLPv2-a862a4130877448387373b9e6a93dd97
let msg = '';
// 1.3: Read token type
// For now, this can only be 00. If not 00, unknown
const tokenType = slpTwoPush.slice(0, 2);
if (tokenType !== '00') {
msg += 'Unknown token type|';
}
// 1.4: Read section type
// Note: these are encoded with push data, so you can use ecash-script
let stack = { remainingHex: slpTwoPush.slice(2) };
const sectionType = Buffer.from(consumeNextPush(stack), 'hex').toString(
'utf8',
);
msg += sectionType;
// Stop here for now
// The rest of the parsing rules get quite complicated and should be handled in a dedicated library
// or indexer
return msg;
},
/**
* Parse a stackArray according to OP_RETURN rules to convert to a useful tg msg
* @param {Array} stackArray an array containing a hex string for every push of this memo OP_RETURN outputScript
* @returns {string} A useful string to describe this tx in a telegram msg
*/
parseMemoOutputScript: function (stackArray) {
let app = opReturn.memo.app;
let msg = '';
// Get the action code from stackArray[0]
// For memo txs, this will be the last 2 characters of this initial push
const actionCode = stackArray[0].slice(-2);
if (Object.keys(opReturn.memo).includes(actionCode)) {
// If you parse for this action code, include its description in the tg msg
msg += opReturn.memo[actionCode];
// Include a formatting spacer in between action code and newsworthy info
msg += '|';
}
switch (actionCode) {
case '01': // Set name <name> (1-217 bytes)
case '02': // Post memo <message> (1-217 bytes)
case '05': // Set profile text <text> (1-217 bytes)
case '0d': // Topic Follow <topic_name> (1-214 bytes)
case '0e': // Topic Unfollow <topic_name> (1-214 bytes)
// Action codes with only 1 push after the protocol identifier
// that is utf8 encoded
// Include decoded utf8 msg
// Make sure the OP_RETURN msg does not contain telegram html escape characters
msg += prepareStringForTelegramHTML(
Buffer.from(stackArray[1], 'hex').toString('utf8'),
);
break;
case '03':
/**
* 03 - Reply to memo
* <tx_hash> (32 bytes)
* <message> (1-184 bytes)
*/
// The tx hash is in hex, not utf8 encoded
// For now, we don't have much to do with this txid in a telegram bot
// Link to the liked or reposted memo
// Do not remove tg escape characters as you want this to parse
msg += `<a href="${config.blockExplorer}/tx/${stackArray[1]}">memo</a>`;
// Include a formatting spacer
msg += '|';
// Add the reply
msg += prepareStringForTelegramHTML(
Buffer.from(stackArray[2], 'hex').toString('utf8'),
);
break;
case '04':
/**
* 04 - Like / tip memo <tx_hash> (32 bytes)
*/
// Link to the liked or reposted memo
msg += `<a href="${config.blockExplorer}/tx/${stackArray[1]}">memo</a>`;
break;
case '0b': {
// 0b - Repost memo <tx_hash> (32 bytes) <message> (0-184 bytes)
// Link to the liked or reposted memo
msg += `<a href="${config.blockExplorer}/tx/${stackArray[1]}">memo</a>`;
// Include a formatting spacer
msg += '|';
// Add the msg
msg += prepareStringForTelegramHTML(
Buffer.from(stackArray[2], 'hex').toString('utf8'),
);
break;
}
case '06':
case '07':
case '16':
case '17': {
/**
* Follow user - 06 <address> (20 bytes)
* Unfollow user - 07 <address> (20 bytes)
* Mute user - 16 <address> (20 bytes)
* Unmute user - 17 <address> (20 bytes)
*/
// The address is a hex-encoded hash160
// all memo addresses are p2pkh
const address = cashaddr.encode(
'ecash',
'P2PKH',
stackArray[1],
);
// Link to the address in the msg
msg += `<a href="${
config.blockExplorer
}/address/${address}">${returnAddressPreview(address)}</a>`;
break;
}
case '0a': {
// 01 - Set profile picture
// <url> (1-217 bytes)
// url is utf8 encoded stack[1]
const url = Buffer.from(stackArray[1], 'hex').toString('utf8');
// Link to it
msg += `<a href="${url}">[img]</a>`;
break;
}
case '0c': {
/**
* 0c - Post Topic Message
* <topic_name> (1-214 bytes)
* <message> (1-[214-len(topic_name)] bytes)
*/
// Add the topic
msg += prepareStringForTelegramHTML(
Buffer.from(stackArray[1], 'hex').toString('utf8'),
);
// Add a format spacer
msg += '|';
// Add the topic msg
msg += prepareStringForTelegramHTML(
Buffer.from(stackArray[2], 'hex').toString('utf8'),
);
break;
}
case '10': {
/**
* 10 - Create Poll
* <poll_type> (1 byte)
* <option_count> (1 byte)
* <question> (1-209 bytes)
* */
// You only need the question here
msg += prepareStringForTelegramHTML(
Buffer.from(stackArray[3], 'hex').toString('utf8'),
);
break;
}
case '13': {
/**
* 13 Add poll option
* <poll_tx_hash> (32 bytes)
* <option> (1-184 bytes)
*/
// Only parse the option for now
msg += prepareStringForTelegramHTML(
Buffer.from(stackArray[2], 'hex').toString('utf8'),
);
break;
}
case '14': {
/**
* 14 - Poll Vote
* <poll_tx_hash> (32 bytes)
* <comment> (0-184 bytes)
*/
// We just want the comment
msg += prepareStringForTelegramHTML(
Buffer.from(stackArray[2], 'hex').toString('utf8'),
);
break;
}
case '20':
case '24':
case '26': {
/**
* 20 - Link request
* 24 - Send money
* 26 - Set address alias
* <address_hash> (20 bytes)
* <message> (1-194 bytes)
*/
// The address is a hex-encoded hash160
// all memo addresses are p2pkh
const address = cashaddr.encode(
'ecash',
'P2PKH',
stackArray[1],
);
// Link to the address in the msg
msg += `<a href="${
config.blockExplorer
}/address/${address}">${returnAddressPreview(address)}</a>`;
// Add a format spacer
msg += '|';
// Add the msg
msg += prepareStringForTelegramHTML(
Buffer.from(stackArray[2], 'hex').toString('utf8'),
);
break;
}
case '21':
case '22':
case '30':
case '31':
case '32':
case '35': {
/**
* https://github.com/memocash/mips/blob/master/mip-0009/mip-0009.md#specification
*
* These would require additional processing to get info about the specific tokens
* For now, not worth it. Just print the action.
*
* 21 - Link accept
* 22 - Link revoke
* 30 - Sell tokens
* 31 - Token buy offer
* 32 - Attach token sale signature
* 35 - Pin token post
*/
// Remove formatting spacer
msg = msg.slice(0, -1);
break;
}
default:
msg += `Unknown memo action`;
}
return { app, msg };
},
/**
* Build a msg about an encrypted cashtab msg tx
* @param {string} sendingAddress
* @param {map} xecReceivingOutputs
* @param {object} coingeckoPrices
* @returns {string} msg
*/
getEncryptedCashtabMsg: function (
sendingAddress,
xecReceivingOutputs,
totalSatsSent,
coingeckoPrices,
) {
let displayedSentQtyString = satsToFormattedValue(
totalSatsSent,
coingeckoPrices,
);
// Remove OP_RETURNs from xecReceivingOutputs
let receivingOutputscripts = [];
for (const outputScript of xecReceivingOutputs.keys()) {
if (!outputScript.startsWith(opReturn.opReturnPrefix)) {
receivingOutputscripts.push(outputScript);
}
}
let msgRecipientString = `${returnAddressPreview(
cashaddr.encodeOutputScript(receivingOutputscripts[0]),
)}`;
if (receivingOutputscripts.length > 1) {
// Subtract 1 because you have already rendered one receiving address
msgRecipientString += ` and ${
receivingOutputscripts.length - 1
} other${receivingOutputscripts.length > 2 ? 's' : ''}`;
}
return `${returnAddressPreview(
sendingAddress,
)} sent an encrypted message and ${displayedSentQtyString} to ${msgRecipientString}`;
},
/**
* Parse the stackArray of an airdrop tx to generate a useful telegram msg
* @param {array} stackArray
* @param {string} airdropSendingAddress
* @param {Map} airdropRecipientsMap
* @param {object} tokenInfo token info for the swapped token. optional. Bool False if API call failed.
* @param {object} coingeckoPrices object containing price info from coingecko. Bool False if API call failed.
* @returns {string} msg ready to send through Telegram API
*/
getAirdropTgMsg: function (
stackArray,
airdropSendingAddress,
airdropRecipientsMap,
totalSatsAirdropped,
tokenInfo,
coingeckoPrices,
) {
// stackArray for an airdrop tx will be
// [airdrop_protocol_identifier, airdropped_tokenId, optional_cashtab_msg_protocol_identifier, optional_cashtab_msg]
// Validate expected format
if (stackArray.length < 2 || stackArray[1].length !== 64) {
return `Invalid ${opReturn.knownApps.airdrop.app}`;
}
// get tokenId
const tokenId = stackArray[1];
// Intialize msg with preview of sending address
let msg = `${returnAddressPreview(airdropSendingAddress)} airdropped `;
let displayedAirdroppedQtyString = satsToFormattedValue(
totalSatsAirdropped,
coingeckoPrices,
);
// Add to msg
msg += `${displayedAirdroppedQtyString} to ${airdropRecipientsMap.size} holders of `;
if (tokenInfo) {
// If API call to get tokenInfo was successful to tokenInfo !== false
const { tokenTicker } = tokenInfo;
// Link to token id
msg += `<a href="${
config.blockExplorer
}/tx/${tokenId}">${prepareStringForTelegramHTML(tokenTicker)}</a>`;
} else {
// Note: tokenInfo is false if the API call to chronik fails
// Link to token id
msg += `<a href="${config.blockExplorer}/tx/${tokenId}">${
tokenId.slice(0, 3) + '...' + tokenId.slice(-3)
}</a>`;
}
// Add Cashtab msg if present
if (
stackArray.length > 3 &&
stackArray[2] === opReturn.knownApps.cashtabMsg.prefix
) {
msg += '|';
msg += prepareStringForTelegramHTML(
Buffer.from(stackArray[3], 'hex').toString('utf8'),
);
}
return msg;
},
/**
* Parse the stackArray of a SWaP tx according to spec to generate a useful telegram msg
* @param {array} stackArray
* @param {object} tokenInfo token info for the swapped token. optional.
* @returns {string} msg ready to send through Telegram API
*/
getSwapTgMsg: function (stackArray, tokenInfo) {
// Intialize msg
let msg = '';
// Generic validation to handle possible txs with SWaP protocol identifier but unexpected stack
if (stackArray.length < 3) {
// If stackArray[1] and stackArray[2] do not exist
return 'Invalid SWaP';
}
// SWaP txs are complex. Parse stackArray to build msg.
// https://github.com/vinarmani/swap-protocol/blob/master/swap-protocol-spec.md
// First, get swp_msg_class at stackArray[1]
// 01 - A Signal
// 02 - A payment
const swp_msg_class = stackArray[1];
// Second , get swp_msg_type at stackArray[2]
// 01 - SLP Atomic Swap
// 02 - Multi-Party Escrow
// 03 - Threshold Crowdfunding
const swp_msg_type = stackArray[2];
// Build msg by class and type
if (swp_msg_class === '01') {
msg += 'Signal';
msg += '|';
switch (swp_msg_type) {
case '01': {
msg += 'SLP Atomic Swap';
msg += '|';
/*
<token_id_bytes> <BUY_or_SELL_ascii> <rate_in_sats_int>
<proof_of_reserve_int> <exact_utxo_vout_hash_bytes> <exact_utxo_index_int>
<minimum_sats_to_exchange_int>
Note that <rate_in_sats_int> is in hex value in the spec example,
but some examples on chain appear to encode this value in ascii
*/
if (tokenInfo) {
const { tokenTicker } = tokenInfo;
// Link to token id
msg += `<a href="${config.blockExplorer}/tx/${
stackArray[3]
}">${prepareStringForTelegramHTML(tokenTicker)}</a>`;
msg += '|';
} else {
// Note: tokenInfo is false if the API call to chronik fails
// Also false if tokenId is invalid for some reason
// Link to token id if valid
if (
stackArray.length >= 3 &&
stackArray[3].length === 64
) {
msg += `<a href="${config.blockExplorer}/tx/${stackArray[3]}">Unknown Token</a>`;
msg += '|';
} else {
msg += 'Invalid tokenId|';
}
}
// buy or sell?
msg += Buffer.from(stackArray[4], 'hex').toString('ascii');
// Add price info if present
// price in XEC, must convert <rate_in_sats_int> from sats to XEC
if (stackArray.length >= 6) {
// In the wild, have seen some SWaP txs use ASCII for encoding rate_in_sats_int
// Make a determination. Spec does not indicate either way, though spec
// example does use hex.
// If stackArray[5] is more than 4 characters long, assume ascii encoding
let rate_in_sats_int;
if (stackArray[5].length > 4) {
rate_in_sats_int = parseInt(
Buffer.from(stackArray[5], 'hex').toString(
'ascii',
),
);
} else {
rate_in_sats_int = parseInt(stackArray[5], 16);
}
msg += ` for ${(
parseInt(rate_in_sats_int) / 100
).toLocaleString('en-US', {
maximumFractionDigits: 2,
})} XEC`;
}
// Display minimum_sats_to_exchange_int
// Note: sometimes a SWaP tx will not have this info
if (stackArray.length >= 10) {
// In the wild, have seen some SWaP txs use ASCII for encoding minimum_sats_to_exchange_int
// Make a determination. Spec does not indicate either way, though spec
// example does use hex.
// If stackArray[9] is more than 4 characters long, assume ascii encoding
let minimum_sats_to_exchange_int;
if (stackArray[9].length > 4) {
minimum_sats_to_exchange_int = Buffer.from(
stackArray[9],
'hex',
).toString('ascii');
} else {
minimum_sats_to_exchange_int = parseInt(
stackArray[9],
16,
);
}
msg += '|';
msg += `Min trade: ${(
parseInt(minimum_sats_to_exchange_int) / 100
).toLocaleString('en-US', {
maximumFractionDigits: 2,
})} XEC`;
}
break;
}
case '02': {
msg += 'Multi-Party Escrow';
// TODO additional parsing
break;
}
case '03': {
msg += 'Threshold Crowdfunding';
// TODO additional parsing
break;
}
default: {
// Malformed SWaP tx
msg += 'Invalid SWaP';
break;
}
}
} else if (swp_msg_class === '02') {
msg += 'Payment';
msg += '|';
switch (swp_msg_type) {
case '01': {
msg += 'SLP Atomic Swap';
// TODO additional parsing
break;
}
case '02': {
msg += 'Multi-Party Escrow';
// TODO additional parsing
break;
}
case '03': {
msg += 'Threshold Crowdfunding';
// TODO additional parsing
break;
}
default: {
// Malformed SWaP tx
msg += 'Invalid SWaP';
break;
}
}
} else {
// Malformed SWaP tx
msg += 'Invalid SWaP';
}
return msg;
},
/**
* Build a string formatted for Telegram's API using HTML encoding
* @param {object} parsedBlock
* @param {array or false} coingeckoPrices if no coingecko API error
* @param {Map or false} tokenInfoMap if no chronik API error
* @param {Map or false} addressInfoMap if no chronik API error
* @returns {function} splitOverflowTgMsg(tgMsg)
*/
getBlockTgMessage: function (
parsedBlock,
coingeckoPrices,
tokenInfoMap,
outputScriptInfoMap,
) {
const { hash, height, miner, numTxs, parsedTxs } = parsedBlock;
const { emojis } = config;
// Define newsworthy types of txs in parsedTxs
// These arrays will be used to present txs in batches by type
const genesisTxTgMsgLines = [];
const tokenSendTxTgMsgLines = [];
const tokenBurnTxTgMsgLines = [];
const opReturnTxTgMsgLines = [];
let 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,
txFee,
xecSendingOutputScripts,
xecReceivingOutputs,
tokenSendInfo,
tokenBurnInfo,
totalSatsSent,
} = 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(
`${emojis.tokenGenesis}<a href="${config.blockExplorer}/tx/${tokenId}">${tokenName}</a> (${tokenTicker}) <a href="${tokenDocumentUrl}">[doc]</a>`,
);
// This parsed tx has a tg msg line. Move on to the next one.
continue;
}
if (opReturnInfo) {
let { app, msg, stackArray, tokenId } = opReturnInfo;
let appEmoji = '';
switch (app) {
case opReturn.memo.app: {
appEmoji = emojis.memo;
break;
}
case opReturn.knownApps.alias.app: {
appEmoji = emojis.alias;
break;
}
case opReturn.knownApps.cashtabMsg.app: {
appEmoji = emojis.cashtabMsg;
const displayedSentAmount = satsToFormattedValue(
totalSatsSent,
coingeckoPrices,
);
const displayedTxFee = satsToFormattedValue(
txFee,
coingeckoPrices,
);
app += `, ${displayedSentAmount} for ${displayedTxFee}`;
break;
}
case opReturn.knownApps.cashtabMsgEncrypted.app: {
msg = module.exports.getEncryptedCashtabMsg(
cashaddr.encodeOutputScript(
xecSendingOutputScripts.values().next().value,
), // Assume first input is sender
xecReceivingOutputs,
totalSatsSent,
coingeckoPrices,
);
appEmoji = emojis.cashtabEncrypted;
break;
}
case opReturn.knownApps.airdrop.app: {
msg = module.exports.getAirdropTgMsg(
stackArray,
cashaddr.encodeOutputScript(
xecSendingOutputScripts.values().next().value,
), // Assume first input is sender
xecReceivingOutputs,
totalSatsSent,
tokenId && tokenInfoMap
? tokenInfoMap.get(tokenId)
: false,
coingeckoPrices,
);
appEmoji = emojis.airdrop;
break;
}
case opReturn.knownApps.swap.app: {
msg = module.exports.getSwapTgMsg(
stackArray,
tokenId && tokenInfoMap
? tokenInfoMap.get(tokenId)
: false,
);
appEmoji = emojis.swap;
break;
}
case opReturn.knownApps.fusion.app: {
// totalSatsSent is total amount fused
let displayedFusedQtyString = satsToFormattedValue(
totalSatsSent,
coingeckoPrices,
);
msg += `Fused ${displayedFusedQtyString} from ${xecSendingOutputScripts.size} inputs into ${xecReceivingOutputs.size} outputs`;
appEmoji = emojis.fusion;
break;
}
default: {
appEmoji = emojis.unknown;
break;
}
}
opReturnTxTgMsgLines.push(
`${appEmoji}<a href="${config.blockExplorer}/tx/${txid}">${app}:</a> ${msg}`,
);
// This parsed tx has a tg msg line. Move on to the next one.
continue;
}
if (tokenSendInfo && tokenInfoMap && !tokenBurnInfo) {
// If this is a token send tx that does not burn any tokens and you have 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 =
bigNumberAmountToLocaleString(
undecimalizedTokenChangeAmount.toString(),
decimals,
);
// Self send tokenSendMsg
tokenSendMsg = `${emojis.tokenSend}${
tokenSendingOutputScripts.size
} ${
tokenSendingOutputScripts.size > 1
? 'addresses'
: 'address'
} <a href="${
config.blockExplorer
}/tx/${txid}">sent</a> ${decimalizedTokenChangeAmount} <a href="${
config.blockExplorer
}/tx/${tokenId}">${tokenTicker}</a> 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 =
bigNumberAmountToLocaleString(
undecimalizedTokenReceivedAmount.toString(),
decimals,
);
tokenSendMsg = `${emojis.tokenSend}${returnAddressPreview(
cashaddr.encodeOutputScript(
tokenSendingOutputScripts.values().next().value,
),
)} <a href="${
config.blockExplorer
}/tx/${txid}">sent</a> ${decimalizedTokenReceivedAmount} <a href="${
config.blockExplorer
}/tx/${tokenId}">${tokenTicker}</a> to ${returnAddressPreview(
cashaddr.encodeOutputScript(
tokenReceivingOutputs.keys().next().value,
),
)}${
tokenReceivingOutputs.size > 1
? ` and ${tokenReceivingOutputs.size - 1} other${
tokenReceivingOutputs.size - 1 > 1 ? 's' : ''
}`
: ''
}`;
}
tokenSendTxTgMsgLines.push(tokenSendMsg);
// This parsed tx has a tg msg line. Move on to the next one.
continue;
}
if (tokenBurnInfo && tokenInfoMap) {
// If this is a token burn tx and you have tokenInfoMap
const { tokenId, tokenSendingOutputScripts } = tokenSendInfo;
const { undecimalizedTokenBurnAmount } = tokenBurnInfo;
if (
typeof tokenId !== 'undefined' &&
tokenInfoMap.has(tokenId)
) {
// Some txs may have tokenBurnInfo, but did not get tokenSendInfo
// e.g. 0bb7e38d7f3968d3c91bba2d7b32273f203bc8b1b486633485f76dc7416a3eca
// This is a token burn tx but it is not indexed as such and requires more sophisticated burn parsing
// So, for now, just parse txs like this as XEC sends
// Get token info from tokenInfoMap
const thisTokenInfo = tokenInfoMap.get(tokenId);
let { tokenTicker, decimals } = thisTokenInfo;
// Make sure tokenName does not contain telegram html escape characters
tokenTicker = prepareStringForTelegramHTML(tokenTicker);
// Calculate true tokenReceivedAmount using decimals
// Use decimals to calculate the burned amount as string
const decimalizedTokenBurnAmount =
bigNumberAmountToLocaleString(
undecimalizedTokenBurnAmount,
decimals,
);
const tokenBurningAddressStr = returnAddressPreview(
cashaddr.encodeOutputScript(
tokenSendingOutputScripts.values().next().value,
),
);
tokenBurnTxTgMsgLines.push(
`${emojis.tokenBurn}${tokenBurningAddressStr} <a href="${config.blockExplorer}/tx/${txid}">burned</a> ${decimalizedTokenBurnAmount} <a href="${config.blockExplorer}/tx/${tokenId}">${tokenTicker}</a> `,
);
// 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
- // Convert sats to XEC. Round as decimals will not be rendered in msgs.
- const displayedXecSent = satsToFormattedValue(
+ const displayedSentAmount = satsToFormattedValue(
totalSatsSent,
coingeckoPrices,
);
const displayedTxFee = satsToFormattedValue(txFee, coingeckoPrices);
// 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(opReturn.opReturnPrefix)) {
map.delete(key);
}
});
// Get address balance emojis for rendered addresses
// NB you are using xecReceivingAddressOutputs to avoid OP_RETURN outputScripts
let xecSenderEmoji = '';
let xecReceiverEmoji = '';
if (outputScriptInfoMap) {
// If you have information about address balances, get balance emojis
const firstXecSendingOutputScript = xecSendingOutputScripts
.values()
.next().value;
const firstXecReceivingOutputScript = xecReceivingAddressOutputs
.keys()
.next().value;
xecSenderEmoji = outputScriptInfoMap.has(
firstXecSendingOutputScript,
)
? outputScriptInfoMap.get(firstXecSendingOutputScript).emoji
: '';
xecReceiverEmoji = outputScriptInfoMap.has(
firstXecReceivingOutputScript,
)
? outputScriptInfoMap.get(firstXecReceivingOutputScript)
.emoji
: '';
}
let xecSendMsg;
if (xecReceivingAddressOutputs.size === 0) {
// self send tx
// In this case, totalSatsSent has already been assigned to changeAmountSats
xecSendMsg = `${emojis.xecSend}<a href="${
config.blockExplorer
- }/tx/${txid}">${displayedXecSent} for ${displayedTxFee}</a>${
+ }/tx/${txid}">${displayedSentAmount} for ${displayedTxFee}</a>${
xecSenderEmoji !== ''
? ` ${xecSenderEmoji}${xecSendingOutputScripts.size} ${
xecSendingOutputScripts.size > 1
? 'addresses'
: 'address'
} ${config.emojis.arrowRight} ${
xecSendingOutputScripts.size > 1
? 'themselves'
: 'itself'
}`
: ''
}`;
} else {
xecSendMsg = `${emojis.xecSend}<a href="${
config.blockExplorer
- }/tx/${txid}">${displayedXecSent} for ${displayedTxFee}</a>${
+ }/tx/${txid}">${displayedSentAmount} for ${displayedTxFee}</a>${
xecSenderEmoji !== '' || xecReceiverEmoji !== ''
? ` ${xecSenderEmoji}${returnAddressPreview(
cashaddr.encodeOutputScript(
xecSendingOutputScripts.values().next().value,
),
)} ${config.emojis.arrowRight} ${
xecReceivingAddressOutputs.keys().next().value ===
xecSendingOutputScripts.values().next().value
? 'itself'
: `${xecReceiverEmoji}${returnAddressPreview(
cashaddr.encodeOutputScript(
xecReceivingAddressOutputs
.keys()
.next().value,
),
)}`
}${
xecReceivingAddressOutputs.size > 1
? ` and ${
xecReceivingAddressOutputs.size - 1
} other${
xecReceivingAddressOutputs.size - 1 > 1
? 's'
: ''
}`
: ''
}`
: ''
}`;
}
xecSendTxTgMsgLines.push(xecSendMsg);
}
// Build up message as an array, with each line as an entry
let tgMsg = [];
// Header
// <emojis.block><height> | <numTxs> | <miner>
tgMsg.push(
`${emojis.block}<a href="${
config.blockExplorer
}/block/${hash}">${height}</a> | ${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
// <n> new eTokens created:
tgMsg.push(
`<b>${genesisTxTgMsgLines.length} new eToken${
genesisTxTgMsgLines.length > 1 ? `s` : ''
} created</b>`,
);
tgMsg = tgMsg.concat(genesisTxTgMsgLines);
}
// eToken Send txs
if (tokenSendTxTgMsgLines.length > 0) {
// Line break for new section
tgMsg.push('');
// 1 eToken send tx:
// or
// <n> eToken send txs:
tgMsg.push(
`<b>${tokenSendTxTgMsgLines.length} eToken send tx${
tokenSendTxTgMsgLines.length > 1 ? `s` : ''
}</b>`,
);
tgMsg = tgMsg.concat(tokenSendTxTgMsgLines);
}
// eToken burn txs
if (tokenBurnTxTgMsgLines.length > 0) {
// Line break for new section
tgMsg.push('');
// 1 eToken burn tx:
// or
// <n> eToken burn txs:
tgMsg.push(
`<b>${tokenBurnTxTgMsgLines.length} eToken burn tx${
tokenBurnTxTgMsgLines.length > 1 ? `s` : ''
}</b>`,
);
tgMsg = tgMsg.concat(tokenBurnTxTgMsgLines);
}
// OP_RETURN txs
if (opReturnTxTgMsgLines.length > 0) {
// Line break for new section
tgMsg.push('');
// App txs
// or
// App tx
tgMsg.push(
`<b>${opReturnTxTgMsgLines.length} app tx${
opReturnTxTgMsgLines.length > 1 ? `s` : ''
}</b>`,
);
// <appName> : <parsedAppData>
// alias: newlyregisteredalias
// Cashtab Msg: This is a Cashtab Msg
tgMsg = tgMsg.concat(opReturnTxTgMsgLines);
}
// XEC txs
const totalXecSendCount = xecSendTxTgMsgLines.length;
if (totalXecSendCount > 0) {
// Line break for new section
tgMsg.push('');
// Don't show more than config-adjustable amount of these txs
if (totalXecSendCount > config.xecSendDisplayCount) {
xecSendTxTgMsgLines = xecSendTxTgMsgLines.slice(
0,
config.xecSendDisplayCount,
);
xecSendTxTgMsgLines.push(
`...and <a href="${config.blockExplorer}/block/${hash}">${
totalXecSendCount - config.xecSendDisplayCount
} more</a>`,
);
}
// 1 eCash tx
// or
// n eCash txs
tgMsg.push(
`<b>${totalXecSendCount} eCash tx${
totalXecSendCount > 1 ? `s` : ''
}</b>`,
);
tgMsg = tgMsg.concat(xecSendTxTgMsgLines);
}
return splitOverflowTgMsg(tgMsg);
},
};

File Metadata

Mime Type
text/x-diff
Expires
Sun, Mar 2, 10:52 (1 d, 11 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5187413
Default Alt Text
(68 KB)

Event Timeline