Page MenuHomePhabricator

No OneTemporary

diff --git a/apps/ecash-herald/constants/lokad.js b/apps/ecash-herald/constants/lokad.js
new file mode 100644
index 0000000000..69bd655035
--- /dev/null
+++ b/apps/ecash-herald/constants/lokad.js
@@ -0,0 +1,31 @@
+// Copyright (c) 2024 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';
+
+/**
+ * Create a map of lokad IDs to app name used for daily summaries and, soon, tweets
+ * Ultimately this should replace the knownApps in constants/op_return.js
+ * However the parsing needs are distinct for daily summary vs block msgs
+ */
+const lokadMap = new Map();
+
+lokadMap.set('64726f70', { name: 'Airdrop', emoji: '🪂' });
+lokadMap.set('00746162', { name: 'Cashtab Msg', emoji: '✏️' });
+lokadMap.set('61757468', { name: 'eCashChat Auth', emoji: '🔓' });
+lokadMap.set('46555a00', { name: 'CashFusion', emoji: '⚛️' });
+lokadMap.set('50415900', { name: 'PayButton tx', emoji: '🛒' });
+lokadMap.set('70617977', { name: 'Paywall tx', emoji: '💸' });
+lokadMap.set('63686174', {
+ name: 'eCashChat tx',
+ emoji: '💬',
+ url: 'https://www.ecashchat.com/',
+});
+lokadMap.set('626c6f67', {
+ name: 'Article/Reply tx',
+ emoji: '🖋',
+ url: 'https://www.ecashchat.com/',
+});
+
+module.exports = lokadMap;
diff --git a/apps/ecash-herald/src/parse.js b/apps/ecash-herald/src/parse.js
index e672c5f5e9..932442b669 100644
--- a/apps/ecash-herald/src/parse.js
+++ b/apps/ecash-herald/src/parse.js
@@ -1,2515 +1,2556 @@
// 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 { consume, consumeNextPush, swapEndianness } = require('ecash-script');
const knownMinersJson = require('../constants/miners');
const cachedTokenInfoMap = require('../constants/tokens');
const { jsonReviver, bigNumberAmountToLocaleString } = require('../src/utils');
const miners = JSON.parse(JSON.stringify(knownMinersJson), jsonReviver);
const cashaddr = require('ecashaddrjs');
const BigNumber = require('bignumber.js');
const {
TOKEN_SERVER_OUTPUTSCRIPT,
BINANCE_OUTPUTSCRIPT,
} = require('../constants/senders');
const {
prepareStringForTelegramHTML,
splitOverflowTgMsg,
} = require('./telegram');
const {
formatPrice,
satsToFormattedValue,
returnAddressPreview,
containsOnlyPrintableAscii,
} = require('./utils');
+const lokadMap = require('../constants/lokad');
// Constants for SLP 1 token types as returned by chronik-client
const SLP_1_PROTOCOL_NUMBER = 1;
const SLP_1_NFT_COLLECTION_PROTOCOL_NUMBER = 129;
const SLP_1_NFT_PROTOCOL_NUMBER = 65;
// Miner fund output script
const minerFundOutputScript = 'a914d37c4c809fe9840e7bfa77b86bd47163f6fb6c6087';
module.exports = {
/**
* Parse a finalized block for newsworthy information
* @param {string} blockHash
* @param {number} blockHeight
* @param {Tx_InNode[]} txs
*/
parseBlockTxs: function (blockHash, blockHeight, txs) {
// Parse coinbase string
const coinbaseTx = txs[0];
const miner = module.exports.getMinerFromCoinbaseTx(
coinbaseTx.inputs[0].inputScript,
coinbaseTx.outputs,
miners,
);
let staker = module.exports.getStakerFromCoinbaseTx(
blockHeight,
coinbaseTx.outputs,
);
try {
staker.staker = cashaddr.encodeOutputScript(staker.staker);
} catch (err) {
staker.staker = 'script(' + staker.staker + ')';
}
// 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);
}
if (thisParsedTx.genesisInfo) {
tokenIds.add(thisParsedTx.genesisInfo.tokenId);
}
if (thisParsedTx.tokenBurnInfo) {
tokenIds.add(thisParsedTx.tokenBurnInfo.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: blockHash,
height: blockHeight,
miner,
staker,
numTxs: txs.length,
parsedTxs,
tokenIds,
outputScripts,
};
},
getStakerFromCoinbaseTx: function (blockHeight, coinbaseOutputs) {
const STAKING_ACTIVATION_HEIGHT = 818670;
if (blockHeight < STAKING_ACTIVATION_HEIGHT) {
// Do not parse for staking rwds if they are not expected to exist
return false;
}
const STAKING_REWARDS_PERCENT = 10;
const totalCoinbaseSats = coinbaseOutputs
.map(output => parseInt(output.value))
.reduce((prev, curr) => prev + curr, 0);
for (let output of coinbaseOutputs) {
const thisValue = parseInt(output.value);
const minStakerValue = Math.floor(
totalCoinbaseSats * STAKING_REWARDS_PERCENT * 0.01,
);
// In practice, the staking reward will almost always be the one that is exactly 10% of totalCoinbaseSats
// Use a STAKER_PERCENT_PADDING range to exclude miner and ifp outputs
const STAKER_PERCENT_PADDING = 1;
const assumedMaxStakerValue = Math.floor(
totalCoinbaseSats *
(STAKING_REWARDS_PERCENT + STAKER_PERCENT_PADDING) *
0.01,
);
if (
thisValue >= minStakerValue &&
thisValue <= assumedMaxStakerValue
) {
return {
// Return the script, there is no guarantee that we can use
// an address to display this.
staker: output.outputScript,
reward: thisValue,
};
}
}
// If you don't find a staker, don't add it in msg. Can troubleshoot if see this in the app.
// This can happen if a miner overpays rwds, underpays miner rwds
return false;
},
getMinerFromCoinbaseTx: function (
coinbaseScriptsig,
coinbaseOutputs,
knownMiners,
) {
// When you find the miner, minerInfo will come from knownMiners
let minerInfo = false;
// First, check outputScripts for a known miner
for (let i = 0; i < coinbaseOutputs.length; i += 1) {
const thisOutputScript = coinbaseOutputs[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 (coinbaseScriptsig.includes(coinbaseHexFragment)) {
minerInfo = knownMinerInfo;
}
});
}
if (!minerInfo) {
// We're still unable to identify the miner, so resort to
// indentifying by the last chars of the payout address. For now
// we assume the ordering of outputs such as the miner reward is at
// the first position.
const minerPayoutSript = coinbaseOutputs[0].outputScript;
try {
const minerAddress =
cashaddr.encodeOutputScript(minerPayoutSript);
return `unknown, ...${minerAddress.slice(-4)}`;
} catch (err) {
console.log(
`Error converting miner payout script (${minerPayoutSript}) to eCash address`,
err,
);
// Give up
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 = coinbaseScriptsig.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`;
}
if (infoAscii === 'mined by iceberg') {
// If the miner self identifies as iceberg, go with it
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;
/* 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.tokenStatus !== 'TOKEN_STATUS_NON_TOKEN' &&
tx.tokenEntries.length > 0
) {
isTokenTx = true;
// We may have more than one token action in a given tx
// chronik will reflect this by having multiple entries in the tokenEntries array
// For now, just parse the first action
// TODO handle txs with multiple tokenEntries
const parsedTokenAction = tx.tokenEntries[0];
const {
tokenId,
tokenType,
txType,
burnSummary,
actualBurnAmount,
} = parsedTokenAction;
const { protocol, number } = tokenType;
const isUnintentionalBurn =
burnSummary !== '' && actualBurnAmount !== '0';
// Get token type
// TODO present the token type in msgs
let parsedTokenType = '';
switch (protocol) {
case 'ALP': {
parsedTokenType = 'ALP';
break;
}
case 'SLP': {
if (number === SLP_1_PROTOCOL_NUMBER) {
parsedTokenType = 'SLP';
} else if (
number === SLP_1_NFT_COLLECTION_PROTOCOL_NUMBER
) {
parsedTokenType = 'NFT Collection';
} else if (number === SLP_1_NFT_PROTOCOL_NUMBER) {
parsedTokenType = 'NFT';
}
break;
}
default: {
parsedTokenType = `${protocol} ${number}`;
break;
}
}
switch (txType) {
case 'GENESIS': {
// Note that NNG chronik provided genesisInfo in this tx
// Now we get it from chronik.token
// Initialize genesisInfo object with tokenId so it can be rendered into a msg later
genesisInfo = { tokenId };
break;
}
case 'SEND': {
if (isUnintentionalBurn) {
tokenBurnInfo = {
tokenId,
undecimalizedTokenBurnAmount: actualBurnAmount,
};
} else {
tokenSendInfo = {
tokenId,
parsedTokenType,
txType,
};
}
break;
}
// TODO handle MINT
default: {
// For now, if we can't parse as above, this will be parsed as an eCash tx (or EMPP)
break;
}
}
}
for (const input of inputs) {
xecSendingOutputScripts.add(input.outputScript);
xecInputAmountSats += input.value;
// The input that sent the token utxos will have key 'slpToken'
if (typeof input.token !== 'undefined') {
// Add amount to undecimalizedTokenInputAmount
// TODO make sure this is for the correct tokenID
// Could have mistakes in parsing ALP txs otherwise
// For now, this is outside the scope of migration
undecimalizedTokenInputAmount =
undecimalizedTokenInputAmount.plus(input.token.amount);
// Collect the input outputScripts to identify change output
tokenSendingOutputScripts.add(input.outputScript);
}
}
// Iterate over outputs to check for OP_RETURN msgs
for (const output of outputs) {
const { value, outputScript } = output;
xecOutputAmountSats += value;
// If this output script is the same as one of the sendingOutputScripts
if (xecSendingOutputScripts.has(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(
outputScript,
(xecReceivingOutputs.get(outputScript) ?? 0) + value,
);
// Increment totalSatsSent
totalSatsSent += value;
}
// Don't parse OP_RETURN values of etoken txs, this info is available from chronik
if (
outputScript.startsWith(opReturn.opReturnPrefix) &&
!isTokenTx
) {
opReturnInfo = module.exports.parseOpReturn(
outputScript.slice(2),
);
}
// For etoken send txs, parse outputs for tokenSendInfo object
if (typeof output.token !== 'undefined') {
// TODO handle EMPP and potential token txs with multiple tokens involved
// Check output script to confirm does not match tokenSendingOutputScript
if (tokenSendingOutputScripts.has(outputScript)) {
// change
tokenChangeOutputs.set(
outputScript,
(
tokenChangeOutputs.get(outputScript) ??
new BigNumber(0)
).plus(output.token.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(
outputScript,
(
tokenReceivingOutputs.get(outputScript) ??
new BigNumber(0)
).plus(output.token.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 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 { data } = consumeNextPush(stack);
if (data !== '') {
// 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(data);
}
}
// 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;
}
case opReturn.knownApps.payButton.prefix: {
app = opReturn.knownApps.payButton.app;
// PayButton v0
// https://github.com/Bitcoin-ABC/bitcoin-abc/blob/master/doc/standards/paybutton.md
// <lokad> <OP_0> <data> <nonce>
// The data could be interesting, ignore the rest
if (stackArray.length >= 3) {
// Version byte is at index 1
const payButtonTxVersion = stackArray[1];
if (payButtonTxVersion !== '00') {
msg = `Unsupported version: 0x${payButtonTxVersion}`;
} else {
const dataPush = stackArray[2];
if (dataPush === '00') {
// Per spec, PayButton txs with no data push OP_0 in this position
msg = 'no data';
} else {
// Data is utf8 encoded
msg = prepareStringForTelegramHTML(
Buffer.from(stackArray[2], 'hex').toString(
'utf8',
),
);
}
}
} else {
msg = '[off spec]';
}
break;
}
case opReturn.knownApps.paywall.prefix: {
app = opReturn.knownApps.paywall.app;
// https://github.com/Bitcoin-ABC/bitcoin-abc/blob/master/doc/standards/op_return-prefix-guideline.md
// <lokad> <txid of the article this paywall is paying for>
if (stackArray.length === 2) {
const articleTxid = stackArray[1];
if (
typeof articleTxid === 'undefined' ||
articleTxid.length !== 64
) {
msg = `Invalid paywall article txid`;
} else {
msg = `<a href="${config.blockExplorer}/tx/${articleTxid}">Article paywall payment</a>`;
}
} else {
msg = '[off spec paywall payment]';
}
break;
}
case opReturn.knownApps.authentication.prefix: {
app = opReturn.knownApps.authentication.app;
// https://github.com/Bitcoin-ABC/bitcoin-abc/blob/master/doc/standards/op_return-prefix-guideline.md
// <lokad> <authentication identifier>
if (stackArray.length === 2) {
const authenticationHex = stackArray[1];
if (authenticationHex === '00') {
msg = `Invalid eCashChat authentication identifier`;
} else {
msg = 'eCashChat authentication via dust tx';
}
} else {
msg = '[off spec eCashChat authentication]';
}
break;
}
default: {
// If you do not recognize the protocol identifier, just print the pushes in hex
// If it is an app or follows a pattern, can be added later
app = 'unknown';
if (containsOnlyPrintableAscii(stackArray.join(''))) {
msg = prepareStringForTelegramHTML(
Buffer.from(stackArray.join(''), 'hex').toString(
'ascii',
),
);
} else {
// If you have non-ascii characters, print each push as a hex number
msg = '';
for (let i = 0; i < stackArray.length; i += 1) {
msg += `0x${stackArray[i]} `;
}
// Remove the last space
msg = msg.slice(0, -1);
// Trim the msg for Telegram to avoid 200+ char msgs
const unknownMaxChars = 20;
if (msg.length > unknownMaxChars) {
msg = msg.slice(0, unknownMaxChars) + '...';
}
}
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 = '';
// Create a stack to use ecash-script consume function
// Note: slp2 parsing is not standard op_return parsing, varchar bytes just use a one-byte push
// So, you can use the 'consume' function of ecash-script, but not consumeNextPush
let stack = { remainingHex: slpTwoPush };
// 1.3: Read token type
// For now, this can only be 00. If not 00, unknown
const tokenType = consume(stack, 1);
if (tokenType !== '00') {
msg += 'Unknown token type|';
}
// 1.4: Read section type
// These are custom varchar per slp2 spec
// <varchar byte hex> <section type>
const sectionBytes = parseInt(consume(stack, 1), 16);
// Note: these are encoded with push data, so you can use ecash-script
const sectionType = Buffer.from(
consume(stack, sectionBytes),
'hex',
).toString('utf8');
msg += sectionType;
// Parsing differs depending on section type
// Note that SEND and MINT have same parsing
const TOKEN_ID_BYTES = 32;
switch (sectionType) {
case 'SEND':
case 'MINT': {
// Next up is tokenId
const tokenId = swapEndianness(consume(stack, TOKEN_ID_BYTES));
const cachedTokenInfo = cachedTokenInfoMap.get(tokenId);
msg += `|<a href="${config.blockExplorer}/tx/${tokenId}">${
typeof cachedTokenInfo === 'undefined'
? `${tokenId.slice(0, 3)}...${tokenId.slice(-3)}`
: prepareStringForTelegramHTML(
cachedTokenInfo.tokenTicker,
)
}</a>`;
const numOutputs = consume(stack, 1);
// Iterate over number of outputs to get total amount sent
// Note: this should be handled with an indexer, as we are not parsing for validity here
// However, it's still useful information for the herald
let totalAmountSent = 0;
for (let i = 0; i < numOutputs; i += 1) {
totalAmountSent += parseInt(
swapEndianness(consume(stack, 6)),
);
}
msg +=
typeof cachedTokenInfo === 'undefined'
? ''
: `|${bigNumberAmountToLocaleString(
totalAmountSent.toString(),
cachedTokenInfo.decimals,
)}`;
break;
}
case 'GENESIS': {
// TODO
// Have not seen one of these in the wild yet
break;
}
case 'BURN': {
// TODO
// Have seen some in the wild but not in spec
break;
}
}
// 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`;
}
// Test for msgs that are intended for non-XEC audience
if (msg.includes('BCH')) {
msg = `[check memo.cash for msg]`;
}
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, staker, 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 = [];
let cashtabTokenRewards = 0;
let cashtabXecRewardTxs = 0;
let cashtabXecRewardsTotalXec = 0;
const tokenSendTxTgMsgLines = [];
const tokenBurnTxTgMsgLines = [];
const opReturnTxTgMsgLines = [];
let xecSendTxTgMsgLines = [];
// We do not get that much newsworthy value from a long list of individual token send txs
// So, we organize token send txs by tokenId
const tokenSendTxMap = new Map();
// 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 && tokenInfoMap) {
// The txid of a genesis tx is the tokenId
const tokenId = txid;
const genesisInfoForThisToken = tokenInfoMap.get(tokenId);
let { tokenTicker, tokenName, tokenDocumentUrl } =
genesisInfoForThisToken;
// 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.payButton.app: {
appEmoji = emojis.payButton;
break;
}
case opReturn.knownApps.paywall.app: {
appEmoji = emojis.paywall;
break;
}
case opReturn.knownApps.authentication.app: {
appEmoji = emojis.authentication;
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, tokenChangeOutputs, tokenReceivingOutputs } =
tokenSendInfo;
// Special handling for Cashtab rewards
if (
// CACHET token id
tokenId ===
'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1' &&
// outputScript of token-server
xecSendingOutputScripts.values().next().value ===
TOKEN_SERVER_OUTPUTSCRIPT
) {
cashtabTokenRewards += 1;
// No further parsing for this tx
continue;
}
// See if you already have info for txs from this token
const tokenSendTxInfo = tokenSendTxMap.get(tokenId);
if (typeof tokenSendTxInfo === 'undefined') {
// We don't have any other txs for this token, initialize an info object
// 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 token outputs (could be receiving or change depending on tx type)
let tokenOutputs =
tokenReceivingOutputs.size === 0
? tokenChangeOutputs
: tokenReceivingOutputs;
let undecimalizedTokenReceivedAmount = new BigNumber(0);
for (const tokenReceivedAmount of tokenOutputs.values()) {
undecimalizedTokenReceivedAmount =
undecimalizedTokenReceivedAmount.plus(
tokenReceivedAmount,
);
}
tokenSendTxMap.set(tokenId, {
sendTxs: 1,
tokenName,
tokenTicker,
decimals,
undecimalizedTokenReceivedAmount,
});
} else {
// We do have other txs for this token, increment the tx count and amount sent
// Initialize token outputs (could be receiving or change depending on tx type)
let tokenOutputs =
tokenReceivingOutputs.size === 0
? tokenChangeOutputs
: tokenReceivingOutputs;
let undecimalizedTokenReceivedAmount = new BigNumber(0);
for (const tokenReceivedAmount of tokenOutputs.values()) {
undecimalizedTokenReceivedAmount =
undecimalizedTokenReceivedAmount.plus(
tokenReceivedAmount,
);
}
tokenSendTxMap.set(tokenId, {
...tokenSendTxInfo,
sendTxs: tokenSendTxInfo.sendTxs + 1,
undecimalizedTokenReceivedAmount:
tokenSendTxInfo.undecimalizedTokenReceivedAmount.plus(
undecimalizedTokenReceivedAmount,
),
});
}
// This parsed tx has info needed to build 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, 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(
xecSendingOutputScripts.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
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;
if (firstXecSendingOutputScript === TOKEN_SERVER_OUTPUTSCRIPT) {
cashtabXecRewardTxs += 1;
cashtabXecRewardsTotalXec += totalSatsSent;
continue;
}
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}">${displayedSentAmount} for ${displayedTxFee}</a>${
xecSenderEmoji !== ''
? ` ${xecSenderEmoji} ${
xecSendingOutputScripts.size > 1
? `${xecSendingOutputScripts.size} addresses`
: returnAddressPreview(
cashaddr.encodeOutputScript(
xecSendingOutputScripts
.values()
.next().value,
),
)
} ${config.emojis.arrowRight} ${
xecSendingOutputScripts.size > 1
? 'themselves'
: 'itself'
}`
: ''
}`;
} else {
xecSendMsg = `${emojis.xecSend}<a href="${
config.blockExplorer
}/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}`,
);
// Halving countdown
const HALVING_HEIGHT = 840000;
const blocksLeft = HALVING_HEIGHT - height;
if (blocksLeft > 0) {
// countdown
tgMsg.push(
`⏰ ${blocksLeft.toLocaleString('en-US')} block${
blocksLeft !== 1 ? 's' : ''
} until eCash halving`,
);
}
if (height === HALVING_HEIGHT) {
tgMsg.push(`🎉🎉🎉 eCash block reward reduced by 50% 🎉🎉🎉`);
}
// Staker
// Staking rewards to <staker>
if (staker) {
// Get fiat amount of staking rwds
tgMsg.push(
`${emojis.staker}${satsToFormattedValue(
staker.reward,
coingeckoPrices,
)} to <a href="${config.blockExplorer}/address/${
staker.staker
}">${returnAddressPreview(staker.staker)}</a>`,
);
}
// 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);
}
// Cashtab rewards
if (cashtabTokenRewards > 0 || cashtabXecRewardTxs > 0) {
tgMsg.push('');
tgMsg.push(`<a href="https://cashtab.com/">Cashtab</a>`);
if (cashtabTokenRewards > 0) {
// 1 CACHET reward:
// or
// <n> CACHET rewards:
tgMsg.push(
`<b>${cashtabTokenRewards}</b> <a href="${
config.blockExplorer
}/tx/aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1">CACHET</a> reward${
cashtabTokenRewards > 1 ? `s` : ''
}`,
);
}
// Cashtab XEC rewards
if (cashtabXecRewardTxs > 0) {
// 1 new user received 42 XEC
// or
// <n> new users received <...>
tgMsg.push(
`<b>${cashtabXecRewardTxs}</b> new user${
cashtabXecRewardTxs > 1 ? `s` : ''
} received <b>${satsToFormattedValue(
cashtabXecRewardsTotalXec,
)}</b>`,
);
}
}
if (tokenSendTxMap.size > 0) {
// eToken Send txs
// Line break for new section
tgMsg.push('');
// We include a 1-line summary for token send txs for each token ID
tokenSendTxMap.forEach((tokenSendInfo, tokenId) => {
const {
sendTxs,
tokenName,
tokenTicker,
decimals,
undecimalizedTokenReceivedAmount,
} = tokenSendInfo;
// Get decimalized receive amount
const decimalizedTokenReceivedAmount =
bigNumberAmountToLocaleString(
undecimalizedTokenReceivedAmount.toString(),
decimals,
);
tgMsg.push(
`${sendTxs} tx${
sendTxs > 1 ? `s` : ''
} sent ${decimalizedTokenReceivedAmount} <a href="${
config.blockExplorer
}/tx/${tokenId}">${tokenName} (${tokenTicker})</a>`,
);
});
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);
},
/**
* Guess the reason why an block was invalidated by avalanche
* @param {ChronikClient} chronik
* @param {number} blockHeight
* @param {object} coinbaseData
* @param {object} memoryCache
* @returns {string} reason
*/
guessRejectReason: async function (
chronik,
blockHeight,
coinbaseData,
memoryCache,
) {
// Let's guess the reject reason by looking for the common cases in order:
// 1. Missing the miner fund output
// 2. Missing the staking reward output
// 3. Wrong staking reward winner
// 4. Normal orphan (another block exists at the same height)
// 5. RTT rejection
if (typeof coinbaseData === 'undefined') {
return undefined;
}
// 1. Missing the miner fund output
// This output is a constant so it's easy to look for
let hasMinerFundOuptut = false;
for (let i = 0; i < coinbaseData.outputs.length; i += 1) {
if (
coinbaseData.outputs[i].outputScript === minerFundOutputScript
) {
hasMinerFundOuptut = true;
break;
}
}
if (!hasMinerFundOuptut) {
return 'missing miner fund output';
}
// 2. Missing the staking reward output
// We checked for missing miner fund output already, so if there are
// fewer than 3 outputs we are sure the staking reward is missing
if (coinbaseData.outputs.length < 3) {
return 'missing staking reward output';
}
// 3. Wrong staking reward winner
const expectedWinner = await memoryCache.get(`${blockHeight}`);
// We might have failed to fetch the expected winner for this block, in
// which case we can't determine if staking reward is the likely cause.
if (typeof expectedWinner !== 'undefined') {
const { address, scriptHex } = expectedWinner;
let stakingRewardOutputIndex = -1;
for (let i = 0; i < coinbaseData.outputs.length; i += 1) {
if (coinbaseData.outputs[i].outputScript === scriptHex) {
stakingRewardOutputIndex = i;
break;
}
}
// We didn't find the expected staking reward output
if (stakingRewardOutputIndex < 0) {
const wrongWinner = module.exports.getStakerFromCoinbaseTx(
blockHeight,
coinbaseData.outputs,
);
if (wrongWinner !== false) {
// Try to show the eCash address and fallback to script hex
// if it is not possible.
if (typeof address !== 'undefined') {
try {
const wrongWinnerAddress =
cashaddr.encodeOutputScript(wrongWinner.staker);
return `wrong staking reward payout (${wrongWinnerAddress} instead of ${address})`;
} catch (err) {
// Fallthrough
}
}
return `wrong staking reward payout (${wrongWinner.staker} instead of ${scriptHex})`;
}
}
}
// 4. Normal orphan (another block exists at the same height)
// If chronik returns a block at the same height, assume it orphaned
// the current invalidated block. It's very possible the block is not
// finalized yet so we have no better way to check it's actually what
// happened.
try {
const blockAtSameHeight = await chronik.block(blockHeight);
return `orphaned by block ${blockAtSameHeight.blockInfo.hash}`;
} catch (err) {
// Block not found, keep guessing
}
// 5. RTT rejection
// FIXME There is currently no way to determine if the block was
// rejected due to RTT violation.
return 'unknown';
},
/**
* Summarize an arbitrary array of chronik txs
* Different logic vs "per block" herald msgs, as we are looking to
* get meaningful info from more txs
* We are interested in what txs were like over a certain time period
* Not details of a particular block
*
* TODO
* Biggest tx
* Highest fee
* Token dex volume
* Biggest token sales
* Whale alerts
*
* @param {number} now unix timestamp in seconds
* @param {Tx[]} txs array of CONFIRMED Txs
* @param {number} xecPriceUsd
* @param {number} blockheight height of the most recently finalized block
*/
summarizeTxHistory: function (now, txs, xecPriceUsd) {
// Throw out any unconfirmed txs
txs.filter(tx => tx.block !== 'undefined');
// Sort by blockheight
txs.sort((a, b) => a.block.height - b.block.height);
const txCount = txs.length;
// Get covered blocks
// Note we add 1 as we include the block at index 0
const blockCount =
txs[txCount - 1].block.height - txs[0].block.height + 1;
// Initialize objects useful for summarizing data
// miner => blocks found
const minerMap = new Map();
// miner pools where we can parse individual miners
let viaBtcBlocks = 0;
const viabtcMinerMap = new Map();
// stakerOutputScript => {count, reward}
const stakerMap = new Map();
// TODO more info about send txs
// inputs[0].outputScript => {count, satoshisSent}
// senderMap
// lokad name => count
- const opReturnMap = new Map();
+ const appTxMap = new Map();
let totalStakingRewardSats = 0;
let cashtabXecRewardCount = 0;
let cashtabXecRewardSats = 0;
let cashtabCachetRewardCount = 0;
let binanceWithdrawalCount = 0;
let binanceWithdrawalSats = 0;
let tokenTxs = 0;
- let opReturnTxs = 0;
+ let appTxs = 0;
+ let unknownLokadTxs = 0;
for (const tx of txs) {
const { inputs, outputs, block, tokenEntries, isCoinbase } = tx;
if (isCoinbase) {
// Coinbase tx - get miner and staker info
const miner = module.exports.getMinerFromCoinbaseTx(
tx.inputs[0].inputScript,
outputs,
miners,
);
if (miner.includes('ViaBTC')) {
viaBtcBlocks += 1;
// ViaBTC pool miner
let blocksFoundThisViaMiner = viabtcMinerMap.get(miner);
if (typeof blocksFoundThisViaMiner === 'undefined') {
viabtcMinerMap.set(miner, 1);
} else {
viabtcMinerMap.set(miner, blocksFoundThisViaMiner + 1);
}
} else {
// Other miner
let blocksFoundThisMiner = minerMap.get(miner);
if (typeof blocksFoundThisMiner === 'undefined') {
minerMap.set(miner, 1);
} else {
minerMap.set(miner, blocksFoundThisMiner + 1);
}
}
const stakerInfo = module.exports.getStakerFromCoinbaseTx(
block.height,
outputs,
);
if (stakerInfo) {
// The coinbase tx may have no staker
// In thise case, we do not have any staking info to update
const { staker, reward } = stakerInfo;
totalStakingRewardSats += reward;
let stakingRewardsThisStaker = stakerMap.get(staker);
if (typeof stakingRewardsThisStaker === 'undefined') {
stakerMap.set(staker, { count: 1, reward });
} else {
stakingRewardsThisStaker.reward += reward;
stakingRewardsThisStaker.count += 1;
}
}
// No further analysis for this tx
continue;
}
const senderOutputScript = inputs[0].outputScript;
if (senderOutputScript === TOKEN_SERVER_OUTPUTSCRIPT) {
// If this tx was sent by token-server
if (tokenEntries.length > 0) {
// We assume all token txs sent by token-server are CACHET rewards
// CACHET reward
cashtabCachetRewardCount += 1;
} else {
// XEC rwd
cashtabXecRewardCount += 1;
for (const output of outputs) {
const { value, outputScript } = output;
if (outputScript !== TOKEN_SERVER_OUTPUTSCRIPT) {
cashtabXecRewardSats += value;
}
}
}
// No further analysis for this tx
continue;
}
if (senderOutputScript === BINANCE_OUTPUTSCRIPT) {
// Tx sent by Binance
// Make sure it's not just a utxo consolidation
for (const output of outputs) {
const { value, outputScript } = output;
if (outputScript !== BINANCE_OUTPUTSCRIPT) {
// If we have an output that is not sending to the binance hot wallet
// Increment total value amount withdrawn
binanceWithdrawalSats += value;
// We also call this a withdrawal
// Note that 1 tx from the hot wallet may include more than 1 withdrawal
binanceWithdrawalCount += 1;
}
}
}
// Other token actions
if (tokenEntries.length > 0) {
tokenTxs += 1;
continue;
/**
* Token tx
*
* Possibilities
*
* GENESIS, SEND, MINT, BURN
* agora list, agora ad prep, agora buy
*
* So first, check if it is agora-related or not
*
* ad prep example
* https://explorer.e.cash/tx/e7ca2e9e9c778f130206520eebfa7244c300ca95e90284782ed54a8b376406da
*
* listing example
* https://explorer.e.cash/tx/d142c4d3ee2fc09bca13314f9b5b1476b6bc3806f6db989b003e2d57a96c6cc5
*/
// Burn, mint, or genesis
// send -- if output size is not dust and it's to a p2sh, call it ad prep tx
// if output size dust and to a p2sh, call it a listing (mb a partial buy?)
}
const firstOutputScript = outputs[0].outputScript;
- if (firstOutputScript.startsWith('6a')) {
- const { app } = module.exports.parseOpReturn(
- firstOutputScript.slice(2),
+ const LOKAD_OPRETURN_STARTSWITH = '6a04';
+ if (firstOutputScript.startsWith(LOKAD_OPRETURN_STARTSWITH)) {
+ appTxs += 1;
+ // We only parse minimally-pushed lokad ids
+
+ // Get the lokadId (the 4-byte first push)
+ const lokadId = firstOutputScript.slice(4, 12);
+
+ // Add to map
+ const countThisLokad = appTxMap.get(lokadId);
+ appTxMap.set(
+ lokadId,
+ typeof countThisLokad === 'undefined'
+ ? 1
+ : countThisLokad + 1,
);
- if (app === 'unknown') {
- // See if there is a lokad
- }
- opReturnTxs += 1;
- let appTxCount = opReturnMap.get(app);
- if (typeof appTxCount === 'undefined') {
- opReturnMap.set(app, 1);
- } else {
- opReturnMap.set(app, appTxCount + 1);
- }
}
}
// Add ViaBTC as a single entity to minerMap
minerMap.set(`ViaBTC`, viaBtcBlocks);
// Sort miner map by blocks found
const sortedMinerMap = new Map(
[...minerMap.entries()].sort(
(keyValueArrayA, keyValueArrayB) =>
keyValueArrayB[1] - keyValueArrayA[1],
),
);
const sortedStakerMap = new Map(
[...stakerMap.entries()].sort(
(keyValueArrayA, keyValueArrayB) =>
keyValueArrayB[1].count - keyValueArrayA[1].count,
),
);
// Build your msg
const tgMsg = [];
tgMsg.push(
`<b>24 hours thru ${new Date(now * 1000).toLocaleTimeString(
'en-GB',
{
year: 'numeric',
month: 'short',
day: 'numeric',
hour12: false,
hour: '2-digit',
minute: '2-digit',
timeZone: 'UTC',
},
)}</b>`,
);
- tgMsg.push(`${config.emojis.block}${blockCount} blocks`);
+ tgMsg.push(
+ `${config.emojis.block}${blockCount.toLocaleString(
+ 'en-US',
+ )} blocks`,
+ );
tgMsg.push(
`${config.emojis.arrowRight}${txs.length.toLocaleString(
'en-US',
)} txs`,
);
tgMsg.push('');
// Top miners
const MINERS_TO_SHOW = 3;
tgMsg.push(
`<b><i>${config.emojis.miner}${sortedMinerMap.size} miners found blocks</i></b>`,
);
tgMsg.push(`<u>Top ${MINERS_TO_SHOW}</u>`);
const topMiners = [...sortedMinerMap.entries()].slice(
0,
MINERS_TO_SHOW,
);
for (let i = 0; i < topMiners.length; i += 1) {
const count = topMiners[i][1];
const pct = (100 * (count / blockCount)).toFixed(0);
tgMsg.push(
`${i + 1}. ${topMiners[i][0]}, ${count} <i>(${pct}%)</i>`,
);
}
tgMsg.push('');
const SATOSHIS_PER_XEC = 100;
const totalStakingRewardsXec =
totalStakingRewardSats / SATOSHIS_PER_XEC;
const renderedTotalStakingRewards =
typeof xecPriceUsd !== 'undefined'
? `$${(totalStakingRewardsXec * xecPriceUsd).toLocaleString(
'en-US',
{
minimumFractionDigits: 0,
maximumFractionDigits: 0,
},
)}`
: `${totalStakingRewardsXec.toLocaleString('en-US', {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
})} XEC`;
// Top stakers
const STAKERS_TO_SHOW = 3;
tgMsg.push(
`<b><i>${config.emojis.staker}${sortedStakerMap.size} stakers earned ${renderedTotalStakingRewards}</i></b>`,
);
tgMsg.push(`<u>Top ${STAKERS_TO_SHOW}</u>`);
const topStakers = [...sortedStakerMap.entries()].slice(
0,
STAKERS_TO_SHOW,
);
for (let i = 0; i < topStakers.length; i += 1) {
const staker = topStakers[i];
const count = staker[1].count;
const pct = (100 * (count / blockCount)).toFixed(0);
const addr = cashaddr.encodeOutputScript(staker[0]);
tgMsg.push(
`${i + 1}. ${`<a href="${
config.blockExplorer
}/address/${addr}">${returnAddressPreview(addr)}</a>`}, ${
staker[1].count
} <i>(${pct}%)</i>`,
);
}
// Tx breakdown
// Cashtab rewards
if (cashtabXecRewardCount > 0 || cashtabCachetRewardCount > 0) {
tgMsg.push('');
tgMsg.push(`<a href="https://cashtab.com/">Cashtab</a>`);
// Cashtab XEC rewards
if (cashtabXecRewardCount > 0) {
// 1 new user received 42 XEC
// or
// <n> new users received <...>
tgMsg.push(
`${
config.emojis.gift
} <b>${cashtabXecRewardCount}</b> new user${
cashtabXecRewardCount > 1 ? `s` : ''
} received <b>${satsToFormattedValue(
cashtabXecRewardSats,
)}</b>`,
);
}
if (cashtabCachetRewardCount > 0) {
// 1 CACHET reward:
// or
// <n> CACHET rewards:
tgMsg.push(
`${
config.emojis.tokenSend
} <b>${cashtabCachetRewardCount}</b> <a href="${
config.blockExplorer
}/tx/aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1">CACHET</a> reward${
cashtabCachetRewardCount > 1 ? `s` : ''
}`,
);
}
tgMsg.push('');
}
- const cashFusionTxs = opReturnMap.get('CashFusion');
- if (typeof cashFusionTxs !== 'undefined') {
- tgMsg.push(
- `${config.emojis.fusion} <b>${cashFusionTxs.toLocaleString(
- 'en-US',
- )}</b> CashFusion tx${cashtabXecRewardCount > 1 ? 's' : ''}`,
- );
- }
-
if (tokenTxs > 0) {
tgMsg.push(
`${config.emojis.token} <b>${tokenTxs.toLocaleString(
'en-US',
)}</b> token tx${tokenTxs > 1 ? 's' : ''}`,
);
+ tgMsg.push('');
}
- if (opReturnTxs > 0) {
+ if (appTxs > 0) {
+ // Sort appTxMap by most common app txs
+ const sortedAppTxMap = new Map(
+ [...appTxMap.entries()].sort(
+ (keyValueArrayA, keyValueArrayB) =>
+ keyValueArrayB[1] - keyValueArrayA[1],
+ ),
+ );
tgMsg.push(
- `${config.emojis.app} <b>${opReturnTxs.toLocaleString(
+ `${config.emojis.app} <b><i>${appTxs.toLocaleString(
'en-US',
- )}</b> app tx${opReturnTxs > 1 ? 's' : ''}`,
+ )} app tx${appTxs > 1 ? 's' : ''}</i></b>`,
);
+ sortedAppTxMap.forEach((count, lokadId) => {
+ // Do we recognize this app?
+ const supportedLokadApp = lokadMap.get(lokadId);
+ if (typeof supportedLokadApp === 'undefined') {
+ unknownLokadTxs += count;
+ // Go to the next lokadId
+ return;
+ }
+ const { name, emoji, url } = supportedLokadApp;
+ if (typeof url === 'undefined') {
+ tgMsg.push(
+ `${emoji} <b>${count.toLocaleString(
+ 'en-US',
+ )}</b> ${name}${count > 1 ? 's' : ''}`,
+ );
+ } else {
+ tgMsg.push(
+ `${emoji} <b>${count.toLocaleString(
+ 'en-US',
+ )}</b> <a href="${url}">${name}${
+ count > 1 ? 's' : ''
+ }</a>`,
+ );
+ }
+ });
+ // Add line for unknown txs
+ if (unknownLokadTxs > 0) {
+ tgMsg.push(
+ `${
+ config.emojis.unknown
+ } <b>${unknownLokadTxs.toLocaleString(
+ 'en-US',
+ )}</b> Unknown app tx${unknownLokadTxs > 1 ? 's' : ''}`,
+ );
+ }
+ tgMsg.push('');
}
- tgMsg.push('');
-
if (binanceWithdrawalCount > 0) {
// Binance hot wallet
const binanceWithdrawalXec =
binanceWithdrawalSats / SATOSHIS_PER_XEC;
const renderedBinanceWithdrawalSats =
typeof xecPriceUsd !== 'undefined'
? `$${(binanceWithdrawalXec * xecPriceUsd).toLocaleString(
'en-US',
{
minimumFractionDigits: 0,
maximumFractionDigits: 0,
},
)}`
: `${binanceWithdrawalXec.toLocaleString('en-US', {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
})} XEC`;
tgMsg.push(`${config.emojis.bank} <b><i>Binance</i></b>`);
tgMsg.push(
`<b>${binanceWithdrawalCount}</b> withdrawal${
binanceWithdrawalCount > 1 ? 's' : ''
}, ${renderedBinanceWithdrawalSats}`,
);
}
return splitOverflowTgMsg(tgMsg);
},
};
diff --git a/apps/ecash-herald/test/mocks/dailyTxs.js b/apps/ecash-herald/test/mocks/dailyTxs.js
index a88a6f7d0f..8b4a9c09e2 100644
--- a/apps/ecash-herald/test/mocks/dailyTxs.js
+++ b/apps/ecash-herald/test/mocks/dailyTxs.js
@@ -1,1711 +1,2073 @@
// Copyright (c) 2024 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';
/**
* Instead of mocking 144 blocks of txs, we create an array
* including all tx types covered in the summary
*
* This is NOT a generated file
* However it does not really need to be reviewed
* Txs to be tested are fetched from chronik.tx and manually added here
*/
module.exports = [
// Coinbase tx 1, miner solopool and staker 0cd
// d86e57cc7caacd61ffa742b13ca4d51177d3e4c8dd619124af79dedf0ac51ea1
{
txid: 'd86e57cc7caacd61ffa742b13ca4d51177d3e4c8dd619124af79dedf0ac51ea1',
version: 1,
inputs: [
{
prevOut: {
txid: '0000000000000000000000000000000000000000000000000000000000000000',
outIdx: 4294967295,
},
inputScript:
'0372390d04d8440e6708bd41863a64000000736f6c6f706f6f6c2e6f7267',
value: 0,
sequenceNo: 0,
},
],
outputs: [
{
value: 181272025,
outputScript:
'76a914f4728f398bb962656803346fb4ac45d776041a2e88ac',
},
{
value: 100012151,
outputScript: 'a914d37c4c809fe9840e7bfa77b86bd47163f6fb6c6087',
},
{
value: 31253797,
outputScript:
'76a914197eaf9b9f4b4f038f967c76cf050e3d8f5f872e88ac',
},
],
lockTime: 0,
timeFirstSeen: 0,
size: 181,
isCoinbase: true,
tokenEntries: [],
tokenFailedParsings: [],
tokenStatus: 'TOKEN_STATUS_NON_TOKEN',
block: {
height: 866674,
hash: '00000000000000000d628341dc623fd7f11136558594ec9a39c271c0ac4922e8',
timestamp: 1728988376,
},
},
// Coinbase tx 2, ViaBTC and staker gyg
// 4e70941e1500315302e2262eb86ac11db879eae5f3ff1964a939bdbf9947cfe9
{
txid: '4e70941e1500315302e2262eb86ac11db879eae5f3ff1964a939bdbf9947cfe9',
version: 1,
inputs: [
{
prevOut: {
txid: '0000000000000000000000000000000000000000000000000000000000000000',
outIdx: 4294967295,
},
inputScript:
'0370390d192f5669614254432f4d696e6564206279206d6f6f72646f632f1003e0a10ffd4bebde9cbcad5d8cb90200',
value: 0,
sequenceNo: 4294967295,
},
],
outputs: [
{
value: 181291465,
outputScript:
'76a914f1c075a01882ae0972f95d3a4177c86c852b7d9188ac',
},
{
value: 100022876,
outputScript: 'a914d37c4c809fe9840e7bfa77b86bd47163f6fb6c6087',
},
{
value: 31257149,
outputScript:
'76a914eaac1f0faac136c3091b67f78c4bc8d0f037b94188ac',
},
],
lockTime: 0,
timeFirstSeen: 0,
size: 198,
isCoinbase: true,
tokenEntries: [],
tokenFailedParsings: [],
tokenStatus: 'TOKEN_STATUS_NON_TOKEN',
block: {
height: 866672,
hash: '00000000000000000f43f2da867ec399fe7c2ff3c7406c1f1a16f2d726771009',
timestamp: 1728985449,
},
},
// Coinbase tx 3, Mining-Dutch and staker 2nu
// defa7547931ac9226e387b9fa803e12b632109bec77552d4994def3879fd0297
{
txid: 'defa7547931ac9226e387b9fa803e12b632109bec77552d4994def3879fd0297',
version: 1,
inputs: [
{
prevOut: {
txid: '0000000000000000000000000000000000000000000000000000000000000000',
outIdx: 4294967295,
},
inputScript:
'0367390d04f0150e6708fabe6d6d25608bc631bda8933fe554bb7a50befe9e4139bda920ac0ee937769cec2ffde7000100000000000001b65911900403003201112f4d696e696e672d44757463682f2d3335',
value: 0,
sequenceNo: 0,
},
],
outputs: [
{
value: 181255884,
outputScript:
'76a914a24e2b67689c3753983d3b408bc7690d31b1b74d88ac',
},
{
value: 100003246,
outputScript: 'a914d37c4c809fe9840e7bfa77b86bd47163f6fb6c6087',
},
{
value: 31251014,
outputScript:
'76a914a07b8141956fca49e54b474e5efd894fabab6bbb88ac',
},
],
lockTime: 0,
timeFirstSeen: 0,
size: 233,
isCoinbase: true,
tokenEntries: [],
tokenFailedParsings: [],
tokenStatus: 'TOKEN_STATUS_NON_TOKEN',
block: {
height: 866663,
hash: '00000000000000001ee721175cdcd42f3bcd5f87589757a56ff8c61af8c9442a',
timestamp: 1728976362,
},
},
// Cashtab XEC reward
// cc385ebf5863ca757b441df009d01d773f3d1031d3b3d7af92a7662fbe6b71f6
{
txid: 'cc385ebf5863ca757b441df009d01d773f3d1031d3b3d7af92a7662fbe6b71f6',
version: 2,
inputs: [
{
prevOut: {
txid: '2b8415239c61e9a2c7c90612ed94b5833f661f1ad44a5ae875f6896575b00c98',
outIdx: 1,
},
inputScript:
'41c61f3e6506b6e66be785e1bd91c934252f36ef1345dbeaeeed82cbbc95279ad570c0aa68646e9908e559ebc5306ad323c8fcdd17286564a92f5f03656c32a7a041210353f81d61d41d6e22c73ab449476113dea124afe3972991cd237e654f15950b7c',
value: 94297394,
sequenceNo: 4294967295,
outputScript:
'76a914821407ac2993f8684227004f4086082f3f801da788ac',
},
],
outputs: [
{
value: 4200,
outputScript:
'76a914e54253a422ad52174e6ad25762c318a2aaa921db88ac',
},
{
value: 94292975,
outputScript:
'76a914821407ac2993f8684227004f4086082f3f801da788ac',
spentBy: {
txid: 'd1ae00e81ca781765bf09f12fc8aefbd6a2d950781be04558ad6a1f92b1e0d26',
outIdx: 1,
},
},
],
lockTime: 0,
timeFirstSeen: 1728987043,
size: 219,
isCoinbase: false,
tokenEntries: [],
tokenFailedParsings: [],
tokenStatus: 'TOKEN_STATUS_NON_TOKEN',
block: {
height: 866674,
hash: '00000000000000000d628341dc623fd7f11136558594ec9a39c271c0ac4922e8',
timestamp: 1728988376,
},
},
// Cashtab CACHET reward
// d391c3ce3e562195fced70b2e75be699e9ea85fcdb742986e5d588f8558670e9
{
txid: 'd391c3ce3e562195fced70b2e75be699e9ea85fcdb742986e5d588f8558670e9',
version: 2,
inputs: [
{
prevOut: {
txid: 'd1ae00e81ca781765bf09f12fc8aefbd6a2d950781be04558ad6a1f92b1e0d26',
outIdx: 2,
},
inputScript:
'416492cfb9939f8d08ce8063069242682bfc62832f5a30a8f9cf427a9d1618ca0c72d53a803667e7fc8de07c393d40a58146b34bc0f0a09f36da5cfa32c45456df41210353f81d61d41d6e22c73ab449476113dea124afe3972991cd237e654f15950b7c',
value: 546,
sequenceNo: 4294967295,
token: {
tokenId:
'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1',
tokenType: {
protocol: 'SLP',
type: 'SLP_TOKEN_TYPE_FUNGIBLE',
number: 1,
},
amount: '454260000',
isMintBaton: false,
entryIdx: 0,
},
outputScript:
'76a914821407ac2993f8684227004f4086082f3f801da788ac',
},
{
prevOut: {
txid: 'd1ae00e81ca781765bf09f12fc8aefbd6a2d950781be04558ad6a1f92b1e0d26',
outIdx: 3,
},
inputScript:
'41128001cde47289dda3455e0c28b85bfd18775ff44b6effee197f7f4c09b9418d1cab4719dc4e3ace8c28a79e3ced43b82b7d8088f949ad1d69f0228048f3046141210353f81d61d41d6e22c73ab449476113dea124afe3972991cd237e654f15950b7c',
value: 94291962,
sequenceNo: 4294967295,
outputScript:
'76a914821407ac2993f8684227004f4086082f3f801da788ac',
},
],
outputs: [
{
value: 0,
outputScript:
'6a04534c500001010453454e4420aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb108000000000000271008000000001b134e10',
},
{
value: 546,
outputScript:
'76a914e54253a422ad52174e6ad25762c318a2aaa921db88ac',
token: {
tokenId:
'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1',
tokenType: {
protocol: 'SLP',
type: 'SLP_TOKEN_TYPE_FUNGIBLE',
number: 1,
},
amount: '10000',
isMintBaton: false,
entryIdx: 0,
},
},
{
value: 546,
outputScript:
'76a914821407ac2993f8684227004f4086082f3f801da788ac',
token: {
tokenId:
'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1',
tokenType: {
protocol: 'SLP',
type: 'SLP_TOKEN_TYPE_FUNGIBLE',
number: 1,
},
amount: '454250000',
isMintBaton: false,
entryIdx: 0,
},
spentBy: {
txid: '34039dcccf1366aa9b40265f6c22268a567fafe6812cf3f436cc079a24a85e95',
outIdx: 0,
},
},
{
value: 94290949,
outputScript:
'76a914821407ac2993f8684227004f4086082f3f801da788ac',
spentBy: {
txid: '34039dcccf1366aa9b40265f6c22268a567fafe6812cf3f436cc079a24a85e95',
outIdx: 1,
},
},
],
lockTime: 0,
timeFirstSeen: 1728987164,
size: 467,
isCoinbase: false,
tokenEntries: [
{
tokenId:
'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1',
tokenType: {
protocol: 'SLP',
type: 'SLP_TOKEN_TYPE_FUNGIBLE',
number: 1,
},
txType: 'SEND',
isInvalid: false,
burnSummary: '',
failedColorings: [],
actualBurnAmount: '0',
intentionalBurn: '0',
burnsMintBatons: false,
},
],
tokenFailedParsings: [],
tokenStatus: 'TOKEN_STATUS_NORMAL',
block: {
height: 866674,
hash: '00000000000000000d628341dc623fd7f11136558594ec9a39c271c0ac4922e8',
timestamp: 1728988376,
},
},
+ // App txs
+ // eCashChat
+ // e40fec30e3e9854553b3c9e5b68cd431722e45ff00366136e77c8f457ebd7d90
+ {
+ txid: 'e40fec30e3e9854553b3c9e5b68cd431722e45ff00366136e77c8f457ebd7d90',
+ version: 2,
+ inputs: [
+ {
+ prevOut: {
+ txid: '8db67429be0cd65c002e8898bae4712b3098bb68a9cd578e18660fad07a784b2',
+ outIdx: 1,
+ },
+ inputScript:
+ '4194fe416801d0ae6f2ed87fcf3e66e74de77ca425040678df43971201f264c7cf01414e773a53be4f36b230fc4c5ee07be6c85073551f5bf3c9a1ae93e85d6c3f41210290b50035060622db41171ef7f9704efd881d4beed12222f807bcc627c94570da',
+ value: 550,
+ sequenceNo: 4294967295,
+ outputScript:
+ '76a91481a14c64a81f0a9c35b17499b355de9856fe1c5c88ac',
+ },
+ {
+ prevOut: {
+ txid: '8db67429be0cd65c002e8898bae4712b3098bb68a9cd578e18660fad07a784b2',
+ outIdx: 2,
+ },
+ inputScript:
+ '41a59a9d4eb3449ef6ef8c3a99256711022b4cf24aec72e8ce7a132ba01428a27d2e671f0c38adfe36a9011556b2859fb7dd5bcf3518eab66474f633c079de1e5a41210290b50035060622db41171ef7f9704efd881d4beed12222f807bcc627c94570da',
+ value: 49493,
+ sequenceNo: 4294967295,
+ outputScript:
+ '76a91481a14c64a81f0a9c35b17499b355de9856fe1c5c88ac',
+ },
+ ],
+ outputs: [
+ {
+ value: 0,
+ outputScript:
+ '6a04636861740468617368203130294bfa5ba853cc17d90afe4ae7a2df7de011ed8713ed7eb90bf016cbb6083065636173683a717a71367a6e727934713073343870346b3936666e7636346d367639646c737574736536686d65783765',
+ },
+ {
+ value: 550,
+ outputScript:
+ '76a91470a784633e942b7e1c9947255910c8132623225c88ac',
+ },
+ {
+ value: 48564,
+ outputScript:
+ '76a91481a14c64a81f0a9c35b17499b355de9856fe1c5c88ac',
+ },
+ ],
+ lockTime: 0,
+ timeFirstSeen: 1729617077,
+ size: 462,
+ isCoinbase: false,
+ tokenEntries: [],
+ tokenFailedParsings: [],
+ tokenStatus: 'TOKEN_STATUS_NON_TOKEN',
+ block: {
+ height: 867696,
+ hash: '00000000000000001bb60e61a63510639c0a7117cef60589f1cbb591b2ac8bd4',
+ timestamp: 1729618823,
+ },
+ },
+ // Articles and Replies (eCashChat)
+ // cff787e0134a39f79378fcdd67b6b0145a630a432ee70758fc74b10fdffe5b39
+ {
+ txid: 'cff787e0134a39f79378fcdd67b6b0145a630a432ee70758fc74b10fdffe5b39',
+ version: 2,
+ inputs: [
+ {
+ prevOut: {
+ txid: '80661ce76d82ece424ffc25804735000d39365a9075dfdc69d4573ff99b22faa',
+ outIdx: 1,
+ },
+ inputScript:
+ '4148e8628c19bd048ba199221ead5dc7f43fb50385cc4190bc556f2a0229b153945e653cfe49072566bc13d1f5b77ef9aa6ef8dcc70f112e57ca1ab0ad6d2273f44121030a06dd7429d8fce700b702a55a012a1f9d1eaa46825bde2d31252ee9cb30e536',
+ value: 25000,
+ sequenceNo: 4294967295,
+ outputScript:
+ '76a91414582d09f61c6580b8a2b6c8af8d6a13c9128b6f88ac',
+ },
+ ],
+ outputs: [
+ {
+ value: 0,
+ outputScript:
+ '6a04626c6f674c6c3938383164646139616530613065623038373238323236303638663231353138653530623630636453756e204f637420313320323032342032313a31343a303220474d542b3131303020284175737472616c69616e204561737465726e204461796c696768742054696d6529',
+ },
+ {
+ value: 550,
+ outputScript:
+ '76a91414582d09f61c6580b8a2b6c8af8d6a13c9128b6f88ac',
+ spentBy: {
+ txid: 'd8372db66496105919611a623c211cd39032f312222c075c086e7d90d98dca92',
+ outIdx: 3,
+ },
+ },
+ {
+ value: 23758,
+ outputScript:
+ '76a91414582d09f61c6580b8a2b6c8af8d6a13c9128b6f88ac',
+ spentBy: {
+ txid: 'd8372db66496105919611a623c211cd39032f312222c075c086e7d90d98dca92',
+ outIdx: 4,
+ },
+ },
+ ],
+ lockTime: 0,
+ timeFirstSeen: 1728814492,
+ size: 344,
+ isCoinbase: false,
+ tokenEntries: [],
+ tokenFailedParsings: [],
+ tokenStatus: 'TOKEN_STATUS_NON_TOKEN',
+ block: {
+ height: 866388,
+ hash: '00000000000000001ff6bdcfc2d78ea092a8dcfddfb41122f10c5473eaedb34b',
+ timestamp: 1728814777,
+ },
+ },
+ // Airdrop
+ // 75f29368afe079e58ff950e384249190359137464261065fd8fc329b38051f55
+ {
+ txid: '75f29368afe079e58ff950e384249190359137464261065fd8fc329b38051f55',
+ version: 2,
+ inputs: [
+ {
+ prevOut: {
+ txid: '9cd18a4852a8e900514cfae551f34f441270c484bbb64aab7adbca1c5e9bf42a',
+ outIdx: 0,
+ },
+ inputScript:
+ '41d1e05374149017f7a14f8cb6978350035aaaba5d24723b959cd44c214796964540cdfce6b593ff850f2eb4336174b038b17e7fe8d5e2a3c7b92c6e78d5625b08412103643c1ca0e8480cf7b8be96ea475714c0122f92e4f9a0b22fbbd3bcf0e302d58c',
+ value: 10000,
+ sequenceNo: 4294967295,
+ outputScript:
+ '76a914c8080b00b32aba4977c9a9af56b9d9226f147e1688ac',
+ },
+ ],
+ outputs: [
+ {
+ value: 0,
+ outputScript:
+ '6a0464726f702009c53c9a9fe0df2cb729dd6f99f2b836c59b842d6652becd85658e277caab611',
+ },
+ {
+ value: 1000,
+ outputScript:
+ '76a9143d1a781059c9915716091c90f0be3e77e02b6bba88ac',
+ },
+ {
+ value: 8463,
+ outputScript:
+ '76a914c8080b00b32aba4977c9a9af56b9d9226f147e1688ac',
+ },
+ ],
+ lockTime: 0,
+ timeFirstSeen: 1729568158,
+ size: 267,
+ isCoinbase: false,
+ tokenEntries: [],
+ tokenFailedParsings: [],
+ tokenStatus: 'TOKEN_STATUS_NON_TOKEN',
+ block: {
+ height: 867624,
+ hash: '00000000000000000ce7ec8b675b84c722675df33c48979375a0c652abda52e5',
+ timestamp: 1729570217,
+ },
+ },
+ // Cashtab msg
+ // b4faa2df37c8802a3998df635e28dccd327a04c39502954acf06d56bfd2d0e9f
+ {
+ txid: 'b4faa2df37c8802a3998df635e28dccd327a04c39502954acf06d56bfd2d0e9f',
+ version: 2,
+ inputs: [
+ {
+ prevOut: {
+ txid: 'c5b54449a8881c833ace3511e44fae405f7e6386a24bf74a88e4697fc78be3c6',
+ outIdx: 0,
+ },
+ inputScript:
+ '414979186503256f02393cbf767fa533f7bfabb35ab14114842807bb7bc9e52a0a0ba0d87806449ae35f23c0b6fdb2e2ddf19e7c5594ccaa0ba74bfbcaba28d2d0412102cbce237226bd8bba1a02def66085cfff2427042a98d8bf9590ccced8207d233b',
+ value: 4628,
+ sequenceNo: 4294967295,
+ outputScript:
+ '76a9140f81c58578bd5ecc95a82ed0a89bc7061f0c04b488ac',
+ },
+ ],
+ outputs: [
+ {
+ value: 0,
+ outputScript:
+ '6a04007461621a5468616e6b732062726f2e200a49206170707265636961746520',
+ },
+ {
+ value: 546,
+ outputScript:
+ '76a914a805a320360fa685f83605d8e56de6f9d8a7a99988ac',
+ },
+ {
+ value: 3557,
+ outputScript:
+ '76a9140f81c58578bd5ecc95a82ed0a89bc7061f0c04b488ac',
+ },
+ ],
+ lockTime: 0,
+ timeFirstSeen: 1729582375,
+ size: 261,
+ isCoinbase: false,
+ tokenEntries: [],
+ tokenFailedParsings: [],
+ tokenStatus: 'TOKEN_STATUS_NON_TOKEN',
+ block: {
+ height: 867641,
+ hash: '00000000000000000e5aaa59d2a19e420be897014fc9e03343273e85fdf295a3',
+ timestamp: 1729587484,
+ },
+ },
+ // eCashChat Auth
+ // 27517286f1b7f159da4db93ed96e6be9bb01dad94179ea889211ef32ebb1a3b4
+ {
+ txid: '27517286f1b7f159da4db93ed96e6be9bb01dad94179ea889211ef32ebb1a3b4',
+ version: 2,
+ inputs: [
+ {
+ prevOut: {
+ txid: '2733f060666ce21d0eff1080a71cf168074a8284e2e6fd36ddfae95868dc942b',
+ outIdx: 0,
+ },
+ inputScript:
+ '411f507db40c8fb326008146f0e4d95e172e3e32c817f74e891b357e8a8ba6e21ac6ff0e3a391cf2395dfa8d5168fee9cb8b8d03812d47f29f76482dac8c7175f341210349c0d5569a0a43c4473bdcd11889de00327303007e24972941ccbf3a3dc61ccc',
+ value: 50000,
+ sequenceNo: 4294967295,
+ outputScript:
+ '76a9148b48669ce8873c71698151db0453e6285aeb07c588ac',
+ },
+ ],
+ outputs: [
+ {
+ value: 0,
+ outputScript:
+ '6a046175746814619e0bb30cc23b2879f7b1131a858ea9f0d3d873',
+ },
+ {
+ value: 550,
+ outputScript:
+ '76a914b20298c1b5d6a82a61f6c8cd708fa87a1ce1a97a88ac',
+ },
+ {
+ value: 48937,
+ outputScript:
+ '76a9148b48669ce8873c71698151db0453e6285aeb07c588ac',
+ },
+ ],
+ lockTime: 0,
+ timeFirstSeen: 1729618002,
+ size: 255,
+ isCoinbase: false,
+ tokenEntries: [],
+ tokenFailedParsings: [],
+ tokenStatus: 'TOKEN_STATUS_NON_TOKEN',
+ block: {
+ height: 867696,
+ hash: '00000000000000001bb60e61a63510639c0a7117cef60589f1cbb591b2ac8bd4',
+ timestamp: 1729618823,
+ },
+ },
+ // PayButton
+ // bfce47f2403031f5465982b821e8e14c78deff2dd5986ca0c21cebb5ed946b4d
+ {
+ txid: 'bfce47f2403031f5465982b821e8e14c78deff2dd5986ca0c21cebb5ed946b4d',
+ version: 2,
+ inputs: [
+ {
+ prevOut: {
+ txid: 'b7f22481b53c0c0eb46a61640324c0ef872acc6cb0d5a62a33364d7036995004',
+ outIdx: 2,
+ },
+ inputScript:
+ '41c4a70b96af0dc94bb5cafd6d847ea6b8d7c509b0914a90fbffb35dcabe119c0a61e5cd376baab1128c44c89d09ca98c84ed2e695d706329f170b3a18c6bc08cf4121039f0061726e4fed07061f705d34707b7f9c2f175bfa2ca7fe7df0a81e9efe1e8b',
+ value: 2903745,
+ sequenceNo: 4294967295,
+ outputScript:
+ '76a9142aba37d6365d3e570cadf3ed65e58ae4ad751a3088ac',
+ },
+ ],
+ outputs: [
+ {
+ value: 0,
+ outputScript: '6a045041590000000863a9892c7792fbfd',
+ },
+ {
+ value: 5000,
+ outputScript:
+ '76a914631dde3df001e09c9cfde6c72c8ae02849f2c0c388ac',
+ },
+ {
+ value: 2898252,
+ outputScript:
+ '76a9142aba37d6365d3e570cadf3ed65e58ae4ad751a3088ac',
+ },
+ ],
+ lockTime: 0,
+ timeFirstSeen: 1729364288,
+ size: 245,
+ isCoinbase: false,
+ tokenEntries: [],
+ tokenFailedParsings: [],
+ tokenStatus: 'TOKEN_STATUS_NON_TOKEN',
+ block: {
+ height: 867294,
+ hash: '00000000000000001635b16e32855d303e148a18c3cd8abf76fe4abbdbf3771b',
+ timestamp: 1729364661,
+ },
+ },
+ // Paywall
+ // 77502089ae5f89ef941a3f71360da13efa8e1c8aedf79a637a505ca09065e5e2
+ {
+ txid: '77502089ae5f89ef941a3f71360da13efa8e1c8aedf79a637a505ca09065e5e2',
+ version: 2,
+ inputs: [
+ {
+ prevOut: {
+ txid: '01a8723ddb110adc71c38f236099a28ebc6cfae48fb0db7134fc41a97aed8c96',
+ outIdx: 0,
+ },
+ inputScript:
+ '41fe808a1d825c384785d0e3e37be0f3af10670e075f61be1c12cd14ed89ae7f01a3af3a21ff1931326058df7cb530b84a430ac9dc65ee73734494b2010f682bc14121031febf12ea22d33cb6da0599c3fcb29b80c88efaf16c1c024192f9c2e03ce4675',
+ value: 18500,
+ sequenceNo: 4294967295,
+ outputScript:
+ '76a914c2b4edba79887da00c8022187195caf7da6ef03788ac',
+ },
+ ],
+ outputs: [
+ {
+ value: 0,
+ outputScript:
+ '6a0470617977204999ccb611ae9d9c28b06215d42e8695cf5230db8c21707b0d36268bc68ffc76',
+ },
+ {
+ value: 550,
+ outputScript:
+ '76a914c4d76949bd98d3a2f7d0b63322c1d4a5c2139a8b88ac',
+ },
+ {
+ value: 17413,
+ outputScript:
+ '76a914c2b4edba79887da00c8022187195caf7da6ef03788ac',
+ },
+ ],
+ lockTime: 0,
+ timeFirstSeen: 1729623047,
+ size: 267,
+ isCoinbase: false,
+ tokenEntries: [],
+ tokenFailedParsings: [],
+ tokenStatus: 'TOKEN_STATUS_NON_TOKEN',
+ block: {
+ height: 867718,
+ hash: '000000000000000008e954e690fa6b69077938de8bc0c22997830c8fcfb8028d',
+ timestamp: 1729623297,
+ },
+ },
// CashFusion tx
- // Note, this is also an app tx
// cae3e4bffeddbea25e74115a69e63bcf36e23d22b30bd30487e5e5a732d9ec78
{
txid: 'cae3e4bffeddbea25e74115a69e63bcf36e23d22b30bd30487e5e5a732d9ec78',
version: 1,
inputs: [
{
prevOut: {
txid: '0249db0d483b46e0cd6a54372a6bd3e1f899147e8d4f454075ba370effdefa00',
outIdx: 43,
},
inputScript:
'41df3bbe84dcbcf9efb6fcbab59878055178d29ea98b1156a8ac2c6f54e4fbd7482a38e34a7a7ea41388362272db9b7ada93109a3e058d004c80fdec75db506de7412102b1a4d65f26afe16fba3760459ffeba454a023da32e618fce929a5531292085ab',
value: 19044530,
sequenceNo: 4294967295,
outputScript:
'76a9140a797a01e2673b24a02fb80b5f46a70cb7b5a4d888ac',
},
{
prevOut: {
txid: '0249db0d483b46e0cd6a54372a6bd3e1f899147e8d4f454075ba370effdefa00',
outIdx: 49,
},
inputScript:
'41995ba8a2c1c789a7b0a23e6a956c61beedc5c47c20b16eb90a03c2dda683caaa6c38101df05adbdd12eb17cbc1445f9c6b922ebee33873409da201eb021afe8c412103d0f243652691c1244f5c77624fec85bf4bed316126bc9bb39ca1154f89017a2a',
value: 21813626,
sequenceNo: 4294967295,
outputScript:
'76a9144fc1984258f87c3bc0900156fe926436a78c12df88ac',
},
{
prevOut: {
txid: '0d0787c6d65a2251eb3e7356302ad98f1e5d1ce6e0ffab4037ed9320a86eff76',
outIdx: 4,
},
inputScript:
'419f2360309485fbb734f4494f30e084204712d829baa2f4a4f05cc4ace0424e9cff4110c6b0997462d9adb30be7565c7cac3897c43ad378797dde20b6d8d896fc41210278f666cae97366d63b06b9e1e7b4cb6d69aa625805ea32862872d8d930c5d50c',
value: 394728,
sequenceNo: 4294967295,
outputScript:
'76a91450c67b8481300a093b5501a6903e8ed98381f53988ac',
},
{
prevOut: {
txid: '0d0787c6d65a2251eb3e7356302ad98f1e5d1ce6e0ffab4037ed9320a86eff76',
outIdx: 5,
},
inputScript:
'410c585f2b3753202282bb590e452a5a955cda78ae6dd7bfc7475734be384cbb0930d92abcb9e0c5a03c7fc8fe5347170f83055ee1db213d47382839a9ddc6dd544121021684217e7f6b9b9fca552a01ca6215f52646a388e986df2c468bea7c42a80f6c',
value: 414991,
sequenceNo: 4294967295,
outputScript:
'76a914799b9c14ebc56fd5ba2a3801ba64743242f1af5e88ac',
},
{
prevOut: {
txid: '0d0787c6d65a2251eb3e7356302ad98f1e5d1ce6e0ffab4037ed9320a86eff76',
outIdx: 42,
},
inputScript:
'4146244f76308483868efe3e04146ef7d8be8b972d17b92124f9f2b833de1acdfa0a0311c359f0e114fd780876e96d3376939cdb8496671bf64b8141ccec9f4b22412102dfbb426682c141297c626c0bbb4eda4d1805a41ec6a5f9046e7431818523d60e',
value: 7028571,
sequenceNo: 4294967295,
outputScript:
'76a914ff4b390e072bdaff1985435509fcc80eb717e6dd88ac',
},
{
prevOut: {
txid: '0d0787c6d65a2251eb3e7356302ad98f1e5d1ce6e0ffab4037ed9320a86eff76',
outIdx: 51,
},
inputScript:
'412dd0e8f66411bd6e637139475b0c48d00493f24c6fee5eb90a4316af61734020bb0b316b8d2dc198c5a8541691e4162121ad307cbb74d33993fb009b2fa81f9841210339f66ba9e96d8e117d1354f3a3e1547efbba247bf6e3cadf25e6b78e6bd119ba',
value: 9302929,
sequenceNo: 4294967295,
outputScript:
'76a91450e0c8c98359095cafb50fb11c40956ff0d4fbf588ac',
},
{
prevOut: {
txid: '0d0787c6d65a2251eb3e7356302ad98f1e5d1ce6e0ffab4037ed9320a86eff76',
outIdx: 62,
},
inputScript:
'4129126ada21287ed43fe7e1644c7eb4fe1bd165941c264c99794c736b38171223ef59816a6d7ad956ea7ea09603d18f8de57fb225f76fe0215e22968230ab53db4121028115cb253822364f3fda454be0fc930ddf7ade935ce836a3fef1946b0b306d7f',
value: 12522752,
sequenceNo: 4294967295,
outputScript:
'76a9149314d58e27f38fb043b9a5aa7a8ae61d2617390688ac',
},
{
prevOut: {
txid: '0d0787c6d65a2251eb3e7356302ad98f1e5d1ce6e0ffab4037ed9320a86eff76',
outIdx: 70,
},
inputScript:
'41e39a6d0e85a2c4513ddaa878b5ed59a6dc995fff3afcda72fbd686b64ce44949c111c71e34f4464efe823dd558b807eefb4524ad9e6cd38c150a13857623cff04121025b46687783b420affaac912c62cc08df3f600a4a61e689621c1a5dc7ac636433',
value: 20061909,
sequenceNo: 4294967295,
outputScript:
'76a914dc82a51a0c936d0d102ec2cbd4d225e8057cec3f88ac',
},
{
prevOut: {
txid: '0d0787c6d65a2251eb3e7356302ad98f1e5d1ce6e0ffab4037ed9320a86eff76',
outIdx: 80,
},
inputScript:
'41880eaf18e82a6367228cb6a710072725ad6bb4521a250745967d431275dc25e5985093dd92de4d83cf5b2af4b86db070f6400d274afc68252e251079e216d3cf412103eb0f7de9e1e9cdcab35ea59e7dec29fc738ac42e9c2f76889ee7a9cafe88c39d',
value: 56109568,
sequenceNo: 4294967295,
outputScript:
'76a914493d17051df0b8e38a08084192b5a38cc493d3c988ac',
},
{
prevOut: {
txid: '1576f259e40e960d6b82d8fd7a2ea2def2482ac2820d87173a3400f78a969f77',
outIdx: 9,
},
inputScript:
'41932b38a6267156ba04423f0ed3a3004844ead66c1240df89769b0a401300f2d084e69ae26712b73e37264ef06b6349f2943ea07b718b84026bf46a9cf29a9d54412102dba5cafe198f94787aac78c77feb3334745f983fbcdd0c523a9e44890410d0e0',
value: 723423,
sequenceNo: 4294967295,
outputScript:
'76a914356f56b50c33cda30907ed5a280adda2c201943b88ac',
},
{
prevOut: {
txid: '1576f259e40e960d6b82d8fd7a2ea2def2482ac2820d87173a3400f78a969f77',
outIdx: 14,
},
inputScript:
'41b5a0fae4c5e44a344663206d2159fde79e93a55d7948b2073f373dffbc28012b43d83896813db3829235e5a0c769c0293e62cb97e771da22c97a22e4eb2b29a54121031cb5f7a8937279d887cde5749cabfc0dbab5bcd0f07269ccd9a136aeeb0ae873',
value: 1574168,
sequenceNo: 4294967295,
outputScript:
'76a91433e10aae895a2ee756b6c9d23ca7e0cd38c62d9988ac',
},
{
prevOut: {
txid: '1576f259e40e960d6b82d8fd7a2ea2def2482ac2820d87173a3400f78a969f77',
outIdx: 34,
},
inputScript:
'41538aa46dea3cb55bc28fdca8d9b912865ce6c42f2a31348d3088b88c8571ac95c9d68d65cfcb3ab4f15a37a13f5a2f00702219cf6a4b3047e56c4e590b963fc9412102be5c732ffd14a71bbc48fc1968137b9718bc12469844123acceecb317a88acb7',
value: 5347523,
sequenceNo: 4294967295,
outputScript:
'76a914e62f1eee7b4e6877dedf7b5aea40f87297467be688ac',
},
{
prevOut: {
txid: '1576f259e40e960d6b82d8fd7a2ea2def2482ac2820d87173a3400f78a969f77',
outIdx: 37,
},
inputScript:
'4102e53a109e468484c2f736b69466cd170ca387275df314a8add14317103b5cbe9c7f032eca20fda1ef15381d015a12b33f6c68591aa912f7852ccc7f96ead1ff412103422e3bbe0e1d4966b0df5e60bd27fac28b8008722c2b06719ca1d8c66e827393',
value: 5709247,
sequenceNo: 4294967295,
outputScript:
'76a914c3c0b825fe2f6a44c745eccb0f47367f7ddbc8d188ac',
},
{
prevOut: {
txid: '1576f259e40e960d6b82d8fd7a2ea2def2482ac2820d87173a3400f78a969f77',
outIdx: 60,
},
inputScript:
'41fe8583171c7b0f9fa661f7e19983eaa04bada5244d5696aa7a47e4d16a8811c231c4cf70d10dedd72745b07a0971781a1cf18fef8405bf9f43f0052654c780b4412102e263921e2c0cc86e5eb011aec807b330afacca37d4709950011efc288edd9e35',
value: 12202011,
sequenceNo: 4294967295,
outputScript:
'76a914e9f60021f56e16676ecad578abb1346db5e30d8888ac',
},
{
prevOut: {
txid: '1576f259e40e960d6b82d8fd7a2ea2def2482ac2820d87173a3400f78a969f77',
outIdx: 66,
},
inputScript:
'41fb8b372ec7450923d58f117401af1da185be2e23c4ec67e0c39a2e2fc51a1732805aac25dbf7d6023b5605e8b23d654a86676e1270a0c7027327c451d094ab9241210388abbae310cc1e415e49375f4a9205a1e48a62be5623f684700aee65f62ba506',
value: 15785620,
sequenceNo: 4294967295,
outputScript:
'76a9149817f6012414b1da0846093b7bdca256b26cbe4388ac',
},
{
prevOut: {
txid: '1a341e639aefe5f2b39b4b3979143eaf09fbd2a4418be47ea938777847be7fcd',
outIdx: 23,
},
inputScript:
'418354b6ce1e067e299e50c749a2582bf2af1340c1b9b0bbc167bc76a90076e9d5bf3226c856342d3e95d89c06bd0172fae0b2a1ab746474e63b3b673db7423ff0412103c96315e98c33ca47add711d1f8d3f18ca9cfa7632353d3fd26eb47fbc83a0eb9',
value: 6027875,
sequenceNo: 4294967295,
outputScript:
'76a91414018c2de14ba872dc49a0b1dbf1b4fad6f5baad88ac',
},
{
prevOut: {
txid: '201ef4c9c25e1e68a2e0e03bbeceb79232467de73e00d98de1fe411a62d93e5c',
outIdx: 9,
},
inputScript:
'415fa716ae82dd29033e38310a38ffa92731bffc4be413dd344270249a0f7743e39180988b42f0cc0af21669b067bbcaa919eb75d27f4c0365a084eec24f462edd412103152fd9fe4d3e7932067de80cc9108427c3dc2c5846fbe90a89bcadf2960fcb62',
value: 3368231,
sequenceNo: 4294967295,
outputScript:
'76a9149f7e0f97d488acaa7e22e7b897bb8b0af442207488ac',
},
{
prevOut: {
txid: '201ef4c9c25e1e68a2e0e03bbeceb79232467de73e00d98de1fe411a62d93e5c',
outIdx: 37,
},
inputScript:
'41471802e2bdb2e81a0404bec557885551d64f68c46144a2f1d30a76a8f46d9f52e8224ecbe0461f6689846a69b7754e3c71a2c06e0936db9b0d9af26dba6782c8412102999cc772ab81937c263a7e16003ecf1a7d66dc2b0e03824adea22eb569f67e38',
value: 29633909,
sequenceNo: 4294967295,
outputScript:
'76a9145c98423dd06247796fd7d15bf6142df6dd65dfb788ac',
},
{
prevOut: {
txid: '20671f9143db94f4c5f46687e174c0aef33b856bbb5b6476d39222a5efa8c413',
outIdx: 14,
},
inputScript:
'41098b8fc3e6357011795c444e0ff9665413380ef7a006ff133f4599f18dcbd476b41272db3a51c254389fe71d1d1d72832467b85e8826729da1538d0c79514a4e41210227c3905f79aacb942750876c92525d0606a60d67479549f30f15727346c391c7',
value: 9883971,
sequenceNo: 4294967295,
outputScript:
'76a91466a8ce404edf535033a47377130e8f1a16dec66888ac',
},
{
prevOut: {
txid: '26649dfccf538d6417ba64f51ffbfb22b1c25d39fb45659a95bfd86f4215744e',
outIdx: 54,
},
inputScript:
'41760d8aeb07e7ca65b4e108d262765da3623e5c8bdec061c3e4d7739af9674a7cab39aa9084334ebf4e93ca92a56a304691c2068d7cc08f4139c72943323495654121022a15b004a007154b8ea8cbfca2b2b1deb7486bd637e3289d52ced0b17c24ba9b',
value: 16806623,
sequenceNo: 4294967295,
outputScript:
'76a914f89793e870c57d76572d3d51eeda078f910b206388ac',
},
{
prevOut: {
txid: '2d4df9aa46ede7349de21ddda85495d4fa31314a046c24278722be59beaeeeba',
outIdx: 7,
},
inputScript:
'41ed99a385474633a98f857089f0dc019c214d155031f871f5018f93d0b5071324faf9f3c01b08bcd4209fbec188da6bffeb50a4c469479cd4f0fbdb71b59c4cbe412103dccb27bc84af3b5dcdfed5f08df935e168051ad32c242e698d0c1f464c2e9832',
value: 186916,
sequenceNo: 4294967295,
outputScript:
'76a914dca716d5af6b1288acd17ee88fb20a2829ab2ec588ac',
},
{
prevOut: {
txid: '2d4df9aa46ede7349de21ddda85495d4fa31314a046c24278722be59beaeeeba',
outIdx: 22,
},
inputScript:
'41821e4c860943978ee09812f333d1b80cc219b3f1e48b60036c3072a1c811f806f28eecb34fc1ffa1c3165be97c1906744e51c8a2900f2d59112635112ddc47144121038b04a447c830a862d06b3ec4ebafd1be3d9ee1c149630616a4a27fed46cea362',
value: 614009,
sequenceNo: 4294967295,
outputScript:
'76a914dd92844bcba0daa04127dd0ff9581741af1feb8288ac',
},
{
prevOut: {
txid: '2d4df9aa46ede7349de21ddda85495d4fa31314a046c24278722be59beaeeeba',
outIdx: 91,
},
inputScript:
'418a9b080fc4cc5b6284578bba95d9152f492a67a70e5ba93a01a40070223bb19f45018032b728edf4f7fbd4168658043ef7ede5aef8122ba1feb7002d714486544121020b4049376b0d37186240f2c834bc8579de9a8e3ad2370a0ad565eb455e1caa0d',
value: 9846080,
sequenceNo: 4294967295,
outputScript:
'76a914489371d01d4633920176d556d614b1df7e3c4c6c88ac',
},
{
prevOut: {
txid: '2e0d7986d8d8a1ee9e133802d6e900af87f1bac46dc72a3bb7887ddbaf1612ed',
outIdx: 9,
},
inputScript:
'41190cb88f905718c97ce7624f518be0895a7ac3dff36b909ad3c4a65f69fd7d5247b7d11f4e578c97be3277b66d355dd02cc0149fefa839a2117a840bdda3ae63412103e74e4014d3a5524029c160af6d3cbfddddedb906b71436c4f8b74ed0f21c0673',
value: 1014962,
sequenceNo: 4294967295,
outputScript:
'76a9140820720277dd2e4c117d7d601957ee8b98719daa88ac',
},
{
prevOut: {
txid: '2e0d7986d8d8a1ee9e133802d6e900af87f1bac46dc72a3bb7887ddbaf1612ed',
outIdx: 28,
},
inputScript:
'41f41af2c300d637d1dc76be1c22d41db11d5dcb96e6c9bf38a436e7c1985dcedce1f27ef0d4d220cc78f3becf5d1ca4ce4d4c3cfb74c86330b6dc5bf444c40591412103f9c5d3255a762234e5fe3b00744dcfdb8824344bc424a6e3de12d4e36b386851',
value: 3195908,
sequenceNo: 4294967295,
outputScript:
'76a9141b4ec15252b9beadd55e564588efd9c58412f08688ac',
},
{
prevOut: {
txid: '2e0d7986d8d8a1ee9e133802d6e900af87f1bac46dc72a3bb7887ddbaf1612ed',
outIdx: 32,
},
inputScript:
'419cdb4446c3b8ad1230193c299f1d22697ee02473acd74436e7690e5a8d8e2635a98e21e5709339b701bb68f2e82fb52c66619acb0c93bdd688ad9bfb6dccc14841210258f27cebaa7be00e2bce38a0462154fb36c066f5338097f5492290a036e4468e',
value: 3547312,
sequenceNo: 4294967295,
outputScript:
'76a914ebabc065b7548d0a97488addb928bfd0bb5ffd9f88ac',
},
{
prevOut: {
txid: '2e0d7986d8d8a1ee9e133802d6e900af87f1bac46dc72a3bb7887ddbaf1612ed',
outIdx: 56,
},
inputScript:
'412c51435daa98770fa841f30c56f57a28e55542b9d38c0958dc54ae9adec71f8b88da708d9f48df308587768f78ba9fa1de813a38f985d568248c2203253d279f4121037816e3882a1bb7d211f279d80514924a67821da755fb975e6b1da22f20d00a3d',
value: 7845327,
sequenceNo: 4294967295,
outputScript:
'76a91410b4904640491d3760f3c6021ecee661621d315788ac',
},
{
prevOut: {
txid: '2e0d7986d8d8a1ee9e133802d6e900af87f1bac46dc72a3bb7887ddbaf1612ed',
outIdx: 60,
},
inputScript:
'41c4d3e5bcac9b4bccba9836816eb92d7e1f2fa8c756c52e16245add08a02f441944a9ef716fe12646f800a63120eb2d045ddfe9bf8e40ab47115a04042fdf71114121030531127dac1724380ead04e01d060d2561db071dc0bca09826109a4dfcc969b2',
value: 8867513,
sequenceNo: 4294967295,
outputScript:
'76a914da4c46bb1853086cdf8ee66e07896d273982696388ac',
},
{
prevOut: {
txid: '2e0d7986d8d8a1ee9e133802d6e900af87f1bac46dc72a3bb7887ddbaf1612ed',
outIdx: 78,
},
inputScript:
'41de89262a098ff147bb603154f923f925b31ff1998b95441b39880421498627c2cbaa595a1920cbedff8fa4402fd8e85b517f82f044cd7b09d6f1e4d89a4712a24121025a31dd35e3dcb26bd847036dcbe581d3890904799d527b490234fdb4008aa60b',
value: 15639927,
sequenceNo: 4294967295,
outputScript:
'76a91497d20588c573a9845baacf6f0b8163e20ece5d7b88ac',
},
{
prevOut: {
txid: '5a5a63d9ac47570d0d1282143d634ae0bcc521524031cd4cd6de78dd99dad913',
outIdx: 40,
},
inputScript:
'413c94e9367505680d3f5e6695b79661da5dcb2effc1fbbf2b6411e26516e9347bb50b79607b691c4199eba64f2efc536512d45cc0bfb650948f8f45f689defeb141210221432af3ad2ce61807769c17528d7bd057cbd870c71fe660bf784d3b9ad2b4d8',
value: 8534104,
sequenceNo: 4294967295,
outputScript:
'76a91441b9b10ab1727cb4d16a94131511ff4376113d2288ac',
},
{
prevOut: {
txid: '6c47f3872ec33345a47b326f0adf1796158f027e61aa508a6fa7fcba8294432a',
outIdx: 15,
},
inputScript:
'419a17384c24d95cfab02e7bb27ec592b35bc98bbfac6109cdf999f7808e613a3795924b484b2be1c3ebddc2fe1564d9092a244309aff78d3bc71d95797184b59441210391ec904754962bfe2138ca65ebd66956d9d1c572bbba41896dddf744b9d20cf8',
value: 11857272,
sequenceNo: 4294967295,
outputScript:
'76a914cde9bc0f8ee4bc5bde25c2e3763186389e9d82eb88ac',
},
{
prevOut: {
txid: '8742d58eab0f68f4bf9138469790ad871f612a2469d5409abf219b05d922b369',
outIdx: 51,
},
inputScript:
'410c5743e207476e6dd36c076ebece2681568de8855581305e350ad8caf54c2cb043230b8ac3b5ddb3e4cc8ee1b1519f95178f19d521b1a4072f3638bcd164cf1341210316c3301c214cf5700a35bc0a5d0c41e7c4a48acff8da61d7ae66e79d8ef68026',
value: 6300897,
sequenceNo: 4294967295,
outputScript:
'76a9147a9abbe29cb7fcf541b68981840dc4c2c672504b88ac',
},
{
prevOut: {
txid: '8742d58eab0f68f4bf9138469790ad871f612a2469d5409abf219b05d922b369',
outIdx: 55,
},
inputScript:
'41b10fc4389ec67240720117d5140fba2a564095314220f5e2fafd7654da1e43fee3bf0df9464a99edc0687c8f86b552f8439c8b5908786ac9aae643c1e8c22fe94121027caaf5c02a2270a282cdb719fc22191fe86df130b98bc2f59c7881c8cd9d8e62',
value: 6765543,
sequenceNo: 4294967295,
outputScript:
'76a91468bf52bfab795729eb8bf2d067507068909c0b3688ac',
},
{
prevOut: {
txid: '9207e96a3e2af57a05d428c2a24bbb3a3a350eb84933ab8948a365209241d2ba',
outIdx: 5,
},
inputScript:
'4179f63f6c2b66cfffcf83f7c4ba49591bc5dad419a75c8e15e46811d0337399abbcab7f6e0655059727d345bf83139d287e8e93232dc75319672135bc9927d9e3412103ed359db19ec76388c224eb7f631a1411939fbcc01da55d5a01d1f6130d214a22',
value: 2129682,
sequenceNo: 4294967295,
outputScript:
'76a914ff119315f056f5a79ad69bc514ccd98fe325cc1288ac',
},
{
prevOut: {
txid: '9207e96a3e2af57a05d428c2a24bbb3a3a350eb84933ab8948a365209241d2ba',
outIdx: 7,
},
inputScript:
'410ad92695b50884c07014a1e7c6cb3126da5cb3ce44c2bc05ae9f2039a156ff958f7198c47f2621eee11a5289fd83d126afbc5a185a4ebf3fbfd6db274aebe94a412103b7f3e5aa641ce8be9ea9268ae50b3561d7b70e6bb7add4ed3805f9a51bfaa3f1',
value: 3175212,
sequenceNo: 4294967295,
outputScript:
'76a9144f78f400b6ea94bcc152fd86a3a342392a0d393088ac',
},
{
prevOut: {
txid: '9207e96a3e2af57a05d428c2a24bbb3a3a350eb84933ab8948a365209241d2ba',
outIdx: 41,
},
inputScript:
'414305bfa6ef186ddd3f4b8b97a4ebeff87161d67b2dec4408ea4a6f91cdf7db47382e96351a33dc49ebe02be37b14e5bc990cad04eaddc90f21a8060dee2e2b24412102b982e67fe423b3dd45e38bb2457f0bf66d94a7d559d32d70319cf4dbddf66c9c',
value: 30314073,
sequenceNo: 4294967295,
outputScript:
'76a914905cf8c762e3027b5562fb4dc949225999b7561a88ac',
},
{
prevOut: {
txid: '9207e96a3e2af57a05d428c2a24bbb3a3a350eb84933ab8948a365209241d2ba',
outIdx: 49,
},
inputScript:
'4192783667e913c048b56c041fc3fcfba6f4eb7f123deca342a2e6b461a66c11178db5082c2e6158b61518de5725970c71199cc2736aa789888d62ab9b290c8ec94121026799d87f335bb8a8705e9a5ecc3098073b988315806b2b0c38fd6d76352474f2',
value: 62502527,
sequenceNo: 4294967295,
outputScript:
'76a914cf53e1bbbb20d10464328fb25ea22ce9c23348cc88ac',
},
{
prevOut: {
txid: '991487316b19383d90be7e23a62720727a1d8781d8257bed99c38805c0789af3',
outIdx: 18,
},
inputScript:
'415ccd55e0a38970dff3a6fbddb96de79199b6e3949997fda69a6daf021b74964570d9a0ec0cedfc31ceed0c0a8f78a4c97a4ad2b372880cc2b5e73190f7e9a59341210208a31c990c4c0e0e5ae7d795d74ecbe782a5798f358ee70607f240b6a62fd925',
value: 12975492,
sequenceNo: 4294967295,
outputScript:
'76a9147ba0a29a0d139dcfcfc37bf44fa3c0b07eb4b08488ac',
},
{
prevOut: {
txid: '991487316b19383d90be7e23a62720727a1d8781d8257bed99c38805c0789af3',
outIdx: 35,
},
inputScript:
'41539175855a3e0ad5babfbc685ba9dcf8b515cbc9b109d016bba34b07ab502c7cf0e0c21be2f5e9320d51a4f33f9b2c46e57c7be56bf893afd4048b8b7863f4e54121036d7c1368ecb38e607c3a4905d4831d0d1b7a0452da451a87524eb585f68c5845',
value: 52978008,
sequenceNo: 4294967295,
outputScript:
'76a914ba83a81ec8cb0e5c4c3d4f0584fcc778a08a431088ac',
},
{
prevOut: {
txid: 'b130d197258b93961683184a7ea738a47b6b077142bf2f58dac31e26b56f9139',
outIdx: 12,
},
inputScript:
'41afd3ae1e1909d2b01a09e4be777857e7340d7b5aac524c4b0f258dcb3c91a80332f9a4c7f3bf1b21646362d5507a7725006c1815de581945ee4808c8e36e8f52412102966e2bc7578c25d70d601d404051d3880327ea8b742a754f19e234397ba67187',
value: 2926809,
sequenceNo: 4294967295,
outputScript:
'76a914e8a84faf2151752cce5b1a90c149cee3de8a175e88ac',
},
{
prevOut: {
txid: 'b130d197258b93961683184a7ea738a47b6b077142bf2f58dac31e26b56f9139',
outIdx: 22,
},
inputScript:
'41400258c69411e26ba097394882ae6b83837103a30b36b224348574375b070c40ae445ccb3301ef95b08215fbabea7fa92392506aa5a8420dd90669e00bbb9cae41210301bd439789b92005e5994ca61c073e91c75265a9ae7d66510a80601e0e46a8ec',
value: 6374291,
sequenceNo: 4294967295,
outputScript:
'76a914e1cf05100ed4381378604695ac6a695dc6cbd59c88ac',
},
{
prevOut: {
txid: 'b130d197258b93961683184a7ea738a47b6b077142bf2f58dac31e26b56f9139',
outIdx: 52,
},
inputScript:
'41178c840dfeddc76f7c8801a8e6625896c2afafe3bf02224b34b5f55576c7ec1e487e39eb04c5da64ad7a1006e939084d6d0c167d2bc5275ded4aa88f91ee2a314121027fab9d61001eba328d64a41394ff6c7d20111cfe6125603b3a402a8a392e2333',
value: 19205093,
sequenceNo: 4294967295,
outputScript:
'76a91412da0c1ee9d0e3ae99bc0d03c0389f82e5d55eb288ac',
},
{
prevOut: {
txid: 'b130d197258b93961683184a7ea738a47b6b077142bf2f58dac31e26b56f9139',
outIdx: 63,
},
inputScript:
'41afd33612b059e08d9137d1c52153a4293d63e06c29ed82fd234e453748b3dc2d887a87567cfa0e71c173cd1afccfbb4d67073b13f51e3aafb8904378366a2e7f412103812b9b680b5621d0e5a4493bcaf1e6d73c5f2141b193fa7740a5df1bf5c3d2e5',
value: 30781733,
sequenceNo: 4294967295,
outputScript:
'76a9145d85e859e9d4e8feb5a618ea903ebf377b54d75e88ac',
},
{
prevOut: {
txid: 'e5da66d1a79d4a3377a2a4f17f3d2c57f896a43654e4d4cd04813bc375465df2',
outIdx: 49,
},
inputScript:
'411b68176b5719f9192ad5f36e40ec03d86bc37af85f7fe1b184e12bc49ce03851a61bd1faae069c73ac471a997938640331edfacfd86f01ab65314900ccc93b20412102af2c8eb8dfd66767d20a2afdd600f75112d820737d8bf5147f5d6a11a94689e1',
value: 7563155,
sequenceNo: 4294967295,
outputScript:
'76a914d35af95bb47672ead30711dea85ac05bb8a854ac88ac',
},
{
prevOut: {
txid: 'e5da66d1a79d4a3377a2a4f17f3d2c57f896a43654e4d4cd04813bc375465df2',
outIdx: 62,
},
inputScript:
'41509fac1d23154ca52bbbed8287f20b81316af0ed83da37383671ef804251e81952c9fdede17a30b8197340f83256f367ed308bb2d26ae9620ea6a117fb634d464121036679c1faac5d79d3292634693cf8aa5de6313859a30a8e024ff03a0ca27756e8',
value: 11318359,
sequenceNo: 4294967295,
outputScript:
'76a914b0dec1ba4c6efb18f91167342c3c2988f5dfa75888ac',
},
{
prevOut: {
txid: 'e5da66d1a79d4a3377a2a4f17f3d2c57f896a43654e4d4cd04813bc375465df2',
outIdx: 68,
},
inputScript:
'41cdb6a6ee3b61ddd9f46b2dda0658f1ce7d5510401e6be6843dbbbc603fbcad46142176090c2c295967287a6a81cf1c9a27fdc5d1b8c9ea5267e9be4aa91b2bc0412103f2a000ceebf0dcc71b78c2076ceb5298d821f186344ef4ed9752ebfd206d3692',
value: 14100332,
sequenceNo: 4294967295,
outputScript:
'76a914e9d34d084b11e4ce88cc9d0869c90c207379f9c988ac',
},
{
prevOut: {
txid: 'e5da66d1a79d4a3377a2a4f17f3d2c57f896a43654e4d4cd04813bc375465df2',
outIdx: 69,
},
inputScript:
'4132a8a9642523289a520a19e325820db0a1b1e64d15b1cf122454c59d2960985e6e9fba429d26a759185220f3f9a5a6bfe915d217443bb134af4483f54815441a4121039fbfbb5269555db15811a1160c4b367ea144661791e4590f3c2214d543dad64e',
value: 14378379,
sequenceNo: 4294967295,
outputScript:
'76a914cd6a09b5535b40ff022140a471cdc16d0e66da4288ac',
},
{
prevOut: {
txid: 'e5da66d1a79d4a3377a2a4f17f3d2c57f896a43654e4d4cd04813bc375465df2',
outIdx: 84,
},
inputScript:
'41acf4a95bd83fbe0c0b853bd82d93bec1333554a4132c7e036175923aa7bf1a96c6aaa5ae6c5347040ca365c1aa24f41eba8ec9154822fac9530d0bef707b93e34121027edf78be3a7f824b1813b82aba7f7be1048817ca5148e9d130e424be56b47730',
value: 65926662,
sequenceNo: 4294967295,
outputScript:
'76a914208e9de659ed7c1d4bdff00eda7cd0ffd4b6ceca88ac',
},
{
prevOut: {
txid: 'e6356400b1994c772ce0968d3e8ffcc989a2970e87103d0096f4b859721d36f7',
outIdx: 20,
},
inputScript:
'4119da1345166c8694db830101e9608dfe7308c1991768cb65291eea3bc0074f7fc71c7e7995570a0fb93f45cb82738efaa036bfce189f1b6388456b987a965d8d4121038d89d5e03a0d7589d3d6c1c5058ad796be4e4f52baa709f10617de8f9f33690e',
value: 3472995,
sequenceNo: 4294967295,
outputScript:
'76a914e10383617cf53b7e9dca0f42e868e2e3dc82502d88ac',
},
{
prevOut: {
txid: 'e6356400b1994c772ce0968d3e8ffcc989a2970e87103d0096f4b859721d36f7',
outIdx: 24,
},
inputScript:
'4115e468dc353553fcc0000d682370329814fa7ca2eecac4c84382d799fc471f46d192874bbaca09168742cefbf8ff400cf3dc48d03454ca15bfe8a6608a5a94be41210309a4885102f697f07d003cbb066a93c68446ccf0a1afacec1b40eb2fe5582ee5',
value: 3836272,
sequenceNo: 4294967295,
outputScript:
'76a91492c4ba6ddd7501ab82286a9e9615364535e2f3f888ac',
},
{
prevOut: {
txid: 'e6356400b1994c772ce0968d3e8ffcc989a2970e87103d0096f4b859721d36f7',
outIdx: 26,
},
inputScript:
'4187e8db9862eed3a1caa1277cb5ad7b98dd35a005e4873b7438a7bf9977c739a68428a630503f46c6298621e5cb5a0390d49e806497e7f00f82ec5504549d948c4121038e774c9432e144b196f92122e67f291cd1bebc704a448bf4d7e156725a1df4d5',
value: 4277798,
sequenceNo: 4294967295,
outputScript:
'76a9144e44c948de92c030bb0626208295d73315b8c5e988ac',
},
{
prevOut: {
txid: 'e6356400b1994c772ce0968d3e8ffcc989a2970e87103d0096f4b859721d36f7',
outIdx: 30,
},
inputScript:
'4131a686c7274fa77c615ba8bfcb2d7430054f0e81c7302e2c1761b80f0bc5291f0dda9c814297799d79c9866391c7614e677d56ab8c376fc40ca97d3082b5c32b412102a69bbd2a7b05ed30d06be79231078b8054266c141123b9614b3e0e0131ec7805',
value: 6063080,
sequenceNo: 4294967295,
outputScript:
'76a914f73e9cf52eb1748d21fcab670fc3e567085685f488ac',
},
{
prevOut: {
txid: 'e6356400b1994c772ce0968d3e8ffcc989a2970e87103d0096f4b859721d36f7',
outIdx: 57,
},
inputScript:
'4130a61428674676161422657ae38362f7d71131efe99725bcb59d49dffb976e3739802b0050d3c462c8742eda191f81340273f045b6edbf49956364003c28712e4121032a1b09b6bff6cd9e7a24fe362c2c2601c21987e3cebadae381d8ced9a7f53ea1',
value: 23507881,
sequenceNo: 4294967295,
outputScript:
'76a9142cc2ac6f08c48193fbbc5eb69a4540c98f434d9688ac',
},
{
prevOut: {
txid: 'e6356400b1994c772ce0968d3e8ffcc989a2970e87103d0096f4b859721d36f7',
outIdx: 58,
},
inputScript:
'41ef5090a88edc015d00612b69ef7185039b94ef6832bcb377cabb8def1ae2d4eeea58b5349591e346ec4947583fff76e79182ee9008e3cfa7a90a864e33678724412103222133f854bc03ddba9e33bf2a835e48cb7104945677912cbb4ed577643fa4aa',
value: 24929079,
sequenceNo: 4294967295,
outputScript:
'76a9142cc23fe84f3b64ff7b24c1a0cf354ea583072a0388ac',
},
{
prevOut: {
txid: 'e6356400b1994c772ce0968d3e8ffcc989a2970e87103d0096f4b859721d36f7',
outIdx: 65,
},
inputScript:
'417febf2d756ce0e52854d180ee6a4f6face48786ce1cac18cd915de9dcd2d981ac94e2501e15167d707e5a8067cc5efe0a3b17a9cdc27850a0191453bf52d416541210208fd87bf70ab65ac9ebbe050a5c7e3bb8dd31edb21e2aea47d7a146e0ae3f8d2',
value: 44828158,
sequenceNo: 4294967295,
outputScript:
'76a9142656de90609eb0cbdd7c4244b3d55d71ceda667b88ac',
},
{
prevOut: {
txid: 'ebd049fda7294f2fe0890cda4b37aa76392b1cad9e159ed5e91bf25daa73552b',
outIdx: 27,
},
inputScript:
'41b93878fb3a450cae4ba3d51076b002cd8f57aca1e3571bef90ab66f94bd05396d7b25e2e46a664f5c17c4fc5ad27f02d5bf5406d588e02b54a774ec627e4e698412103067a6da11957d765e831d47e6dee1a45e59aa28f60500a39bb158157c3eb1509',
value: 15070744,
sequenceNo: 4294967295,
outputScript:
'76a914f7c311ddfc0ed8a87d8a699df9c9acbb98720d2b88ac',
},
],
outputs: [
{
value: 0,
outputScript:
'6a0446555a0020194246d7e7a3e1c8b88559ee210b390ceb8b9ab82f860d9c9d6f9daa01f6c82f',
},
{
value: 165265,
outputScript:
'76a914d0a105a83b6c44147cf2b4c5b5c8a3c75de1163e88ac',
},
{
value: 415331,
outputScript:
'76a914ca82960460fd35b5a59b2c6bf6372f3ad171bcb988ac',
spentBy: {
txid: '1edc89d04563375cb42b3a7f20f29154669b74409a553a220a132bc5279bf69f',
outIdx: 41,
},
},
{
value: 513772,
outputScript:
'76a9143505593cb0094cece0165201c1b5320e8e0e88cb88ac',
},
{
value: 620346,
outputScript:
'76a9144523d89bee4b8481f9111865bfabcedf70d5f73488ac',
spentBy: {
txid: '4ec1e8ee8b8d543856747abf5d6f1bc0886eebb327ae8d1903fbc109a56b5c69',
outIdx: 47,
},
},
{
value: 745522,
outputScript:
'76a914e0577e495872e411ec85bb2db60c08bd41d970c788ac',
},
{
value: 951733,
outputScript:
'76a9142884eb394c83672e40e24eed0d9f2532def1df2488ac',
},
{
value: 977317,
outputScript:
'76a9143cdaf26959fe02da6dd9b1ecf57fdc8e70f80def88ac',
},
{
value: 1637364,
outputScript:
'76a9149f30bd564d189088a41af3298dc5e87b33482d6188ac',
spentBy: {
txid: '32d48f0606578ff1789ab579f2e733d9d5b7504aa42fc20a76a23cbfa93af57e',
outIdx: 51,
},
},
{
value: 1709164,
outputScript:
'76a91456341f200e54c76f06fff1e610603d489e87e2ed88ac',
spentBy: {
txid: 'dd5fa01b7aea079e5898968e4dcb89a54d2feab880805f9a49878ec038849685',
outIdx: 43,
},
},
{
value: 1734037,
outputScript:
'76a914deda3755e569aa7ad431b679376465e234c1ab0e88ac',
},
{
value: 1787574,
outputScript:
'76a914e577b97b63b684ff4bce76638b39e9bf7337391a88ac',
},
{
value: 1794906,
outputScript:
'76a914122e9d948f848e322accc989c63b68b1f1f4a27e88ac',
},
{
value: 1809132,
outputScript:
'76a914d2d288f72d59325aa7a93f378456416b35c1089488ac',
},
{
value: 1830109,
outputScript:
'76a91437ff7ce27ff184efda22c428d870713d7071531788ac',
},
{
value: 2048566,
outputScript:
'76a91471a3f02e0e7c1bd4b6c06a72aeeb9aef3c84226888ac',
},
{
value: 2179080,
outputScript:
'76a9147133b978e3d9c4004ef8c8e288395f86a4ca64fc88ac',
spentBy: {
txid: '4ec1e8ee8b8d543856747abf5d6f1bc0886eebb327ae8d1903fbc109a56b5c69',
outIdx: 48,
},
},
{
value: 2390444,
outputScript:
'76a914f974ac8c5784b78316c66ee06720d6694b6a454288ac',
spentBy: {
txid: 'dd5fa01b7aea079e5898968e4dcb89a54d2feab880805f9a49878ec038849685',
outIdx: 44,
},
},
{
value: 2911412,
outputScript:
'76a914f2a178204a2efeaa652ed11df741ff13c715014388ac',
},
{
value: 2966953,
outputScript:
'76a9144b8bcd98ebee333f0018b11071e8308fbe95554e88ac',
},
{
value: 3004755,
outputScript:
'76a914d68020a4bb2d9f6cdb6516938c1b1f4c4b6a4efd88ac',
spentBy: {
txid: 'dd5fa01b7aea079e5898968e4dcb89a54d2feab880805f9a49878ec038849685',
outIdx: 45,
},
},
{
value: 3143178,
outputScript:
'76a91413fe018b9d729cb23e86628d04365ea77e8d443888ac',
},
{
value: 3199496,
outputScript:
'76a914a72c7bf5b4661af684c4990af751bdc9cdb871cf88ac',
},
{
value: 3258788,
outputScript:
'76a914f339d9aff74cb1bd22d600e38d09b0649225317d88ac',
spentBy: {
txid: '1edc89d04563375cb42b3a7f20f29154669b74409a553a220a132bc5279bf69f',
outIdx: 42,
},
},
{
value: 3270709,
outputScript:
'76a914ea8762d67b8ba330a86703abfb4a0eed28719f1b88ac',
spentBy: {
txid: '4ec1e8ee8b8d543856747abf5d6f1bc0886eebb327ae8d1903fbc109a56b5c69',
outIdx: 49,
},
},
{
value: 3428216,
outputScript:
'76a9141021bc93de524c2b76c8ff770f1db96a11dd07eb88ac',
spentBy: {
txid: '32d48f0606578ff1789ab579f2e733d9d5b7504aa42fc20a76a23cbfa93af57e',
outIdx: 52,
},
},
{
value: 3634054,
outputScript:
'76a9146a224d728e90a9796fdb2dde295788453a0121c788ac',
},
{
value: 3806325,
outputScript:
'76a914b0f96288a5544986d45a5d67992b52534e61585a88ac',
spentBy: {
txid: '4ec1e8ee8b8d543856747abf5d6f1bc0886eebb327ae8d1903fbc109a56b5c69',
outIdx: 50,
},
},
{
value: 3961600,
outputScript:
'76a914581217d2826ef7cdfe158da53797752d1b1a84fa88ac',
},
{
value: 4188894,
outputScript:
'76a914dae3359f7056323a08cfb480334a8b4543cc7caf88ac',
},
{
value: 4417013,
outputScript:
'76a914d6dcbb2c33b936e7f4e2d38b9782ee9c4bcc370d88ac',
},
{
value: 4496101,
outputScript:
'76a91487f03b804d802f3ee5357149c4d192d6150c254088ac',
},
{
value: 5142036,
outputScript:
'76a9142c5c4f436e1e0adc06592e188dff7731600b9c3a88ac',
},
{
value: 5192325,
outputScript:
'76a9140817b0b24e07482730af2f30b5b5811a905071b388ac',
},
{
value: 5219666,
outputScript:
'76a914cfa9bc00c9d7fa1932b786986f10082af9c6b48f88ac',
},
{
value: 5350325,
outputScript:
'76a914d34f5216d288ab84cf5ef3e2675fac013c82408b88ac',
},
{
value: 5525456,
outputScript:
'76a914a046783da29e24286e887bec2be51e776e56ec6f88ac',
spentBy: {
txid: 'dd5fa01b7aea079e5898968e4dcb89a54d2feab880805f9a49878ec038849685',
outIdx: 46,
},
},
{
value: 5576379,
outputScript:
'76a914bfc31c471ae0929046880f56977bce04bafce82a88ac',
},
{
value: 5882456,
outputScript:
'76a914633e45a98df30365d95b560a5dece001d40da15788ac',
},
{
value: 6227395,
outputScript:
'76a91474615b2ba0fb2c37106aa0ff42f6213af036d45788ac',
},
{
value: 6337179,
outputScript:
'76a914764ca1c4108ecb20e0daad7b8e3ecf64da8a546a88ac',
spentBy: {
txid: '1edc89d04563375cb42b3a7f20f29154669b74409a553a220a132bc5279bf69f',
outIdx: 43,
},
},
{
value: 6464974,
outputScript:
'76a914e32dd21a1b04ffcfb3b4d6a2a78c1bf1c1e1c63d88ac',
},
{
value: 6529342,
outputScript:
'76a914c4eefae07113114399c503d5663154c1563aae8288ac',
},
{
value: 6560940,
outputScript:
'76a91454ecb17cfe67b65105f09c5ee7d1fe066d5340ae88ac',
},
{
value: 6622255,
outputScript:
'76a91426e17f7957ac242c076351839cb02ae879c9412e88ac',
},
{
value: 6649523,
outputScript:
'76a914ad2d233aad04cc481b19ead6a15b37ae865c33d588ac',
},
{
value: 6966573,
outputScript:
'76a914294aa90705c956c74700c97a92d7ce219bc8088088ac',
spentBy: {
txid: '4ec1e8ee8b8d543856747abf5d6f1bc0886eebb327ae8d1903fbc109a56b5c69',
outIdx: 51,
},
},
{
value: 7237077,
outputScript:
'76a914b55a3f80593d1ef2b85dc25e20335347871b28c488ac',
spentBy: {
txid: '1edc89d04563375cb42b3a7f20f29154669b74409a553a220a132bc5279bf69f',
outIdx: 44,
},
},
{
value: 7247037,
outputScript:
'76a91409360004e44bb8b116cfd70301cb3e58c85bdc6e88ac',
},
{
value: 7571632,
outputScript:
'76a914b746811bdaf55f875770cd78b068209ed987323788ac',
},
{
value: 7936466,
outputScript:
'76a9147eadf036427bcf114711bd09f1c6cd71f7d99a5988ac',
spentBy: {
txid: '32d48f0606578ff1789ab579f2e733d9d5b7504aa42fc20a76a23cbfa93af57e',
outIdx: 53,
},
},
{
value: 9483487,
outputScript:
'76a91471308336e8b5e92ac1e57414e9f0a025b2a5019888ac',
},
{
value: 9577280,
outputScript:
'76a914e85d5b1a53a128308e326aad645027b2c8c4ac2788ac',
spentBy: {
txid: 'dd5fa01b7aea079e5898968e4dcb89a54d2feab880805f9a49878ec038849685',
outIdx: 47,
},
},
{
value: 9828407,
outputScript:
'76a91488528685bbe4c696b58c8b6d5411606bcb3cc63888ac',
},
{
value: 10210178,
outputScript:
'76a914d1ac9f7674d3881fa7675a552691d4a433aeb38e88ac',
spentBy: {
txid: 'dd5fa01b7aea079e5898968e4dcb89a54d2feab880805f9a49878ec038849685',
outIdx: 48,
},
},
{
value: 10402019,
outputScript:
'76a9146674b3bdf74ae56d356815b40821385e23b88cdc88ac',
},
{
value: 10651856,
outputScript:
'76a914269454c951ad997703c868cab35a8a80a87dc54e88ac',
},
{
value: 11003947,
outputScript:
'76a9147b8c09c622dcfca426b5b9c46680b80265ee9c5c88ac',
spentBy: {
txid: '32d48f0606578ff1789ab579f2e733d9d5b7504aa42fc20a76a23cbfa93af57e',
outIdx: 54,
},
},
{
value: 11036265,
outputScript:
'76a914fd793cd1790bcc882cdd531f085353bd9cceee7088ac',
},
{
value: 11983981,
outputScript:
'76a914f3983d2e5ce9a94f386c17f4526665ed8fc776bf88ac',
},
{
value: 12224144,
outputScript:
'76a914f4f21ca1c5cdc979bc0d88de0e6a4faaae77affe88ac',
},
{
value: 12434515,
outputScript:
'76a914047e120012f39c07127dc9dcce6490c1b3af613288ac',
},
{
value: 12544565,
outputScript:
'76a91498884aa69b0213b32dd58f4bcf9d76edde2f796a88ac',
},
{
value: 13856128,
outputScript:
'76a9146a1d4ac47decfd68e4223e6932d2d27797429a2f88ac',
spentBy: {
txid: '32d48f0606578ff1789ab579f2e733d9d5b7504aa42fc20a76a23cbfa93af57e',
outIdx: 55,
},
},
{
value: 13926555,
outputScript:
'76a9147c8467c80862216ceed083263064dee5a285eff088ac',
},
{
value: 13953640,
outputScript:
'76a914e815252fbb0c8649aa03851e828e6d6ce554ca8188ac',
},
{
value: 14604288,
outputScript:
'76a914c6f3e86c0b5be88b5dea5c5f97f8898d83dde84b88ac',
},
{
value: 14796850,
outputScript:
'76a914f18a494b498b8893efac9877c58259f9f956df3488ac',
},
{
value: 15003691,
outputScript:
'76a9144b8093c3ddf67df71ad3a55d6a1f786f07c6e9f388ac',
},
{
value: 15069088,
outputScript:
'76a914e7b2d4e1a0269e16e506b9532e8d0fcd6146f38988ac',
spentBy: {
txid: 'dd5fa01b7aea079e5898968e4dcb89a54d2feab880805f9a49878ec038849685',
outIdx: 49,
},
},
{
value: 15542738,
outputScript:
'76a9148f2abbbc1531b363b250cd45748ab017230d971d88ac',
spentBy: {
txid: '1edc89d04563375cb42b3a7f20f29154669b74409a553a220a132bc5279bf69f',
outIdx: 45,
},
},
{
value: 16694191,
outputScript:
'76a914158f31b3ec66a43d8115e4711cfe2a8623ca513688ac',
},
{
value: 16936236,
outputScript:
'76a914d0b8bee8deb1e0c45128d6da73bb3a88f583cf5188ac',
},
{
value: 17419891,
outputScript:
'76a914496bfb89eeaf405b4cabb03705c65246414f8f9088ac',
},
{
value: 17878439,
outputScript:
'76a914320a5c0c0c17172aa799df5e9f01fcfe34cc8acc88ac',
},
{
value: 18203996,
outputScript:
'76a9149c532629e3bbd6b5df9fa7b2a3a8cc447a9c96b088ac',
},
{
value: 18343505,
outputScript:
'76a9140c15e03cb7db4f39a4c5dc5318a1e8a5b3c6c9ce88ac',
spentBy: {
txid: '1edc89d04563375cb42b3a7f20f29154669b74409a553a220a132bc5279bf69f',
outIdx: 46,
},
},
{
value: 20055678,
outputScript:
'76a914d82b642270c662ad12ba298b677d543deb27b4bf88ac',
},
{
value: 21519776,
outputScript:
'76a914da876799bf44f7eee7c17a0628223caa24b9dd6e88ac',
},
{
value: 22638667,
outputScript:
'76a9140f6308b30e5406711fe10ff4d091c35168d83b0388ac',
},
{
value: 23648127,
outputScript:
'76a914283acdf34fe0b72b8c668057ef09e68de883f1dc88ac',
spentBy: {
txid: '4ec1e8ee8b8d543856747abf5d6f1bc0886eebb327ae8d1903fbc109a56b5c69',
outIdx: 52,
},
},
{
value: 24750932,
outputScript:
'76a9149213d8dfdafe9925f3c3005ab195c0b9dec4b94888ac',
},
{
value: 33183717,
outputScript:
'76a91495d307ac4c97d450c8e8520a40478cfdb7631cf988ac',
spentBy: {
txid: '1edc89d04563375cb42b3a7f20f29154669b74409a553a220a132bc5279bf69f',
outIdx: 47,
},
},
{
value: 38734503,
outputScript:
'76a9143a6425c522cbfee2ee3892d77734ff4170bae1fe88ac',
},
{
value: 69219464,
outputScript:
'76a9140f20b1267a5ab07ba65f860285c5f8c4a726ee2288ac',
},
],
lockTime: 0,
timeFirstSeen: 1728974402,
size: 10810,
isCoinbase: false,
tokenEntries: [],
tokenFailedParsings: [],
tokenStatus: 'TOKEN_STATUS_NON_TOKEN',
block: {
height: 866659,
hash: '0000000000000000108f9dce76f6ee3060c72c0f5643febcf9eaf6bcb40704f2',
timestamp: 1728974999,
},
},
// Token tx
// df956d36c9a7c3540eeee22955e1d95c450dd931939a9d54fcd21e56c40e8a38
{
txid: 'df956d36c9a7c3540eeee22955e1d95c450dd931939a9d54fcd21e56c40e8a38',
version: 2,
inputs: [
{
prevOut: {
txid: 'da3c897eb6d4e5299cb3ae2d8235d46632647303eab61236a1072885d5e56d66',
outIdx: 2,
},
inputScript:
'41d3e2a6b3b740a0b79fcf437b4a4bf5a8d7b97d9295236aef0de5d08f3b757e7386328ae2b702bf469b69d115eada9c2f4871075bcabee76e59635321056e802a4121021d7fd45a888292cf3a022a95acdbcf82f9f2d5bbbfbdbc740acd558a9f25b5d0',
value: 546,
sequenceNo: 4294967295,
token: {
tokenId:
'20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8',
tokenType: {
protocol: 'SLP',
type: 'SLP_TOKEN_TYPE_FUNGIBLE',
number: 1,
},
amount: '999756000000000',
isMintBaton: false,
entryIdx: 0,
},
outputScript:
'76a9140d94ba179ec21c42417a71a77873b3619363d8ea88ac',
},
{
prevOut: {
txid: 'b90c07e196fa5c18d9f4ba056a962732254c2f6bd7ec63e579f4990160c894ed',
outIdx: 301,
},
inputScript:
'41d6b0dc3a7426777555d9f824c5c508c2c7b46d5a8d3f30864bb7377aa17f5c2fda0ba3951559cef4100b79b9070a32faa31e8047825dc47889f7cffca8fd028b4121021d7fd45a888292cf3a022a95acdbcf82f9f2d5bbbfbdbc740acd558a9f25b5d0',
value: 4300,
sequenceNo: 4294967295,
outputScript:
'76a9140d94ba179ec21c42417a71a77873b3619363d8ea88ac',
},
],
outputs: [
{
value: 0,
outputScript:
'6a04534c500001010453454e442020a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f80800000002540be4000800038d4381321400',
},
{
value: 546,
outputScript:
'76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac',
token: {
tokenId:
'20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8',
tokenType: {
protocol: 'SLP',
type: 'SLP_TOKEN_TYPE_FUNGIBLE',
number: 1,
},
amount: '10000000000',
isMintBaton: false,
entryIdx: 0,
},
},
{
value: 546,
outputScript:
'76a9140d94ba179ec21c42417a71a77873b3619363d8ea88ac',
token: {
tokenId:
'20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8',
tokenType: {
protocol: 'SLP',
type: 'SLP_TOKEN_TYPE_FUNGIBLE',
number: 1,
},
amount: '999746000000000',
isMintBaton: false,
entryIdx: 0,
},
},
{
value: 2815,
outputScript:
'76a9140d94ba179ec21c42417a71a77873b3619363d8ea88ac',
},
],
lockTime: 0,
timeFirstSeen: 1728941100,
size: 467,
isCoinbase: false,
tokenEntries: [
{
tokenId:
'20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8',
tokenType: {
protocol: 'SLP',
type: 'SLP_TOKEN_TYPE_FUNGIBLE',
number: 1,
},
txType: 'SEND',
isInvalid: false,
burnSummary: '',
failedColorings: [],
actualBurnAmount: '0',
intentionalBurn: '0',
burnsMintBatons: false,
},
],
tokenFailedParsings: [],
tokenStatus: 'TOKEN_STATUS_NORMAL',
block: {
height: 866601,
hash: '000000000000000004cf8ec8f13467a6469b2647702928767d70fac936f64494',
timestamp: 1728941119,
},
},
// Binance hot wallet withdrawal
{
txid: '8d096fac948cbd65eea8c399182f169bdff891f5a8fd799c5e495c82f62a5dce',
version: 1,
inputs: [
{
prevOut: {
txid: '695e60f49e8959f740468dbc9273ceef7d0c1e33d50c03364890e9d8582a2441',
outIdx: 1,
},
inputScript:
'47304402202be54e9bb516220fd0bc48755e50374ca274b10325b896657d898849b3abc0b902201eb90668fdac9fb3133810b645f1b95ac5c8a2d410727eea355bcb07c13d48bd412103562731a08eb23e6260b516c4564f746033e9080bc9f61ad2158a63927500b8b1',
value: 2016227,
sequenceNo: 4294967295,
outputScript:
'76a914231f7087937684790d1049294f3aef9cfb7b05dd88ac',
},
],
outputs: [
{
value: 1972000,
outputScript:
'76a9145f41a47e1a4a86143ea999604cc504a3f19dc67088ac',
},
{
value: 44001,
outputScript:
'76a914231f7087937684790d1049294f3aef9cfb7b05dd88ac',
},
],
lockTime: 0,
timeFirstSeen: 1728985099,
size: 225,
isCoinbase: false,
tokenEntries: [],
tokenFailedParsings: [],
tokenStatus: 'TOKEN_STATUS_NON_TOKEN',
block: {
height: 866672,
hash: '00000000000000000f43f2da867ec399fe7c2ff3c7406c1f1a16f2d726771009',
timestamp: 1728985449,
},
},
];
diff --git a/apps/ecash-herald/test/parse.test.js b/apps/ecash-herald/test/parse.test.js
index 2da97f5867..6e96529763 100644
--- a/apps/ecash-herald/test/parse.test.js
+++ b/apps/ecash-herald/test/parse.test.js
@@ -1,424 +1,440 @@
// 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 opReturn = require('../constants/op_return');
const unrevivedBlock = require('./mocks/block');
const minersJson = require('../constants/miners');
const minerTestFixtures = require('./fixtures/miners');
const stakerTestFixtures = require('./fixtures/stakers');
const invalidatedBlocksTestFixtures = require('./fixtures/invalidatedBlocks');
const { jsonReviver } = require('../src/utils');
const block = JSON.parse(JSON.stringify(unrevivedBlock), jsonReviver);
const miners = JSON.parse(JSON.stringify(minersJson), jsonReviver);
const memoOutputScripts = require('./mocks/memo');
const { consumeNextPush } = require('ecash-script');
const { MockChronikClient } = require('../../../modules/mock-chronik-client');
const { caching } = require('cache-manager');
const {
parseBlockTxs,
getStakerFromCoinbaseTx,
getMinerFromCoinbaseTx,
parseMemoOutputScript,
getBlockTgMessage,
parseOpReturn,
getSwapTgMsg,
getAirdropTgMsg,
getEncryptedCashtabMsg,
parseMultipushStack,
parseSlpTwo,
guessRejectReason,
summarizeTxHistory,
} = require('../src/parse');
const {
swaps,
airdrops,
encryptedCashtabMsgs,
slp2PushVectors,
slp2TxVectors,
aliasRegistrations,
cashtabMsgs,
payButtonTxs,
paywallTxs,
authenticationTxs,
} = require('./mocks/appTxSamples');
const dailyTxs = require('./mocks/dailyTxs');
describe('parse.js functions', function () {
it('Parses the master test block', function () {
const thisBlock = block;
const {
blockTxs,
parsedBlock,
coingeckoPrices,
tokenInfoMap,
outputScriptInfoMap,
blockSummaryTgMsgs,
} = thisBlock;
assert.deepEqual(
parseBlockTxs(parsedBlock.hash, parsedBlock.height, blockTxs),
parsedBlock,
);
assert.deepEqual(
getBlockTgMessage(
parsedBlock,
coingeckoPrices,
tokenInfoMap,
outputScriptInfoMap,
),
blockSummaryTgMsgs,
);
});
it('parseOpReturn handles all types of SWaP txs', function () {
for (let i = 0; i < swaps.length; i += 1) {
const { hex, stackArray, tokenId } = swaps[i];
assert.deepEqual(parseOpReturn(hex), {
app: opReturn.knownApps.swap.app,
msg: '',
stackArray,
tokenId,
});
}
});
it('getSwapTgMsg handles all types of SWaP txs', function () {
for (let i = 0; i < swaps.length; i += 1) {
const { stackArray, msg, tokenInfo } = swaps[i];
const result = getSwapTgMsg(stackArray, tokenInfo);
assert.strictEqual(result, msg);
}
});
it('parseOpReturn handles alias registration txs', function () {
for (let i = 0; i < aliasRegistrations.length; i += 1) {
const { hex, stackArray, msg } = aliasRegistrations[i];
assert.deepEqual(parseOpReturn(hex), {
app: opReturn.knownApps.alias.app,
msg,
stackArray,
tokenId: false,
});
}
});
it('parseOpReturn handles Cashtab Msgs', function () {
for (let i = 0; i < cashtabMsgs.length; i += 1) {
const { hex, stackArray, msg } = cashtabMsgs[i];
assert.deepEqual(parseOpReturn(hex), {
app: opReturn.knownApps.cashtabMsg.app,
msg,
stackArray,
tokenId: false,
});
}
});
it('parseOpReturn handles PayButton txs', function () {
for (let i = 0; i < payButtonTxs.length; i += 1) {
const { hex, stackArray, msg } = payButtonTxs[i];
assert.deepEqual(parseOpReturn(hex), {
app: opReturn.knownApps.payButton.app,
msg,
stackArray,
tokenId: false,
});
}
});
it('parseOpReturn handles airdrop txs with and without a cashtab msg', function () {
for (let i = 0; i < airdrops.length; i += 1) {
const { hex, stackArray, tokenId } = airdrops[i];
assert.deepEqual(parseOpReturn(hex), {
app: opReturn.knownApps.airdrop.app,
msg: '',
stackArray,
tokenId,
});
}
});
it('getAirdropMsg handles airdrop txs with and without a cashtab msg', function () {
for (let i = 0; i < airdrops.length; i += 1) {
const {
stackArray,
airdropSendingAddress,
airdropRecipientsKeyValueArray,
msg,
msgApiFailure,
tokenInfo,
coingeckoPrices,
} = airdrops[i];
const xecReceivingOutputs = new Map(airdropRecipientsKeyValueArray);
let totalSatsSent = 0;
for (const satoshis of xecReceivingOutputs.values()) {
totalSatsSent += satoshis;
}
const result = getAirdropTgMsg(
stackArray,
airdropSendingAddress,
xecReceivingOutputs,
totalSatsSent,
tokenInfo,
coingeckoPrices,
);
const resultApiFailure = getAirdropTgMsg(
stackArray,
airdropSendingAddress,
xecReceivingOutputs,
totalSatsSent,
false,
false,
);
assert.strictEqual(result, msg);
assert.strictEqual(resultApiFailure, msgApiFailure);
}
});
it('parseOpReturn handles encrypted cashtab msg txs', function () {
for (let i = 0; i < encryptedCashtabMsgs.length; i += 1) {
const { hex, stackArray } = encryptedCashtabMsgs[i];
assert.deepEqual(parseOpReturn(hex), {
app: opReturn.knownApps.cashtabMsgEncrypted.app,
msg: '',
stackArray,
tokenId: false,
});
}
});
it('parseOpReturn handles paywall payment txs', function () {
for (let i = 0; i < paywallTxs.length; i += 1) {
const { hex, stackArray, msg } = paywallTxs[i];
assert.deepEqual(parseOpReturn(hex), {
app: opReturn.knownApps.paywall.app,
msg: msg,
stackArray,
tokenId: false,
});
}
});
it('getEncryptedCashtabMsg handles encrypted cashtab msg txs with and without price info', function () {
for (let i = 0; i < encryptedCashtabMsgs.length; i += 1) {
const {
sendingAddress,
xecReceivingOutputsKeyValueArray,
msg,
msgApiFailure,
coingeckoPrices,
} = encryptedCashtabMsgs[i];
const xecReceivingOutputs = new Map(
xecReceivingOutputsKeyValueArray,
);
let totalSatsSent = 0;
for (const satoshis of xecReceivingOutputs.values()) {
totalSatsSent += satoshis;
}
const result = getEncryptedCashtabMsg(
sendingAddress,
xecReceivingOutputs,
totalSatsSent,
coingeckoPrices,
);
const resultApiFailure = getEncryptedCashtabMsg(
sendingAddress,
xecReceivingOutputs,
totalSatsSent,
false,
);
assert.strictEqual(result, msg);
assert.strictEqual(resultApiFailure, msgApiFailure);
}
});
it('parseOpReturn handles slp2 txs', function () {
for (let i = 0; i < slp2TxVectors.length; i += 1) {
const { hex, msg } = slp2TxVectors[i];
assert.deepEqual(parseOpReturn(hex), {
app: 'EMPP',
msg,
});
}
});
it('parseOpReturn handles authentication txs', function () {
for (let i = 0; i < authenticationTxs.length; i += 1) {
const { hex, stackArray, msg } = authenticationTxs[i];
assert.deepEqual(parseOpReturn(hex), {
app: opReturn.knownApps.authentication.app,
msg: msg,
stackArray,
tokenId: false,
});
}
});
it('parseMultipushStack handles a range of observed slp2 empp pushes', function () {
for (let i = 0; i < slp2TxVectors.length; i += 1) {
const { emppStackArray, msg } = slp2TxVectors[i];
assert.deepEqual(parseMultipushStack(emppStackArray), {
app: 'EMPP',
msg,
});
}
});
it('parseSlpTwo handles a range of observed slp2 empp pushes', function () {
for (let i = 0; i < slp2PushVectors.length; i += 1) {
const { push, msg } = slp2PushVectors[i];
assert.strictEqual(parseSlpTwo(push.slice(8)), msg);
}
});
it('parseOpReturn recognizes legacy Cash Fusion prefix', function () {
assert.deepEqual(
parseOpReturn(
'0446555a0020771c2fa0d402fe15ba0aa2e98660facf4a8ab6801b5baf3c0b08ced685dd85ed',
),
{
app: opReturn.knownApps.fusionLegacy.app,
msg: '',
tokenId: false,
stackArray: [
'46555a00',
'771c2fa0d402fe15ba0aa2e98660facf4a8ab6801b5baf3c0b08ced685dd85ed',
],
},
);
});
it(`parseMemoOutputScript correctly parses all tested memo actions in memo.js`, function () {
memoOutputScripts.map(memoTestObj => {
const { outputScript, msg } = memoTestObj;
// Get array of pushes
let stack = { remainingHex: outputScript.slice(2) };
let stackArray = [];
while (stack.remainingHex.length > 0) {
stackArray.push(consumeNextPush(stack).data);
}
assert.deepEqual(parseMemoOutputScript(stackArray), {
app: opReturn.memo.app,
msg,
});
});
});
it('getStakerFromCoinbaseTx parses miner for all test vectors', function () {
for (let i = 0; i < stakerTestFixtures.length; i += 1) {
const { coinbaseTx, staker } = stakerTestFixtures[i];
assert.deepEqual(
getStakerFromCoinbaseTx(
coinbaseTx.block.height,
coinbaseTx.outputs,
),
staker,
);
}
});
it('getMinerFromCoinbaseTx parses miner for all test vectors', function () {
for (let i = 0; i < minerTestFixtures.length; i += 1) {
const { parsed, coinbaseHex, payoutOutputScript } =
minerTestFixtures[i];
// Minimally mock the coinbase tx
const inputScript = coinbaseHex;
const outputs = [{ outputScript: payoutOutputScript }];
assert.strictEqual(
getMinerFromCoinbaseTx(inputScript, outputs, miners),
parsed,
);
}
});
it('guessRejectReason returns the expected guess for all test vectors', async function () {
for (let i = 0; i < invalidatedBlocksTestFixtures.length; i += 1) {
const {
height,
coinbaseData,
expectedRejectReason,
expectedCacheData,
mockedBlock,
} = invalidatedBlocksTestFixtures[i];
const mockedChronik = new MockChronikClient();
mockedChronik.mockedResponses.block = mockedBlock;
const testMemoryCache = await caching('memory', {
max: 100,
ttl: 60,
});
testMemoryCache.set(`${height}`, expectedCacheData);
assert.strictEqual(
await guessRejectReason(
mockedChronik,
height,
coinbaseData,
testMemoryCache,
),
expectedRejectReason,
);
}
});
it('summarizeTxHistory summarizes a collection of txs across multiple blocks including fiat prices', function () {
const mockUtcNewDayTimestampSeconds = 1728950400;
assert.deepEqual(
summarizeTxHistory(
mockUtcNewDayTimestampSeconds,
dailyTxs,
0.000033,
),
[
'<b>24 hours thru 15 Oct 2024, 00:00</b>\n' +
- '📦74 blocks\n' +
- '➡️8 txs\n' +
+ '📦1,331 blocks\n' +
+ '➡️15 txs\n' +
'\n' +
'<b><i>⛏️3 miners found blocks</i></b>\n' +
'<u>Top 3</u>\n' +
- '1. Mining-Dutch, 1 <i>(1%)</i>\n' +
- '2. solopool.org, 1 <i>(1%)</i>\n' +
- '3. ViaBTC, 1 <i>(1%)</i>\n' +
+ '1. Mining-Dutch, 1 <i>(0%)</i>\n' +
+ '2. solopool.org, 1 <i>(0%)</i>\n' +
+ '3. ViaBTC, 1 <i>(0%)</i>\n' +
'\n' +
'<b><i>💰3 stakers earned $31</i></b>\n' +
'<u>Top 3</u>\n' +
- '1. <a href="https://explorer.e.cash/address/ecash:qzs8hq2pj4hu5j09fdr5uhha3986h2mthvfp7362nu">qzs...2nu</a>, 1 <i>(1%)</i>\n' +
- '2. <a href="https://explorer.e.cash/address/ecash:qr42c8c04tqndscfrdnl0rzterg0qdaegyjzt8egyg">qr4...gyg</a>, 1 <i>(1%)</i>\n' +
- '3. <a href="https://explorer.e.cash/address/ecash:qqvhatumna957qu0je78dnc9pc7c7hu89crkq6k0cd">qqv...0cd</a>, 1 <i>(1%)</i>\n' +
+ '1. <a href="https://explorer.e.cash/address/ecash:qzs8hq2pj4hu5j09fdr5uhha3986h2mthvfp7362nu">qzs...2nu</a>, 1 <i>(0%)</i>\n' +
+ '2. <a href="https://explorer.e.cash/address/ecash:qr42c8c04tqndscfrdnl0rzterg0qdaegyjzt8egyg">qr4...gyg</a>, 1 <i>(0%)</i>\n' +
+ '3. <a href="https://explorer.e.cash/address/ecash:qqvhatumna957qu0je78dnc9pc7c7hu89crkq6k0cd">qqv...0cd</a>, 1 <i>(0%)</i>\n' +
'\n' +
'<a href="https://cashtab.com/">Cashtab</a>\n' +
'🎁 <b>1</b> new user received <b>42 XEC</b>\n' +
'🎟 <b>1</b> <a href="https://explorer.e.cash/tx/aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1">CACHET</a> reward\n' +
'\n' +
- '⚛️ <b>1</b> CashFusion tx\n' +
'🪙 <b>1</b> token tx\n' +
- '📱 <b>1</b> app tx\n' +
+ '\n' +
+ '📱 <b><i>8 app txs</i></b>\n' +
+ '🖋 <b>1</b> <a href="https://www.ecashchat.com/">Article/Reply tx</a>\n' +
+ '⚛️ <b>1</b> CashFusion\n' +
+ '🛒 <b>1</b> PayButton tx\n' +
+ '🪂 <b>1</b> Airdrop\n' +
+ '✏️ <b>1</b> Cashtab Msg\n' +
+ '💬 <b>1</b> <a href="https://www.ecashchat.com/">eCashChat tx</a>\n' +
+ '🔓 <b>1</b> eCashChat Auth\n' +
+ '💸 <b>1</b> Paywall tx\n' +
'\n' +
'🏦 <b><i>Binance</i></b>\n' +
'<b>1</b> withdrawal, $1',
],
);
});
it('summarizeTxHistory summarizes a collection of txs across multiple blocks without fiat price', function () {
const mockUtcNewDayTimestampSeconds = 1728950400;
assert.deepEqual(
summarizeTxHistory(mockUtcNewDayTimestampSeconds, dailyTxs),
[
'<b>24 hours thru 15 Oct 2024, 00:00</b>\n' +
- '📦74 blocks\n' +
- '➡️8 txs\n' +
+ '📦1,331 blocks\n' +
+ '➡️15 txs\n' +
'\n' +
'<b><i>⛏️3 miners found blocks</i></b>\n' +
'<u>Top 3</u>\n' +
- '1. Mining-Dutch, 1 <i>(1%)</i>\n' +
- '2. solopool.org, 1 <i>(1%)</i>\n' +
- '3. ViaBTC, 1 <i>(1%)</i>\n' +
+ '1. Mining-Dutch, 1 <i>(0%)</i>\n' +
+ '2. solopool.org, 1 <i>(0%)</i>\n' +
+ '3. ViaBTC, 1 <i>(0%)</i>\n' +
'\n' +
'<b><i>💰3 stakers earned 937,620 XEC</i></b>\n' +
'<u>Top 3</u>\n' +
- '1. <a href="https://explorer.e.cash/address/ecash:qzs8hq2pj4hu5j09fdr5uhha3986h2mthvfp7362nu">qzs...2nu</a>, 1 <i>(1%)</i>\n' +
- '2. <a href="https://explorer.e.cash/address/ecash:qr42c8c04tqndscfrdnl0rzterg0qdaegyjzt8egyg">qr4...gyg</a>, 1 <i>(1%)</i>\n' +
- '3. <a href="https://explorer.e.cash/address/ecash:qqvhatumna957qu0je78dnc9pc7c7hu89crkq6k0cd">qqv...0cd</a>, 1 <i>(1%)</i>\n' +
+ '1. <a href="https://explorer.e.cash/address/ecash:qzs8hq2pj4hu5j09fdr5uhha3986h2mthvfp7362nu">qzs...2nu</a>, 1 <i>(0%)</i>\n' +
+ '2. <a href="https://explorer.e.cash/address/ecash:qr42c8c04tqndscfrdnl0rzterg0qdaegyjzt8egyg">qr4...gyg</a>, 1 <i>(0%)</i>\n' +
+ '3. <a href="https://explorer.e.cash/address/ecash:qqvhatumna957qu0je78dnc9pc7c7hu89crkq6k0cd">qqv...0cd</a>, 1 <i>(0%)</i>\n' +
'\n' +
'<a href="https://cashtab.com/">Cashtab</a>\n' +
'🎁 <b>1</b> new user received <b>42 XEC</b>\n' +
'🎟 <b>1</b> <a href="https://explorer.e.cash/tx/aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1">CACHET</a> reward\n' +
'\n' +
- '⚛️ <b>1</b> CashFusion tx\n' +
'🪙 <b>1</b> token tx\n' +
- '📱 <b>1</b> app tx\n' +
+ '\n' +
+ '📱 <b><i>8 app txs</i></b>\n' +
+ '🖋 <b>1</b> <a href="https://www.ecashchat.com/">Article/Reply tx</a>\n' +
+ '⚛️ <b>1</b> CashFusion\n' +
+ '🛒 <b>1</b> PayButton tx\n' +
+ '🪂 <b>1</b> Airdrop\n' +
+ '✏️ <b>1</b> Cashtab Msg\n' +
+ '💬 <b>1</b> <a href="https://www.ecashchat.com/">eCashChat tx</a>\n' +
+ '🔓 <b>1</b> eCashChat Auth\n' +
+ '💸 <b>1</b> Paywall tx\n' +
'\n' +
'🏦 <b><i>Binance</i></b>\n' +
'<b>1</b> withdrawal, 19,720 XEC',
],
);
});
});

File Metadata

Mime Type
text/x-diff
Expires
Thu, Apr 17, 03:38 (13 h, 26 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5508732
Default Alt Text
(211 KB)

Event Timeline