diff --git a/apps/ecash-herald/config.ts b/apps/ecash-herald/config.ts index f536899cf..ed989f5ec 100644 --- a/apps/ecash-herald/config.ts +++ b/apps/ecash-herald/config.ts @@ -1,190 +1,191 @@ -// Copyright (c) 2023 The Bitcoin developers +// 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. + import { SendMessageOptions } from 'node-telegram-bot-api'; interface CryptoSlug { coingeckoSlug: string; ticker: string; } export type FiatCode = 'usd' | 'eur' | 'gbp' | 'jpy'; export interface HeraldPriceApi { apiBase: string; cryptos: CryptoSlug[]; fiat: FiatCode; precision: number; } export interface HeraldConfig { cacheTtlMsecs: number; xecSendDisplayCount: number; chronik: string[]; blockExplorer: string; priceApi: HeraldPriceApi; fiatReference: { usd: string; jpy: string; eur: string; gbp: string }; stakingRewardApiUrl: string; ifpAddress: string; tgMsgOptions: SendMessageOptions; whaleSats: { bigWhale: number; // 10 billion xec modestWhale: number; // 5 billion xec shark: number; // 1 billion xec swordfish: number; // 700 million xec barracuda: number; // 500 million xec octopus: number; // 250 million xec piranha: number; // 100 million xec crab: number; // anything under 100 million xec shrimp: number; }; emojis: { agora: string; agoraBuy: string; agoraList: string; agoraCancel: string; alias: string; alp: string; invalid: string; nft: string; mintvault: string; block: string; miner: string; staker: string; xecSend: string; arrowRight: string; tokenBurn: string; tokenGenesis: string; tokenSend: string; tokenMint: string; tokenFixed: string; gift: string; bank: string; app: string; token: string; fusion: string; cashtabMsg: string; cashtabEncrypted: string; payButton: string; swap: string; airdrop: string; paywall: string; authentication: string; unknown: string; memo: string; bigWhale: string; modestWhale: string; shark: string; swordfish: string; barracuda: string; octopus: string; piranha: string; crab: string; shrimp: string; priceUp: string; priceDown: string; }; } const config: HeraldConfig = { cacheTtlMsecs: 1000 * 60 * 60 * 4, // 4 hours xecSendDisplayCount: 12, chronik: [ 'https://chronik-native1.fabien.cash', 'https://chronik-native2.fabien.cash', 'https://chronik-native.fabien.cash', 'https://chronik.pay2stay.com/xec', 'https://chronik.be.cash/xec2', ], blockExplorer: 'https://explorer.e.cash', priceApi: { apiBase: 'https://api.coingecko.com/api/v3/simple/price', cryptos: [ { coingeckoSlug: 'ecash', ticker: 'XEC' }, { coingeckoSlug: 'bitcoin', ticker: 'BTC' }, { coingeckoSlug: 'ethereum', ticker: 'ETH' }, ], fiat: 'usd', precision: 8, }, fiatReference: { usd: '$', jpy: 'Β₯', eur: 'β¬', gbp: 'Β£' }, stakingRewardApiUrl: 'https://avalanche.cash/api/nextstakingreward', ifpAddress: 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07', tgMsgOptions: { parse_mode: 'HTML', disable_web_page_preview: true, }, whaleSats: { // 20 billion xec bigWhale: 2000000000000, // 10 billion xec modestWhale: 1000000000000, // 5 billion xec shark: 500000000000, // 1 billion xec swordfish: 100000000000, // 700 million xec barracuda: 70000000000, // 500 million xec octopus: 50000000000, // 250 million xec piranha: 25000000000, // 100 million xec crab: 10000000000, // anything under 100 million xec shrimp: 0, }, emojis: { agora: 'π', agoraBuy: 'π°', agoraList: 'π·', agoraCancel: 'β', alias: 'πΎ', alp: 'π»', invalid: 'β', nft: 'πΌ', mintvault: 'π§©', block: 'π¦', miner: 'βοΈ', staker: 'π°', xecSend: 'πΈ', arrowRight: 'β‘οΈ', tokenBurn: 'π₯', tokenGenesis: 'π§ͺ', tokenSend: 'π', tokenMint: 'π¨', tokenFixed: 'π', gift: 'π', bank: 'π¦', app: 'π±', token: 'πͺ', fusion: 'βοΈ', cashtabMsg: 'π', cashtabEncrypted: 'π', payButton: 'π', swap: 'π€³', airdrop: 'πͺ', paywall: 'πΈ', authentication: 'π', unknown: 'β', memo: 'π', bigWhale: 'π', modestWhale: 'π³', shark: 'π¦', swordfish: 'π¬', barracuda: 'π ', octopus: 'π', piranha: 'π‘', crab: 'π¦', // Most addresses seen by the app are shrimp, so use empty string shrimp: '', priceUp: 'π', priceDown: 'π', }, }; export default config; diff --git a/apps/ecash-herald/scripts/generateMock.ts b/apps/ecash-herald/scripts/generateMock.ts index ca8d5fb94..c34920c42 100644 --- a/apps/ecash-herald/scripts/generateMock.ts +++ b/apps/ecash-herald/scripts/generateMock.ts @@ -1,278 +1,278 @@ // 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. import config from '../config'; import fs from 'fs'; import path from 'path'; import { ChronikClient, Tx } from 'chronik-client'; import { MockChronikClient } from '../../../modules/mock-chronik-client'; import { jsonReplacer, getCoingeckoApiUrl } from '../src/utils'; import unrevivedBlockMocks from '../test/mocks/block'; import { jsonReviver } from '../src/utils'; import { handleBlockFinalized, StoredMock } from '../src/events'; import { parseBlockTxs } from '../src/parse'; import { sendBlockSummary } from '../src/telegram'; import cashaddr from 'ecashaddrjs'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { caching } from 'cache-manager'; import { MockTelegramBot } from '../test/mocks/telegramBotMock'; import secrets from '../secrets'; import TelegramBot from 'node-telegram-bot-api'; const mockedChronik = new MockChronikClient(); const chronik = new ChronikClient(config.chronik); const mockedTelegramBot = new MockTelegramBot(); const { dev } = secrets; const { botId, channelId } = dev.telegram; const blockMocks = JSON.parse(JSON.stringify(unrevivedBlockMocks), jsonReviver); // Initialize telegram bot to send msgs to dev channel const telegramBotDev = new TelegramBot(botId, { polling: true }); /** * generateMock * * This script takes an array of txids and builds a fake block with them * In this way we can still use ecash-herald's block-parsing functionality * while showcasing all of its features, without needing to duplicate txids * that are already tested */ const MOCK_HASH = '0000000000000000000000000000000000000000000000000000000000000000'; const MOCK_HEIGHT = 819346; // Test vectors // Add txids to this array related to new features as new diffs are added const txids = [ // Coinbase tx '0bf6e9cd974cd5fc6fbbf739a42447d41a301890e2db242295c64df63dc3ee7e', // Coinbase tx with staking rwds // eToken mint tx '010114b9bbe776def1a512ad1e96a4a06ec4c34fc79bcb5d908845f5102f6b0f', // etoken genesis txs // Cashtab CACHET rewards '004e018dd98520aa722ee76c608771dd578a044f38103a8298f25e6ffbc7c3ba', '0110cd886ecd2d9570e98b7501cd039f4e5352d69659a46f1a49cc19c1869701', '327101f6f3b740280a6e9fbd8edc41f4f0500633672975a5974a4147c94016a5', // Cashtab CACHET send tx that is not a Cashtab Reward 'aa13c6f214ff58f36ed5e108a7f36d8f98729c50186b27a53b989c7f36fbf517', // Cashtab XEC rewards 'd8fe456c89357c23ac6d240fe9319ce9ba393c9c3833631046a265ca7c8349e6', '083b7862bae48e78549ccf63833896f5f4f5bdef5c380a108fa99cdb64261fa3', // eToken send txs '6ffcc83e76226bd32821cc6862ce9b363b22594247a4e73ccf3701b0023592b2', // etoken send tx, 0 decimals 'fb70df00c07749082756054522d3f08691fd9caccd0e0abf736df23d22845a6e', // etoken send tx, 7 decimals '25345b0bf921a2a9080c647768ba440bbe84499f4c7773fba8a1b03e88ae7fe7', // etoken send locale string formatting '0167e881fcb359cdfc82af5fc6c0821daf55f40767694eea2f23c0d42a9b1c17', // etoken self-send tx, BUX // ALP send txs 'b2c9c056339d41ec59341541dda8bd6e570730beba485e14eb54d0a073700c22', // etoken send tx, CRD '45ec66bc2440d2f94fa2c645e20a44f6fab7c397053ce77a95484c6053104cdc', // EMPP SLPv2 send '413b57617d2c497b137d31c53151fee595415ec273ef7a111160da8093147ed8', // EMPP SLPv2 mint // eToken burn tx '6b139007a0649f99a1a099c7c924716ee1920f74ea83111f6426854d4c3c3c79', // etoken burn tx // App txs 'd5be7a4b483f9fdbbe3bf46cfafdd0100d5dbeee0b972f4dabc8ae9d9962fa55', // Cash fusion tx 'd02d94a1a520877c60d1e3026c3e85f8995d48d7b90140f83e24ede592c30306', // Cashtab msg '1083da7ead4779fbab5c5e8291bb7a37abaf4f97f5ff99ee654759b2eaee445b', // Encrypted cashtab msg 'ad44bf5e214ab71bb60a2eee165f368c139cd49c2380c3352f0a4fffc746b36a', // SWaP SLP Atomic Swap Signal 'a8c348539a1470b28b9f99693994b918b475634352994dddce80ad544e871b3a', // memo | reply to memo '7a0d6ae3384e293183478f681f51a77ef4c71f29957199364bb9ba4d8e1938be', // Airdrop '22135bb69435023a84c80b1b93b31fc8898c3507eaa70569ed038f32d59599a9', // alias beta '9094e1aab7ac73c680bf66e78cc8311831b3d813e608bff1e07b1854855fc0f1', // Unknown app tx, parsable 'b5782d3a3b55e5ee9e4330a969c2891042ae05fafab7dc05cd14da63e7242f8e', // Unknown app tx, likely hex // XEC send txs '4f33c81d95641eb0f80e793dc96c58a2438f9bb1f18750d8fb3b56c28cd25035', // π, Address in directory, self-send XEC tx 'f5d4c112cfd22701226ba050cacfacc3aff570964c6196f67e326fc3224300a2', // π¦ recipient ]; async function generateMock( chronik: ChronikClient, mockedChronik: typeof MockChronikClient, telegramBot: TelegramBot, mockedTelegramBot: MockTelegramBot, channelId: string, block: StoredMock, txids: string[], ) { const { outputScriptInfoMap, tokenInfoMap, coingeckoResponse } = block; // Get txids from your saved block const savedTxids = block.blockTxs.map(tx => { return tx.txid; }); // Determine which, if any, txids need to be added to savedBlock - let newTxids = []; - for (let i in txids) { + const newTxids: string[] = []; + for (const i in txids) { if (!savedTxids.includes(txids[i])) { newTxids.push(txids[i]); } } // Build array of promises to get txid info from chronik - let chronikTxidPromises = []; - for (let i in newTxids) { + const chronikTxidPromises = []; + for (const i in newTxids) { chronikTxidPromises.push( new Promise<Tx>((resolve, reject) => { chronik.tx(newTxids[i]).then( result => { resolve(result as Tx); }, err => { reject(err); }, ); }), ); } let newChronikTxs: Tx[]; try { newChronikTxs = await Promise.all(chronikTxidPromises); } catch (err) { console.log( '\x1b[31m%s\x1b[0m', `Error in Promise.all(chronikTxidPromises)`, err, ); // Exit in error condition process.exit(1); } // Add these new chronik tx objects to the txs: key of your savedBlock const blockTxs = block.blockTxs.concat(newChronikTxs); // Mock chronik response for chronik.blockTxs mockedChronik.setTxHistoryByBlock(MOCK_HEIGHT, blockTxs); // Get parsedBlock const parsedBlock = parseBlockTxs(MOCK_HASH, MOCK_HEIGHT, blockTxs); // Tell mockedChronik what response we expect for chronik.tx const { tokenIds, outputScripts } = parsedBlock; // Instead of saving all the chronik responses as mocks, which would be very large // Just set them as mocks based on tokenInfoMap, which contains the info we need tokenIds.forEach(tokenId => { mockedChronik.setMock('token', { input: tokenId, output: { genesisInfo: tokenInfoMap.has(tokenId) ? tokenInfoMap.get(tokenId) : { tokenTicker: 'STUB', tokenName: 'Placeholder Token Name', decimals: 0, }, }, }); }); outputScripts.forEach(outputScript => { - let { type, hash } = + const { type, hash } = cashaddr.getTypeAndHashFromOutputScript(outputScript); mockedChronik.setScript(type, hash); const outputScriptInfo = outputScriptInfoMap.get(outputScript); if (typeof outputScriptInfo !== 'undefined') { const { utxos } = outputScriptInfo; mockedChronik.setUtxos(type, hash, { outputScript, utxos }); } else { // If you don't have a mock for this particular outputScript in block.js, // mock it as an address with a single utxo for 100 XEC mockedChronik.setUtxos(type, hash, { outputScript, utxos: [{ value: 10000 }], }); } }); // Mock coingecko price response // onNoMatch: 'throwException' helps to debug if mock is not being used const mock = new MockAdapter(axios, { onNoMatch: 'throwException', }); // Mock a successful API request mock.onGet(getCoingeckoApiUrl(config)).reply(200, coingeckoResponse); // Generate app mocks using this block // TODO need to mock all the calls here // so need to manually build outputscriptinfomap, tokeninfomap const CACHE_TTL = 2 * config.cacheTtlMsecs; const memoryCache = await caching('memory', { max: 100, ttl: CACHE_TTL, }); const returnedMocks = (await handleBlockFinalized( mockedChronik, mockedTelegramBot, channelId, MOCK_HASH, MOCK_HEIGHT, memoryCache, true, )) as StoredMock; // Save it to a file // Directory for mocks. Relative to /scripts, ../test/mocks/generated/ //const mocksDir = path.join(__dirname, '..', 'test', 'mocks', 'generated'); //const mocksFileName = `uber_block_${Date.now()}.json`; const mocksDir = path.join(__dirname, '..', 'test', 'mocks'); const mocksFileName = 'block.ts'; // Create directory if it does not exist if (!fs.existsSync(mocksDir)) { fs.mkdirSync(mocksDir); } // We want this string to appear in the generated blocks.js file, // but not in this file, as we want this file to show up in phab diffs - const mocksWrite = `// Copyright (c) 2023 The Bitcoin developers\n// Distributed under the MIT software license, see the accompanying\n// file COPYING or http://www.opensource.org/licenses/mit-license.php.\n\nconst mockedBlock: any =${JSON.stringify( + const mocksWrite = `// Copyright (c) 2023 The Bitcoin developers\n// Distributed under the MIT software license, see the accompanying\n// file COPYING or http://www.opensource.org/licenses/mit-license.php.\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst mockedBlock: any =${JSON.stringify( returnedMocks, jsonReplacer, 2, )};\n\nexport default mockedBlock;\n`; fs.writeFileSync(`${mocksDir}/${mocksFileName}`, mocksWrite, 'utf-8'); // Send msg(s) to Telegram const { blockSummaryTgMsgs, blockSummaryTgMsgsApiFailure } = returnedMocks; // Send msg with successful price API call await sendBlockSummary(blockSummaryTgMsgs, telegramBot, channelId); // Send msg with failed price API call await sendBlockSummary( blockSummaryTgMsgsApiFailure, telegramBot, channelId, ); console.log( '\x1b[32m%s\x1b[0m', `β Built mocks and sent msgs for ecash-herald mock block.`, ); process.exit(0); } generateMock( chronik, mockedChronik, telegramBotDev, mockedTelegramBot, channelId, blockMocks, txids, ); diff --git a/apps/ecash-herald/scripts/getCoingeckoPrices.ts b/apps/ecash-herald/scripts/getCoingeckoPrices.ts index e11bbeab6..d7ecec7fc 100644 --- a/apps/ecash-herald/scripts/getCoingeckoPrices.ts +++ b/apps/ecash-herald/scripts/getCoingeckoPrices.ts @@ -1,65 +1,70 @@ // 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. import { getCoingeckoPrices, formatPrice } from '../src/utils'; import config from '../config'; const testedPriceObjects = [ config.priceApi, { apiBase: 'https://api.coingecko.com/api/v3/simple/price', cryptos: [ { coingeckoSlug: 'ecash', ticker: 'XEC' }, { coingeckoSlug: 'dogecoin', ticker: 'DOGE' }, { coingeckoSlug: 'solana', ticker: 'SOL' }, { coingeckoSlug: 'monero', ticker: 'XMR' }, ], fiat: 'usd', precision: 8, }, { apiBase: 'https://api.coingecko.com/api/v3/simple/price', cryptos: [ { coingeckoSlug: 'ecash', ticker: 'XEC' }, { coingeckoSlug: 'dogecoin', ticker: 'DOGE' }, { coingeckoSlug: 'solana', ticker: 'SOL' }, { coingeckoSlug: 'monero', ticker: 'XMR' }, ], fiat: 'eur', precision: 6, }, ]; +// Use any type as this is an overengineered function from before ts implementation +// not impactful to improve the types since we never use the feature +// todo change if we need it +// eslint-disable-next-line @typescript-eslint/no-explicit-any async function printGetPricesInfo(priceInfoObj: any) { const { cryptos, fiat, precision } = priceInfoObj; const resp = await getCoingeckoPrices(priceInfoObj); let coingeckoPrices; if (resp !== false) { coingeckoPrices = resp.coingeckoPrices; } else { return console.error(`Failed to fetch coingeckoPrices`); } console.log( `Price info for ${cryptos + // eslint-disable-next-line @typescript-eslint/no-explicit-any .map((crypto: any) => { return crypto.ticker; }) .join( ', ', )} in ${fiat.toUpperCase()} with ${precision}-decimal precision`, ); - for (let i in coingeckoPrices) { + for (const i in coingeckoPrices) { const { fiat, price, ticker } = coingeckoPrices[i]; const formattedPrice = formatPrice(price, fiat); console.log(`1 ${ticker} = ${formattedPrice} ${fiat.toUpperCase()}`); } // New line console.log(''); } for (let i = 0; i < testedPriceObjects.length; i += 1) { printGetPricesInfo(testedPriceObjects[i]); } diff --git a/apps/ecash-herald/scripts/sendMsgByBlock.ts b/apps/ecash-herald/scripts/sendMsgByBlock.ts index 75d13d066..36e87b1ae 100644 --- a/apps/ecash-herald/scripts/sendMsgByBlock.ts +++ b/apps/ecash-herald/scripts/sendMsgByBlock.ts @@ -1,121 +1,119 @@ // 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. /** * sendMsgByBlockheight.ts * * A script to allow developer to generate and broadcast an ecash-herald message * by blockheight * * Use cases * - Developing and testing a feature that is not in any of the current test blocks * - Proof of concept for future version of app where you may want to send "missed" msgs * if the app is down for some period of time * * Example use * * Send msg to test channel for default blockheight (genesis block) * node scripts/sendMsgByBlock.js * * Send msg to test channel for blockheight 700000 * node scripts/sendMsgByBlock.js 700000 */ import { handleBlockFinalized } from '../src/events'; import { sendBlockSummary } from '../src/telegram'; import { getCoingeckoApiUrl } from '../src/utils'; import config from '../config'; import { ChronikClient } from 'chronik-client'; import secrets from '../secrets'; import TelegramBot from 'node-telegram-bot-api'; import { caching } from 'cache-manager'; import { StoredMock } from '../src/events'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; // Default to the genesis block let height = 0; // Use live API for axios calls and not mocks // Default to false as it doesn't take much testing to rate limit coingecko API let liveApi = false; // Look for blockheight specified from command line if (process.argv && typeof process.argv[2] !== 'undefined') { // user input if available, commas removed height = parseInt(process.argv[2].replace(/,/g, '')); if (typeof process.argv[3] !== 'undefined' && process.argv[3] === 'true') { liveApi = true; console.log(`Sending msg with live API calls`); } } const chronik = new ChronikClient(config.chronik); const { dev } = secrets; const { botId, channelId } = dev.telegram; const telegramBotDev = new TelegramBot(botId, { polling: true }); -// Mock price API call to prevent rate limiting during testing -const axios = require('axios'); -const MockAdapter = require('axios-mock-adapter'); - async function sendMsgByBlock( chronik: ChronikClient, telegramBot: TelegramBot, channelId: string, height: number, ) { // Need cache to pass to function const CACHE_TTL = 2 * config.cacheTtlMsecs; const memoryCache = await caching('memory', { max: 100, ttl: CACHE_TTL, }); // We do not need this value if we are not using the live API let hash = height.toString(); if (!liveApi) { // Mock price API const mock = new MockAdapter(axios, { onNoMatch: 'throwException' }); const mockResult = { bitcoin: { usd: 25000.0 }, ecash: { usd: 0.00003333 }, ethereum: { usd: 1900.0 }, }; mock.onGet(getCoingeckoApiUrl(config)).reply(200, mockResult); } else { // Get hash if we are using live API calls const block = await chronik.block(height); hash = block.blockInfo.hash; } const returnedMocks = (await handleBlockFinalized( chronik, telegramBot, channelId, hash, height, memoryCache, true, )) as StoredMock; const { blockSummaryTgMsgs, blockSummaryTgMsgsApiFailure } = returnedMocks; // Send msg with successful price API call await sendBlockSummary(blockSummaryTgMsgs, telegramBot, channelId, height); // Send msg with failed price API call await sendBlockSummary( blockSummaryTgMsgsApiFailure, telegramBot, channelId, ); console.log( '\x1b[32m%s\x1b[0m', `β Sent telegram msg for block ${height.toLocaleString()}`, ); process.exit(0); } sendMsgByBlock(chronik, telegramBotDev, channelId, height); diff --git a/apps/ecash-herald/src/chronik.ts b/apps/ecash-herald/src/chronik.ts index befa30f79..c27e0c639 100644 --- a/apps/ecash-herald/src/chronik.ts +++ b/apps/ecash-herald/src/chronik.ts @@ -1,266 +1,266 @@ // 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. import { ChronikClient, ScriptType, Tx, GenesisInfo, Utxo, } from 'chronik-client'; import { getEmojiFromBalanceSats } from './utils'; import cashaddr from 'ecashaddrjs'; // Max txs we can get in one request const CHRONIK_MAX_PAGESIZE = 200; export type TokenInfoMap = Map<string, GenesisInfo>; export const getTokenInfoMap = async ( chronik: ChronikClient, tokenIdSet: Set<string>, ) => { - let tokenInfoMap: TokenInfoMap = new Map(); + const tokenInfoMap: TokenInfoMap = new Map(); const tokenInfoPromises: Promise<void>[] = []; tokenIdSet.forEach(tokenId => { tokenInfoPromises.push( new Promise((resolve, reject) => { chronik.token(tokenId).then( response => { // Note: txDetails.slpTxData.genesisInfo only exists for token genesis txs try { const genesisInfo = response.genesisInfo; tokenInfoMap.set(tokenId, genesisInfo); resolve(); } catch (err) { console.log( `Error getting genesis info for ${tokenId}`, err, ); reject(err); } }, err => { reject(err); }, ); }), ); }); try { await Promise.all(tokenInfoPromises); } catch (err) { console.log(`Error in await Promise.all(tokenInfoPromises)`, err); // Print all tokenIds in event of error // Note: any 1 promise failing in Promise.all() will hit this // catch block console.log(`tokenIdSet:`); tokenIdSet.forEach(tokenId => { console.log(tokenId); }); return false; } return tokenInfoMap; }; export interface OutputscriptInfo { emoji: string; balanceSats: number; utxos: Utxo[]; } /** * Build a reference map of outputScripts and their balance in satoshis * @param {object} chronik * @param {set} outputScripts * @returns {map} addressInfoMap, a map with key = address, value = {balanceSats, emoji, utxos} */ export const getOutputscriptInfoMap = async ( chronik: ChronikClient, outputScripts: Set<string>, ): Promise<false | Map<string, OutputscriptInfo>> => { - let outputScriptInfoMap = new Map(); + const outputScriptInfoMap = new Map(); const outputScriptInfoPromises: Promise<void>[] = []; // For each outputScript, create a promise to get its balance and add // info related to this balance to outputScriptInfoMap outputScripts.forEach(outputScript => { // Decode output script const { type, hash } = cashaddr.getTypeAndHashFromOutputScript(outputScript); outputScriptInfoPromises.push( new Promise((resolve, reject) => { chronik .script(type as ScriptType, hash) .utxos() .then( response => { // If this address has no utxos, then utxos.length is 0 // If this address has utxos, then utxos = [{utxos: []}] const balanceSats = response.utxos.length === 0 ? 0 : response.utxos .map(utxo => utxo.value) .reduce( (prev, curr) => prev + curr, 0, ); // Set the map outputScript => emoji outputScriptInfoMap.set(outputScript, { emoji: getEmojiFromBalanceSats(balanceSats), balanceSats, utxos: response.utxos, }); resolve(); }, err => { reject(err); }, ); }), ); }); try { await Promise.all(outputScriptInfoPromises); } catch (err) { console.log( `Error in await Promise.all(outputScriptInfoPromises)`, err, ); // Print all outputScripts in event of error // Note: any 1 promise failing in Promise.all() will hit this // catch block console.log(`outputScripts:`); outputScripts.forEach(outputScript => { console.log(outputScript); }); return false; } return outputScriptInfoMap; }; /** * Get all txs in a block * Txs are paginated so this may require more than one API call * @param chronik * @param blockHeight * @throws on chronik error */ export const getAllBlockTxs = async ( chronik: ChronikClient, blockHeight: number, pageSize = CHRONIK_MAX_PAGESIZE, ): Promise<Tx[]> => { const firstPage = await chronik.blockTxs(blockHeight, 0, pageSize); const { txs, numPages } = firstPage; if (numPages === 1) { return txs; } const remainingPagesPromises: Promise<Tx[]>[] = []; // Start with i=1 as you already have the first page of txs, which corresponds with pagenum = 0 for (let i = 1; i < numPages; i += 1) { remainingPagesPromises.push( new Promise((resolve, reject) => { chronik.blockTxs(blockHeight, i, pageSize).then( result => { resolve(result.txs); }, err => { reject(err); }, ); }), ); } const remainingTxs = await Promise.all(remainingPagesPromises); // Combine all txs into an array return txs.concat(remainingTxs.flat()); }; /** * Get the start and end blockheights that will include all txs within a specified time period * Note: This function only works for time intervals relative to "right now" * We always return chaintip as the end height * @param chronik * @param now unix timestamp in seconds * @param secondsAgo how far back we are interested in getting blocks */ export const getBlocksAgoFromChaintipByTimestamp = async ( chronik: ChronikClient, now: number, secondsAgo: number, ): Promise<{ chaintip: number; startBlockheight: number }> => { // Get the chaintip const chaintip = (await chronik.blockchainInfo()).tipHeight; // Make an educated guess about how many blocks ago the first block we want should be // = 10 minutes per block * 60 seconds per minute const SECONDS_PER_BLOCK = 600; const guessedBlocksAgo = Math.floor(secondsAgo / SECONDS_PER_BLOCK); const guessedBlockheight = chaintip - guessedBlocksAgo; // Get the block from blocksAgo and check its timestamp const guessedBlock = (await chronik.block(guessedBlockheight)).blockInfo; let guessedBlockTimestampDelta = now - guessedBlock.timestamp; // We won't keep guessing forever const ADDITIONAL_BLOCKS_TO_GUESS = 200; let startBlockheight; if (guessedBlockTimestampDelta > secondsAgo) { // If the guessed block was further back in time than desired secondsAgo // Then we need to guess a higher block for ( let i = guessedBlockheight + 1; i <= guessedBlockheight + ADDITIONAL_BLOCKS_TO_GUESS; i += 1 ) { const guessedBlock = (await chronik.block(i)).blockInfo; const thisBlockTimestampDelta = now - guessedBlock.timestamp; if (thisBlockTimestampDelta <= secondsAgo) { startBlockheight = i; break; } } } else { // We might already be looking at the right block // But mb we some previous blocks are also in this acceptable window // If the guessed block was NOT further back in time than desired secondsAgo // Then we need to guess a LOWER block for ( let i = guessedBlockheight - 1; i >= guessedBlockheight - ADDITIONAL_BLOCKS_TO_GUESS; i -= 1 ) { const guessedBlock = (await chronik.block(i)).blockInfo; guessedBlockTimestampDelta = now - guessedBlock.timestamp; if (guessedBlockTimestampDelta > secondsAgo) { // We keep looking for blocks until we find one that is "too old" // Then we take the immediately newer block startBlockheight = i + 1; break; } } } if (typeof startBlockheight === 'undefined') { console.log( `Did not find startBlockheight in ${ADDITIONAL_BLOCKS_TO_GUESS} blocks`, ); console.log(`Chaintip: ${chaintip}`); console.log(`guessedBlockheight: ${guessedBlockheight}`); console.log( `guessedBlockTimestampDelta: ${guessedBlockTimestampDelta}`, ); throw new Error( `Start block more than ${ADDITIONAL_BLOCKS_TO_GUESS} off our original guess`, ); } return { chaintip, startBlockheight }; }; diff --git a/apps/ecash-herald/src/chronikWsHandler.ts b/apps/ecash-herald/src/chronikWsHandler.ts index 1ac0ed953..f47b4c5cb 100644 --- a/apps/ecash-herald/src/chronikWsHandler.ts +++ b/apps/ecash-herald/src/chronikWsHandler.ts @@ -1,86 +1,87 @@ -// Copyright (c) 2023 The Bitcoin developers +// 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. + import { ChronikClient, WsEndpoint, WsMsgClient } from 'chronik-client'; import { handleBlockFinalized, handleBlockInvalidated } from './events'; import TelegramBot from 'node-telegram-bot-api'; import { MemoryCache } from 'cache-manager'; import { MockTelegramBot } from '../test/mocks/telegramBotMock'; -export const initializeWebsocket = async ( - chronik: ChronikClient, - telegramBot: TelegramBot | MockTelegramBot, - channelId: string, - memoryCache: MemoryCache, -): Promise<WsEndpoint> => { - // Subscribe to chronik websocket - const ws = chronik.ws({ - onMessage: async msg => { - await parseWebsocketMessage( - chronik, - msg, - telegramBot, - channelId, - memoryCache, - ); - }, - }); - // Wait for WS to be connected: - await ws.waitForOpen(); - console.log(`Listening for chronik block msgs`); - // Subscribe to blocks - ws.subscribeToBlocks(); - return ws; -}; - export const parseWebsocketMessage = async ( chronik: ChronikClient, wsMsg: WsMsgClient, telegramBot: TelegramBot | MockTelegramBot, channelId: string, memoryCache: MemoryCache, ) => { // Get height and msg type // Note 1: herald only subscribes to blocks, so only MsgBlockClient is expected here // Note 2: blockTimestamp and coinbaseData might be undefined, they are // introduced in chronik v0.30.0 and client version 1.3.0 const { type } = wsMsg; if (type === 'Error') { // Do nothing on ws error msgs return false; } const { msgType } = wsMsg; switch (msgType) { case 'BLK_FINALIZED': { const { blockHeight, blockHash } = wsMsg; return handleBlockFinalized( chronik, telegramBot, channelId, blockHash, blockHeight, memoryCache, ); } case 'BLK_INVALIDATED': { // coinbaseData is defined for BLK_INVALIDATED const { blockHeight, blockHash, blockTimestamp, coinbaseData } = wsMsg; return handleBlockInvalidated( chronik, telegramBot, channelId, blockHash, blockHeight, blockTimestamp, coinbaseData!, memoryCache, ); } default: // Do nothing for other events return false; } }; + +export const initializeWebsocket = async ( + chronik: ChronikClient, + telegramBot: TelegramBot | MockTelegramBot, + channelId: string, + memoryCache: MemoryCache, +): Promise<WsEndpoint> => { + // Subscribe to chronik websocket + const ws = chronik.ws({ + onMessage: async msg => { + await parseWebsocketMessage( + chronik, + msg, + telegramBot, + channelId, + memoryCache, + ); + }, + }); + // Wait for WS to be connected: + await ws.waitForOpen(); + console.log(`Listening for chronik block msgs`); + // Subscribe to blocks + ws.subscribeToBlocks(); + return ws; +}; diff --git a/apps/ecash-herald/src/events.ts b/apps/ecash-herald/src/events.ts index 95caf7d58..87a28e89d 100644 --- a/apps/ecash-herald/src/events.ts +++ b/apps/ecash-herald/src/events.ts @@ -1,342 +1,342 @@ // 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'; import config from '../config'; import axios from 'axios'; import cashaddr from 'ecashaddrjs'; import { parseBlockTxs, getBlockTgMessage, getMinerFromCoinbaseTx, getStakerFromCoinbaseTx, guessRejectReason, summarizeTxHistory, HeraldParsedBlock, } from './parse'; import { getCoingeckoPrices, jsonReviver, getNextStakingReward, CoinGeckoPrice, + CoinGeckoResponse, } from './utils'; import { sendBlockSummary } from './telegram'; import { getTokenInfoMap, getOutputscriptInfoMap, getAllBlockTxs, getBlocksAgoFromChaintipByTimestamp, OutputscriptInfo, } from './chronik'; import knownMinersJson from '../constants/miners'; import { ChronikClient, CoinbaseData, Tx, GenesisInfo } from 'chronik-client'; import TelegramBot, { Message } from 'node-telegram-bot-api'; import { MemoryCache } from 'cache-manager'; import { MockTelegramBot } from '../test/mocks/telegramBotMock'; const miners = JSON.parse(JSON.stringify(knownMinersJson), jsonReviver); // This is expected for TelegramBot.sendMessage but is not available in its types // Based on Telegram API docs export interface SendMessageResponse { message_id: number; from: { id: number; is_bot: boolean; first_name: string; username: string; }; chat: { id: number; first_name: string; username: string; type: 'private'; }; date: number; text: string; } export interface StoredMock { blockTxs: Tx[]; parsedBlock: HeraldParsedBlock; - coingeckoResponse: any; + coingeckoResponse: CoinGeckoResponse; coingeckoPrices: CoinGeckoPrice[]; tokenInfoMap: Map<string, GenesisInfo>; outputScriptInfoMap: Map<string, OutputscriptInfo>; blockSummaryTgMsgs: string[]; blockSummaryTgMsgsApiFailure: string[]; } /** * Callback function for a new finalized block on the eCash blockchain * Summarize on-chain activity in this block * @param chronik * @param telegramBot A connected telegramBot instance * @param channelId The channel ID where the telegram msg(s) will be sent * @param height blockheight * @param returnMocks If true, return mocks for unit tests * @param memoryCache */ export const handleBlockFinalized = async ( chronik: ChronikClient, telegramBot: TelegramBot | MockTelegramBot, channelId: string, blockHash: string, blockHeight: number, memoryCache: MemoryCache, returnMocks = false, ): Promise< | StoredMock | Message | SendMessageResponse | boolean | (Message | SendMessageResponse)[] > => { // Get block txs // TODO blockTxs are paginated, need a function to get them all let blockTxs; try { blockTxs = await getAllBlockTxs(chronik, blockHeight); } catch (err) { console.log(`Error in getAllBlockTxs(${blockHeight})`, err); // Default Telegram message if chronik API error const errorTgMsg = `New Block Found\n` + `\n` + `${blockHeight.toLocaleString('en-US')}\n` + `\n` + `${blockHash}\n` + `\n` + `<a href="${config.blockExplorer}/block/${blockHash}">explorer</a>`; try { return (await telegramBot.sendMessage( channelId, errorTgMsg, config.tgMsgOptions, )) as Message | SendMessageResponse; } catch (err) { console.log( `Error in telegramBot.sendMessage(channelId=${channelId}, msg=${errorTgMsg}, options=${config.tgMsgOptions}) called from handleBlockFinalized`, err, ); return false; } } const parsedBlock = parseBlockTxs(blockHash, blockHeight, blockTxs); // Get token genesis info for token IDs in this block const { tokenIds, outputScripts } = parsedBlock; const tokenInfoMap = await getTokenInfoMap(chronik, tokenIds); const outputScriptInfoMap = await getOutputscriptInfoMap( chronik, outputScripts, ); // Get price info for tg msg, if available const resp = await getCoingeckoPrices(config.priceApi); const coingeckoPrices = resp !== false ? resp.coingeckoPrices : false; const coingeckoResponse = resp !== false ? resp.coingeckoResponse : false; const blockSummaryTgMsgs = getBlockTgMessage( parsedBlock, coingeckoPrices, tokenInfoMap, outputScriptInfoMap, ); if (returnMocks) { // returnMocks is used in the script function generateMocks // Using it as a flag here ensures the script is always using the same function // as the app // Note you need coingeckoResponse so you can mock the axios response for coingecko return { blockTxs, parsedBlock, coingeckoResponse, coingeckoPrices, tokenInfoMap, outputScriptInfoMap, blockSummaryTgMsgs, blockSummaryTgMsgsApiFailure: getBlockTgMessage( parsedBlock, false, // failed coingecko price lookup false, // failed chronik token ID lookup false, // failed balances lookup for output scripts ), } as StoredMock; } // Don't await, this can take some time to complete due to remote // caching. getNextStakingReward(blockHeight + 1, memoryCache); // Broadcast block summary telegram message(s) return await sendBlockSummary( blockSummaryTgMsgs, telegramBot, channelId, blockHeight, ); }; /** * Handle block invalidated event * @param {ChronikClient} chronik * @param {object} telegramBot * @param {string} channelId * @param {string} blockHash * @param {number} blockHeight * @param {number} blockTimestamp * @param {object} coinbaseData * @param {object} memoryCache */ export const handleBlockInvalidated = async ( chronik: ChronikClient, telegramBot: TelegramBot | MockTelegramBot, channelId: string, blockHash: string, blockHeight: number, blockTimestamp: number, coinbaseData: CoinbaseData, memoryCache: MemoryCache, ) => { const miner = getMinerFromCoinbaseTx( coinbaseData.scriptsig, coinbaseData.outputs, miners, ); const stakingRewardWinner = getStakerFromCoinbaseTx( blockHeight, coinbaseData.outputs, ); let stakingRewardWinnerAddress = 'unknown'; if (stakingRewardWinner !== false) { try { stakingRewardWinnerAddress = cashaddr.encodeOutputScript( stakingRewardWinner.staker, ); - } catch (err) { + } catch { // Use the script stakingRewardWinnerAddress = `script ${stakingRewardWinner.staker}`; } } const reason = await guessRejectReason( chronik, blockHeight, coinbaseData, memoryCache, ); const errorTgMsg = `Block invalidated by avalanche\n` + `\n` + `Height: ${blockHeight.toLocaleString('en-US')}\n` + `\n` + `Hash: ${blockHash}` + `\n` + `Timestamp: ${blockTimestamp}\n` + `Mined by ${miner}\n` + `Staking reward winner: ${stakingRewardWinnerAddress}\n` + `Guessed reject reason: ${reason}`; try { return await telegramBot.sendMessage( channelId, errorTgMsg, config.tgMsgOptions, ); } catch (err) { console.log( `Error in telegramBot.sendMessage(channelId=${channelId}, msg=${errorTgMsg}, options=${config.tgMsgOptions}) called from handleBlockInvalidated`, err, ); } }; export const handleUtcMidnight = async ( chronik: ChronikClient, telegramBot: TelegramBot, channelId: string, ) => { // It is a new day // Send the daily summary // Get a datestring // e.g. Wed Oct 23 2024 const dateString = new Date().toDateString(); // Get timestamp for UTC midnight // Will always be divisible by 1000 as will always be a midnight UTC date const MS_PER_S = 1000; const newDayTimestamp = new Date(dateString).getTime() / MS_PER_S; const SECONDS_PER_DAY = 86400; const { startBlockheight, chaintip } = await getBlocksAgoFromChaintipByTimestamp( chronik, newDayTimestamp, SECONDS_PER_DAY, ); const getAllBlockTxPromises = []; for (let i = startBlockheight; i <= chaintip; i += 1) { getAllBlockTxPromises.push(getAllBlockTxs(chronik, i)); } const allBlockTxs = (await Promise.all(getAllBlockTxPromises)).flat(); // We only want txs in the specified window // NB coinbase txs have timeFirstSeen of 0. We include all of them as the block // timestamps are in the window const timeFirstSeenTxs = allBlockTxs.filter( (tx: Tx) => (tx.timeFirstSeen > newDayTimestamp - SECONDS_PER_DAY && tx.timeFirstSeen <= newDayTimestamp) || tx.isCoinbase, ); // Get tokenIds of all tokens seen in this batch of txs const tokensToday: Set<string> = new Set(); for (const tx of timeFirstSeenTxs) { const { tokenEntries } = tx; for (const tokenEntry of tokenEntries) { const { tokenId, groupTokenId } = tokenEntry; tokensToday.add(tokenId); if (typeof groupTokenId !== 'undefined') { // We want the groupTokenId info even if we only have child txs in this window tokensToday.add(groupTokenId); } } } // Get all the token info of tokens from today const tokenInfoMap = await getTokenInfoMap(chronik, tokensToday); // Get XEC price and market info let priceInfo; try { priceInfo = ( await axios.get( `https://api.coingecko.com/api/v3/simple/price?ids=ecash&vs_currencies=usd&include_market_cap=true&include_24hr_vol=true&include_24hr_change=true`, ) ).data.ecash; } catch (err) { console.error(`Error getting daily summary price info`, err); } const dailySummaryTgMsgs = summarizeTxHistory( newDayTimestamp, timeFirstSeenTxs, tokenInfoMap, priceInfo, ); // Send msg with successful price API call await sendBlockSummary(dailySummaryTgMsgs, telegramBot, channelId, 'daily'); }; diff --git a/apps/ecash-herald/src/parse.ts b/apps/ecash-herald/src/parse.ts index 679f7ff0a..6ebfd8d02 100644 --- a/apps/ecash-herald/src/parse.ts +++ b/apps/ecash-herald/src/parse.ts @@ -1,3701 +1,3707 @@ // 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. import config from '../config'; import opReturn from '../constants/op_return'; import { consume, consumeNextPush, swapEndianness } from 'ecash-script'; import knownMinersJson, { KnownMiners, MinerInfo } from '../constants/miners'; import cachedTokenInfoMap from '../constants/tokens'; import { jsonReviver, bigNumberAmountToLocaleString, CoinGeckoPrice, } from '../src/utils'; import cashaddr from 'ecashaddrjs'; import BigNumber from 'bignumber.js'; import { TOKEN_SERVER_OUTPUTSCRIPT, BINANCE_OUTPUTSCRIPT, } from '../constants/senders'; import { prepareStringForTelegramHTML, splitOverflowTgMsg } from './telegram'; import { OutputscriptInfo } from './chronik'; import { formatPrice, satsToFormattedValue, returnAddressPreview, containsOnlyPrintableAscii, } from './utils'; import lokadMap from '../constants/lokad'; import { scriptOps } from 'ecash-agora'; import { Script, fromHex, OP_0 } from 'ecash-lib'; import { ChronikClient, CoinbaseData, Tx, TxOutput, GenesisInfo, } from 'chronik-client'; import { MemoryCache } from 'cache-manager'; const miners: KnownMiners = JSON.parse( JSON.stringify(knownMinersJson), jsonReviver, ); // 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'; interface PriceInfo { usd: number; usd_market_cap: number; usd_24h_vol: number; usd_24h_change: number; } interface HeraldStaker { staker: string; reward: number; } interface HeraldOpReturnInfo { app: string; msg: string; stackArray?: string[]; tokenId?: string | false; } interface TokenSendInfo { tokenId: string; parsedTokenType: string; txType: string; tokenChangeOutputs?: Map<string, BigNumber>; tokenReceivingOutputs?: Map<string, BigNumber>; tokenSendingOutputScripts?: Set<string>; } interface HeraldParsedTx { txid: string; genesisInfo: false | { tokenId: string }; opReturnInfo: false | HeraldOpReturnInfo; txFee: number; xecSendingOutputScripts: Set<string>; xecReceivingOutputs: Map<string, number>; totalSatsSent: number; tokenSendInfo: false | TokenSendInfo; tokenBurnInfo: | false | { tokenId: string; undecimalizedTokenBurnAmount: string; }; } export interface HeraldParsedBlock { hash: string; height: number; miner: string; staker: HeraldStaker | false; numTxs: number; parsedTxs: HeraldParsedTx[]; tokenIds: Set<string>; outputScripts: Set<string>; } enum TrackedTokenAction { Genesis = 'genesis', Send = 'send', Mint = 'mint', Burn = 'burn', Buy = 'buy', List = 'list', AdPrep = 'adPrep', Cancel = 'cancel', } interface TokenAction { count: number; } interface TokenActions { actionCount: number; send?: TokenAction; mint?: TokenAction; burn?: TokenAction; adPrep?: TokenAction; buy?: TokenAction; list?: TokenAction; cancel?: TokenAction; genesis?: | TokenAction | { hasBaton: boolean; amount: string; count?: number }; } -/** - * Parse a finalized block for newsworthy information - * @param blockHash - * @param blockHeight - * @param txs - */ -export const parseBlockTxs = ( - blockHash: string, - blockHeight: number, - txs: Tx[], -): HeraldParsedBlock => { - // Parse coinbase string - const coinbaseTx = txs[0]; - const miner = getMinerFromCoinbaseTx( - coinbaseTx.inputs[0].inputScript, - coinbaseTx.outputs, - miners, - ); - let staker = getStakerFromCoinbaseTx(blockHeight, coinbaseTx.outputs); - if (staker !== false) { - 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(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: Set<string> = new Set(); // we only need each tokenId once - // Collect outputScripts seen in this block to parse for balance - let outputScripts: Set<string> = 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: string[] = 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, - }; -}; export const getStakerFromCoinbaseTx = ( blockHeight: number, coinbaseOutputs: TxOutput[], ): HeraldStaker | false => { 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 => output.value) .reduce((prev, curr) => prev + curr, 0); - for (let output of coinbaseOutputs) { + for (const output of coinbaseOutputs) { const thisValue = 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; }; export const getMinerFromCoinbaseTx = ( coinbaseScriptsig: string, coinbaseOutputs: TxOutput[], knownMiners: KnownMiners, ): string => { // When you find the miner, minerInfo will come from knownMiners let minerInfo: boolean | MinerInfo = false; // First, check outputScripts for a known miner for (let i = 0; i < coinbaseOutputs.length; i += 1) { const thisOutputScript = coinbaseOutputs[i].outputScript; const knownMinerInfo = knownMiners.get(thisOutputScript); if (typeof knownMinerInfo !== 'undefined') { minerInfo = knownMinerInfo; 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; } } }; /** - * Parse an eCash tx as returned by chronik for newsworthy information + * 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 */ -export const parseTx = (tx: Tx): HeraldParsedTx => { - const { txid, inputs, outputs } = tx; +export const parseSlpTwo = (slpTwoPush: string): string => { + // Parse an empp push hex string with the SLP protocol identifier removed per SLP v2 spec + // https://ecashbuilders.notion.site/SLPv2-a862a4130877448387373b9e6a93dd97 - let isTokenTx = false; - let genesisInfo: false | { tokenId: string } = false; - let opReturnInfo: false | HeraldOpReturnInfo = false; + let msg = ''; - /* 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 - */ + // 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 + const stack = { remainingHex: slpTwoPush }; - /* 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 | TokenSendInfo = false; - let tokenSendingOutputScripts: Set<string> = new Set(); - let tokenReceivingOutputs = new Map(); - let tokenChangeOutputs = new Map(); - let undecimalizedTokenInputAmount = new BigNumber(0); + // 1.3: Read token type + // For now, this can only be 00. If not 00, unknown + const tokenType = consume(stack, 1); - // tokenBurn parsing variables - let tokenBurnInfo: - | false - | { - tokenId: string; - undecimalizedTokenBurnAmount: string; - } = false; + if (tokenType !== '00') { + msg += 'Unknown token type|'; + } - /* 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 - */ + // 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 - // xecSend parsing variables - let xecSendingOutputScripts: Set<string> = new Set(); - let xecReceivingOutputs = new Map(); - let xecInputAmountSats = 0; - let xecOutputAmountSats = 0; - let totalSatsSent = 0; - let changeAmountSats = 0; + const sectionType = Buffer.from( + consume(stack, sectionBytes), + 'hex', + ).toString('utf8'); + msg += sectionType; - if ( - tx.tokenStatus !== 'TOKEN_STATUS_NON_TOKEN' && - tx.tokenEntries.length > 0 - ) { - isTokenTx = true; + // Parsing differs depending on section type + // Note that SEND and MINT have same parsing - // We may have more than one token action in a given tx - // chronik will reflect this by having multiple entries in the tokenEntries array + const TOKEN_ID_BYTES = 32; + switch (sectionType) { + case 'SEND': + case 'MINT': { + // Next up is tokenId + const tokenId = swapEndianness(consume(stack, TOKEN_ID_BYTES)); - // For now, just parse the first action - // TODO handle txs with multiple tokenEntries - const parsedTokenAction = tx.tokenEntries[0]; + const cachedTokenInfo = cachedTokenInfoMap.get(tokenId); - const { tokenId, tokenType, txType, burnSummary, actualBurnAmount } = - parsedTokenAction; - const { protocol, number } = tokenType; - const isUnintentionalBurn = - burnSummary !== '' && actualBurnAmount !== '0'; + msg += `|<a href="${config.blockExplorer}/tx/${tokenId}">${ + typeof cachedTokenInfo === 'undefined' + ? `${tokenId.slice(0, 3)}...${tokenId.slice(-3)}` + : prepareStringForTelegramHTML(cachedTokenInfo.tokenTicker) + }</a>`; - // 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; + 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; } - 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) { - if (typeof input.outputScript !== 'undefined') { - xecSendingOutputScripts.add(input.outputScript); + case 'GENESIS': { + // TODO + // Have not seen one of these in the wild yet + break; } - 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 - if (typeof input.outputScript !== 'undefined') { - tokenSendingOutputScripts.add(input.outputScript); - } + 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; +}; - // 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 +/** + * 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 + */ +export const parseMultipushStack = ( + emppStackArray: string[], +): HeraldOpReturnInfo => { + // 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 - // Add outputScript and value to map - // If this outputScript is already in xecReceivingOutputs, increment its value - xecReceivingOutputs.set( - outputScript, - (xecReceivingOutputs.get(outputScript) ?? 0) + value, - ); + // So, parsing empp txs will require specific rules depending on the type of tx + const msgs = []; - // 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 = 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), - ); - } + // 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 = 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) } - - // 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, - }; + return { app: 'EMPP', msg: msgs.length > 0 ? msgs.join('|') : '' }; }; /** - * - * @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 + * Parse a stackArray according to OP_RETURN rules to convert to a useful tg msg + * @param stackArray an array containing a hex string for every push of this memo OP_RETURN outputScript + * @returns A useful string to describe this tx in a telegram msg */ -export const parseOpReturn = (opReturnHex: string): HeraldOpReturnInfo => { - // Initialize required vars - let app; - let msg; - let tokenId: string | false = 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); - } - } +export const parseMemoOutputScript = ( + stackArray: string[], +): HeraldOpReturnInfo => { + const app = opReturn.memo.app; + let msg = ''; - // Get the protocolIdentifier, the first push - const protocolIdentifier = stackArray[0]; + // 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); - // 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 parseMemoOutputScript(stackArray); + 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 += '|'; } - // 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 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'; + 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 opReturn.knownApps.airdrop.prefix: { - app = opReturn.knownApps.airdrop.app; + case '03': + /** + * 03 - Reply to memo + * <tx_hash> (32 bytes) + * <message> (1-184 bytes) + */ - // Initialize msg as empty string. Need tokenId info to complete. - msg = ''; + // 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 - // Airdrop tx has structure - // <prefix> <tokenId> + // 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>`; - // 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 + // Include a formatting spacer + msg += '|'; - // Valid Cashtab Msg - // <protocol identifier> <msg in utf8> - msg = - stackArray.length >= 2 - ? prepareStringForTelegramHTML( - Buffer.from(stackArray[1], 'hex').toString('utf8'), - ) - : `Invalid ${app}`; + // Add the reply + msg += prepareStringForTelegramHTML( + Buffer.from(stackArray[2], 'hex').toString('utf8'), + ); 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 + 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 opReturn.knownApps.fusionLegacy.prefix: - case opReturn.knownApps.fusion.prefix: { + case '06': + case '07': + case '16': + case '17': { /** - * Cash Fusion tx - * <protocolIdentifier> <sessionHash> - * https://github.com/cashshuffle/spec/blob/master/CASHFUSION.md + * Follow user - 06 <address> (20 bytes) + * Unfollow user - 07 <address> (20 bytes) + * Mute user - 16 <address> (20 bytes) + * Unmute user - 17 <address> (20 bytes) */ - app = opReturn.knownApps.fusion.app; - // The session hash is not particularly interesting to users - // Provide tx info in telegram prep function - msg = ''; + + // 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 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 = ''; + case '0a': { + // 01 - Set profile picture + // <url> (1-217 bytes) - 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]; - } + // 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 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]'; - } + 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 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]'; - } + 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 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]'; - } + 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; } - 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); + case '14': { + /** + * 14 - Poll Vote + * <poll_tx_hash> (32 bytes) + * <comment> (0-184 bytes) + */ - // Trim the msg for Telegram to avoid 200+ char msgs - const unknownMaxChars = 20; - if (msg.length > unknownMaxChars) { - msg = msg.slice(0, unknownMaxChars) + '...'; - } - } + // 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) + */ - return { app, msg, stackArray, tokenId }; -}; + // The address is a hex-encoded hash160 + // all memo addresses are p2pkh + const address = cashaddr.encode('ecash', 'P2PKH', stackArray[1]); -/** - * 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 - */ -export const parseMultipushStack = ( - emppStackArray: string[], -): HeraldOpReturnInfo => { - // 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 + // Link to the address in the msg + msg += `<a href="${ + config.blockExplorer + }/address/${address}">${returnAddressPreview(address)}</a>`; - // So, parsing empp txs will require specific rules depending on the type of tx - let msgs = []; + // Add a format spacer + msg += '|'; - // 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 = 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')}`, + // Add the msg + msg += prepareStringForTelegramHTML( + Buffer.from(stackArray[2], 'hex').toString('utf8'), ); + break; } - // Do not parse any other empp (haven't seen any in the wild, no existing specs to follow) + case '21': + case '22': + case '30': + case '31': + case '32': + case '35': { + /** + * https://github.com/memocash/mips/blob/master/mip-0009/mip-0009.md#specification + * + * These would require additional processing to get info about the specific tokens + * For now, not worth it. Just print the action. + * + * 21 - Link accept + * 22 - Link revoke + * 30 - Sell tokens + * 31 - Token buy offer + * 32 - Attach token sale signature + * 35 - Pin token post + */ + + // Remove formatting spacer + msg = msg.slice(0, -1); + break; + } + + default: + msg += `Unknown memo action`; } - return { app: 'EMPP', msg: msgs.length > 0 ? msgs.join('|') : '' }; + // Test for msgs that are intended for non-XEC audience + if (msg.includes('BCH')) { + msg = `[check memo.cash for msg]`; + } + return { app, msg }; }; /** - * 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 + * + * @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 */ -export const parseSlpTwo = (slpTwoPush: string): string => { - // Parse an empp push hex string with the SLP protocol identifier removed per SLP v2 spec - // https://ecashbuilders.notion.site/SLPv2-a862a4130877448387373b9e6a93dd97 - - let msg = ''; +export const parseOpReturn = (opReturnHex: string): HeraldOpReturnInfo => { + // Initialize required vars + let app; + let msg; + let tokenId: string | false = false; - // 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 }; + // Get array of pushes + const stack = { remainingHex: opReturnHex }; + const 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); + } + } - // 1.3: Read token type - // For now, this can only be 00. If not 00, unknown - const tokenType = consume(stack, 1); + // Get the protocolIdentifier, the first push + const protocolIdentifier = stackArray[0]; - if (tokenType !== '00') { - msg += 'Unknown token type|'; + // 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 parseMemoOutputScript(stackArray); } - // 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; + // 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 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> - // Parsing differs depending on section type - // Note that SEND and MINT have same parsing + 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'; - const TOKEN_ID_BYTES = 32; - switch (sectionType) { - case 'SEND': - case 'MINT': { - // Next up is tokenId - const tokenId = swapEndianness(consume(stack, TOKEN_ID_BYTES)); + break; + } + case opReturn.knownApps.airdrop.prefix: { + app = opReturn.knownApps.airdrop.app; - const cachedTokenInfo = cachedTokenInfoMap.get(tokenId); + // Initialize msg as empty string. Need tokenId info to complete. + msg = ''; - msg += `|<a href="${config.blockExplorer}/tx/${tokenId}">${ - typeof cachedTokenInfo === 'undefined' - ? `${tokenId.slice(0, 3)}...${tokenId.slice(-3)}` - : prepareStringForTelegramHTML(cachedTokenInfo.tokenTicker) - }</a>`; + // Airdrop tx has structure + // <prefix> <tokenId> - 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))); + // 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]; } - msg += - typeof cachedTokenInfo === 'undefined' - ? '' - : `|${bigNumberAmountToLocaleString( - totalAmountSent.toString(), - cachedTokenInfo.decimals, - )}`; 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 - case 'GENESIS': { - // TODO - // Have not seen one of these in the wild yet + // Valid Cashtab Msg + // <protocol identifier> <msg in utf8> + msg = + stackArray.length >= 2 + ? prepareStringForTelegramHTML( + Buffer.from(stackArray[1], 'hex').toString('utf8'), + ) + : `Invalid ${app}`; break; } - - case 'BURN': { - // TODO - // Have seen some in the wild but not in spec + 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; } - } - // 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 stackArray an array containing a hex string for every push of this memo OP_RETURN outputScript - * @returns A useful string to describe this tx in a telegram msg - */ -export const parseMemoOutputScript = ( - stackArray: string[], -): HeraldOpReturnInfo => { - 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': + case opReturn.knownApps.fusionLegacy.prefix: + case opReturn.knownApps.fusion.prefix: { /** - * 03 - Reply to memo - * <tx_hash> (32 bytes) - * <message> (1-184 bytes) + * 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 = ''; - // 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>`; + 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'; - // Include a formatting spacer - msg += '|'; + 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); - // Add the reply - msg += prepareStringForTelegramHTML( - Buffer.from(stackArray[2], 'hex').toString('utf8'), - ); - break; - case '04': - /** - * 04 - Like / tip memo <tx_hash> (32 bytes) - */ + // Trim the msg for Telegram to avoid 200+ char msgs + const unknownMaxChars = 20; + if (msg.length > unknownMaxChars) { + msg = msg.slice(0, unknownMaxChars) + '...'; + } + } - // 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>`; + return { app, msg, stackArray, tokenId }; +}; - // Include a formatting spacer - msg += '|'; +/** + * Parse an eCash tx as returned by chronik for newsworthy information + */ +export const parseTx = (tx: Tx): HeraldParsedTx => { + const { txid, inputs, outputs } = tx; - // Add the msg - msg += prepareStringForTelegramHTML( - Buffer.from(stackArray[2], 'hex').toString('utf8'), - ); + let isTokenTx = false; + let genesisInfo: false | { tokenId: string } = false; + let opReturnInfo: false | HeraldOpReturnInfo = false; - 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) - */ + /* 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 + */ - // The address is a hex-encoded hash160 - // all memo addresses are p2pkh - const address = cashaddr.encode('ecash', 'P2PKH', stackArray[1]); + /* 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 | TokenSendInfo = false; + const tokenSendingOutputScripts: Set<string> = new Set(); + const tokenReceivingOutputs = new Map(); + const tokenChangeOutputs = new Map(); + let undecimalizedTokenInputAmount = new BigNumber(0); - // 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) + // tokenBurn parsing variables + let tokenBurnInfo: + | false + | { + tokenId: string; + undecimalizedTokenBurnAmount: string; + } = false; - // 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) - */ + /* 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 + */ - // Add the topic - msg += prepareStringForTelegramHTML( - Buffer.from(stackArray[1], 'hex').toString('utf8'), - ); + // xecSend parsing variables + const xecSendingOutputScripts: Set<string> = new Set(); + const xecReceivingOutputs = new Map(); + let xecInputAmountSats = 0; + let xecOutputAmountSats = 0; + let totalSatsSent = 0; + let changeAmountSats = 0; - // Add a format spacer - msg += '|'; + if ( + tx.tokenStatus !== 'TOKEN_STATUS_NON_TOKEN' && + tx.tokenEntries.length > 0 + ) { + isTokenTx = true; - // Add the topic msg - msg += prepareStringForTelegramHTML( - Buffer.from(stackArray[2], 'hex').toString('utf8'), + // 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) { + if (typeof input.outputScript !== 'undefined') { + 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, ); - break; + // Collect the input outputScripts to identify change output + if (typeof input.outputScript !== 'undefined') { + tokenSendingOutputScripts.add(input.outputScript); + } } - 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'), + // 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, ); - break; + // Increment totalSatsSent + totalSatsSent += value; } - case '13': { - /** - * 13 Add poll option - * <poll_tx_hash> (32 bytes) - * <option> (1-184 bytes) - */ + // Don't parse OP_RETURN values of etoken txs, this info is available from chronik + if (outputScript.startsWith(opReturn.opReturnPrefix) && !isTokenTx) { + opReturnInfo = 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), + ); + } + } + } - // Only parse the option for now - msg += prepareStringForTelegramHTML( - Buffer.from(stackArray[2], 'hex').toString('utf8'), - ); + // Determine tx fee + const txFee = xecInputAmountSats - xecOutputAmountSats; - break; - } - case '14': { - /** - * 14 - Poll Vote - * <poll_tx_hash> (32 bytes) - * <comment> (0-184 bytes) - */ + // 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; + } - // We just want the comment - msg += prepareStringForTelegramHTML( - Buffer.from(stackArray[2], 'hex').toString('utf8'), - ); + // 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; + } - 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) - */ + return { + txid, + genesisInfo, + opReturnInfo, + txFee, + xecSendingOutputScripts, + xecReceivingOutputs, + totalSatsSent, + tokenSendInfo, + tokenBurnInfo, + }; +}; - // The address is a hex-encoded hash160 - // all memo addresses are p2pkh - const address = cashaddr.encode('ecash', 'P2PKH', stackArray[1]); +/** + * Parse a finalized block for newsworthy information + * @param blockHash + * @param blockHeight + * @param txs + */ +export const parseBlockTxs = ( + blockHash: string, + blockHeight: number, + txs: Tx[], +): HeraldParsedBlock => { + // Parse coinbase string + const coinbaseTx = txs[0]; + const miner = getMinerFromCoinbaseTx( + coinbaseTx.inputs[0].inputScript, + coinbaseTx.outputs, + miners, + ); + const staker = getStakerFromCoinbaseTx(blockHeight, coinbaseTx.outputs); + if (staker !== false) { + try { + staker.staker = cashaddr.encodeOutputScript(staker.staker); + } catch { + staker.staker = 'script(' + staker.staker + ')'; + } + } - // Link to the address in the msg - msg += `<a href="${ - config.blockExplorer - }/address/${address}">${returnAddressPreview(address)}</a>`; + // Start with i=1 to skip Coinbase tx + let parsedTxs = []; + for (let i = 1; i < txs.length; i += 1) { + parsedTxs.push(parseTx(txs[i])); + } - // Add a format spacer - msg += '|'; + // Sort parsedTxs by totalSatsSent, highest to lowest + parsedTxs = parsedTxs.sort((a, b) => { + return b.totalSatsSent - a.totalSatsSent; + }); - // Add the msg - msg += prepareStringForTelegramHTML( - Buffer.from(stackArray[2], 'hex').toString('utf8'), - ); - break; + // Collect token info needed to parse token send txs + const tokenIds: Set<string> = new Set(); // we only need each tokenId once + // Collect outputScripts seen in this block to parse for balance + const outputScripts: Set<string> = new Set(); + for (let i = 0; i < parsedTxs.length; i += 1) { + const thisParsedTx = parsedTxs[i]; + if (thisParsedTx.tokenSendInfo) { + tokenIds.add(thisParsedTx.tokenSendInfo.tokenId); } - 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; + 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; - 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]`; + // 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: string[] = 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 { app, msg }; + return { + hash: blockHash, + height: blockHeight, + miner, + staker, + numTxs: txs.length, + parsedTxs, + tokenIds, + outputScripts, + }; }; /** * Build a msg about an encrypted cashtab msg tx * @param sendingAddress * @param xecReceivingOutputs * @param coingeckoPrices * @returns msg */ export const getEncryptedCashtabMsg = ( sendingAddress: string, xecReceivingOutputs: Map<string, number>, totalSatsSent: number, coingeckoPrices: false | CoinGeckoPrice[], ): string => { - let displayedSentQtyString = satsToFormattedValue( + const displayedSentQtyString = satsToFormattedValue( totalSatsSent, coingeckoPrices, ); // Remove OP_RETURNs from xecReceivingOutputs - let receivingOutputscripts = []; + const 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 stackArray * @param airdropSendingAddress * @param airdropRecipientsMap * @param tokenInfo token info for the swapped token. optional. Bool False if API call failed. * @param coingeckoPrices object containing price info from coingecko. Bool False if API call failed. * @returns msg ready to send through Telegram API */ export const getAirdropTgMsg = ( stackArray: string[], airdropSendingAddress: string, airdropRecipientsMap: Map<string, number>, totalSatsAirdropped: number, tokenInfo: false | GenesisInfo, coingeckoPrices: false | CoinGeckoPrice[], ): string => { // 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( + const 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 stackArray * @param tokenInfo token info for the swapped token. optional. * @returns msg ready to send through Telegram API */ export const getSwapTgMsg = ( stackArray: string[], tokenInfo: false | GenesisInfo, ): string => { // 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 ${(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 = parseInt( Buffer.from(stackArray[9], 'hex').toString('ascii'), ); } else { minimum_sats_to_exchange_int = parseInt( stackArray[9], 16, ); } msg += '|'; msg += `Min trade: ${( 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) */ export const getBlockTgMessage = ( parsedBlock: HeraldParsedBlock, coingeckoPrices: false | CoinGeckoPrice[], tokenInfoMap: false | Map<string, GenesisInfo>, outputScriptInfoMap: false | Map<string, OutputscriptInfo>, ): string[] => { 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: string[] = []; 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, url } = genesisInfoForThisToken!; + let { tokenTicker, tokenName } = genesisInfoForThisToken!; + const { url } = 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="${url}">[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 { app, msg } = opReturnInfo; + const { 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 = 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 = 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 = 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( + const 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 } = + const { 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!; + let { tokenTicker, tokenName } = thisTokenInfo!; + const { 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 = + const 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 = + const 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!; + let { tokenTicker } = thisTokenInfo!; + const { 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); + const 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; const xecSenderInfoMap = outputScriptInfoMap.get( firstXecSendingOutputScript!, ); xecSenderEmoji = typeof xecSenderInfoMap !== 'undefined' ? xecSenderInfoMap.emoji : ''; const xecReceiverInfoMap = outputScriptInfoMap.get( firstXecReceivingOutputScript!, ); xecReceiverEmoji = typeof xecReceiverInfoMap !== 'undefined' ? xecReceiverInfoMap.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 */ export const guessRejectReason = async ( chronik: ChronikClient, blockHeight: number, coinbaseData: CoinbaseData, memoryCache: MemoryCache, ): Promise<string | undefined> => { // 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: undefined | { address: string; scriptHex: string } = 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 = 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) { + } catch { // 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) { + } catch { // 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'; }; +/** + * Initialize action data for a token if not yet intialized + * Update action count if initialized + * @param tokenActionMap + * @param existingAction result from tokenActionMap.get(tokenId) + * @param tokenId + * @param action + */ +export const initializeOrIncrementTokenData = ( + tokenActionMap: Map<string, TokenActions>, + existingActions: undefined | TokenActions, + tokenId: string, + action: TrackedTokenAction, +) => { + tokenActionMap.set( + tokenId, + typeof existingActions === 'undefined' + ? { + [action]: { + count: 1, + }, + actionCount: 1, + } + : { + ...existingActions, + [action]: { + count: + action in existingActions + ? existingActions[action]!.count! + 1 + : 1, + }, + actionCount: existingActions.actionCount + 1, + }, + ); +}; + /** * 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 now unix timestamp in seconds * @param txs array of CONFIRMED Txs * @param tokenInfoMap tokenId => genesisInfo * @param priceInfo { usd, usd_market_cap, usd_24h_vol, usd_24h_change } */ export const summarizeTxHistory = ( now: number, txs: Tx[], tokenInfoMap: false | Map<string, GenesisInfo>, priceInfo?: PriceInfo, ): string[] => { const xecPriceUsd = typeof priceInfo !== 'undefined' ? priceInfo.usd : undefined; // Throw out any unconfirmed txs txs.filter(tx => typeof 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 appTxMap = new Map(); let totalStakingRewardSats = 0; let cashtabXecRewardCount = 0; let cashtabXecRewardSats = 0; let cashtabCachetRewardCount = 0; let binanceWithdrawalCount = 0; let binanceWithdrawalSats = 0; let slpFungibleTxs = 0; let appTxs = 0; let unknownLokadTxs = 0; // tokenId => {info, list, cancel, buy, adPrep, send, burn, mint, genesis: {genesisQty: <>, hasBaton: <>}} const tokenActions = new Map(); let invalidTokenEntries = 0; let nftNonAgoraTokenEntries = 0; let mintVaultTokenEntries = 0; let alpTokenEntries = 0; let newSlpTokensFixedSupply = 0; let newSlpTokensVariableSupply = 0; // Nft vars const nftActions = new Map(); const nftAgoraActions = new Map(); const uniqueAgoraNfts = new Set(); const uniqueNonAgoraNfts = new Set(); let agoraOneshotTxs = 0; let nftMints = 0; // Agora vars let agoraTxs = 0; const agoraActions = new Map(); for (const tx of txs) { const { inputs, outputs, block, tokenEntries, isCoinbase } = tx; if (isCoinbase) { // Coinbase tx - get miner and staker info const miner = getMinerFromCoinbaseTx( tx.inputs[0].inputScript, outputs, miners, ); if (miner.includes('ViaBTC')) { viaBtcBlocks += 1; // ViaBTC pool miner - let blocksFoundThisViaMiner = viabtcMinerMap.get(miner); + const 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); + const blocksFoundThisMiner = minerMap.get(miner); if (typeof blocksFoundThisMiner === 'undefined') { minerMap.set(miner, 1); } else { minerMap.set(miner, blocksFoundThisMiner + 1); } } const stakerInfo = 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); + const 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) { for (const tokenEntry of tokenEntries) { // Get the tokenId // Note that groupTokenId is only defined for NFT child const { tokenId, tokenType, txType, groupTokenId, isInvalid, actualBurnAmount, } = tokenEntry; const { type } = tokenType; if (isInvalid) { // TODO find this for test tx invalidTokenEntries += 1; // Log to console so if we see this tx, we can analyze it for parsing console.info( `Unparsed isInvalid tokenEntry in tx: ${tx.txid}`, ); // No other parsing for this tokenEntry continue; } if (type === 'ALP_TOKEN_TYPE_STANDARD') { // TODO ALP parsing alpTokenEntries += 1; // Log to console so if we see this tx, we can analyze it for parsing console.info( `Unparsed ALP_TOKEN_TYPE_STANDARD tokenEntry in tx: ${tx.txid}`, ); // No other parsing for this tokenEntry continue; } if (type === 'SLP_TOKEN_TYPE_MINT_VAULT') { // TODO mint valt parsing mintVaultTokenEntries += 1; // Log to console so if we see this tx, we can analyze it for parsing console.info( `Unparsed SLP_TOKEN_TYPE_MINT_VAULT tokenEntry in tx: ${tx.txid}`, ); // No other parsing for this tokenEntry continue; } if (type === 'SLP_TOKEN_TYPE_NFT1_CHILD') { if (typeof groupTokenId === 'undefined') { // Should never happen invalidTokenEntries += 1; // Log to console so if we see this tx, we can analyze it for parsing console.info( `Unparsed SLP_TOKEN_TYPE_NFT1_CHILD with undefined groupTokenId: ${tx.txid}`, ); // No other parsing for this tokenEntry continue; } // Note that we organize all NFT1 children by their collection for herald purposes // Parse NFT child tx switch (txType) { case 'NONE': { invalidTokenEntries += 1; // Log to console so if we see this tx, we can analyze it for parsing console.info( `Unparsed SLP_TOKEN_TYPE_NFT1_CHILD txType NONE tokenEntry in tx: ${tx.txid}`, ); // No other parsing for this tokenEntry continue; } case 'UNKNOWN': { invalidTokenEntries += 1; // Log to console so if we see this tx, we can analyze it for parsing console.info( `Unparsed SLP_TOKEN_TYPE_NFT1_CHILD txType UNKNOWN tokenEntry in tx: ${tx.txid}`, ); // No other parsing for this tokenEntry continue; } case 'GENESIS': { // NFT1 NFTs have special genesis, in that they burn 1 of the group // their txType is still genesis // For the herald, these are better represented as "NFT mints" than // "NFT1 Child Genesis" // But coding side, we organize them this way nftMints += 1; nftNonAgoraTokenEntries += 1; // See if we already have tokenActions at this tokenId const existingNftActions = nftActions.get(groupTokenId); initializeOrIncrementTokenData( nftActions, existingNftActions, groupTokenId, TrackedTokenAction.Genesis, ); uniqueNonAgoraNfts.add(tokenId); // No further parsing for this token entry continue; } case 'SEND': { // SEND may be Agora ONESHOT or Burn const existingNftActions = nftActions.get(groupTokenId); const existingNftAgoraActions = nftAgoraActions.get(groupTokenId); // For now, we assume that any p2sh token input is agora buy/cancel // and any p2sh token output is an ad setup tx // No other known cases of p2sh for token txs on ecash today // tho multisig is possible, no supporting wallets let isAgoraBuySellList = false; for (const input of inputs) { if (typeof input.token !== 'undefined') { const { outputScript, inputScript } = input; // A token input that is p2sh may be // a listing, an ad setup, a buy, or a cancel try { const { type } = cashaddr.getTypeAndHashFromOutputScript( outputScript!, ); if (type === 'p2sh') { // Note that a ONESHOT agora tx does not necessarily // have 0441475230 in the inputscript // But we do not have any other p2sh token input txs, so parse // Agora tx // For now, we know all listing txs only have a single p2sh input if (inputs.length === 1) { // Agora ONESHOT listing in collection groupTokenId initializeOrIncrementTokenData( nftAgoraActions, existingNftAgoraActions, groupTokenId, TrackedTokenAction.List, ); isAgoraBuySellList = true; // Stop processing inputs for this tx break; } // Check if this is a cancellation // See agora.ts from ecash-agora lib // For now, I don't think it makes sense to have an 'isCanceled' method from ecash-agora // This is a pretty specific application const ops = scriptOps( new Script( fromHex(inputScript), ), ); // isCanceled is always the last pushop (before redeemScript) const opIsCanceled = ops[ops.length - 2]; const isCanceled = opIsCanceled === OP_0; if (isCanceled) { // Agora ONESHOT cancel in collection groupTokenId initializeOrIncrementTokenData( nftAgoraActions, existingNftAgoraActions, groupTokenId, TrackedTokenAction.Cancel, ); isAgoraBuySellList = true; // Stop processing inputs for this tx break; } else { // Agora ONESHOT purchase initializeOrIncrementTokenData( nftAgoraActions, existingNftAgoraActions, groupTokenId, TrackedTokenAction.Buy, ); isAgoraBuySellList = true; // Stop processing inputs for this tx break; } } - } catch (err) { + } catch { console.error( `Error in cashaddr.getTypeAndHashFromOutputScript(${outputScript}) from txid ${tx.txid}`, ); // Do not parse it as an agora tx } // We don't need to find any other inputs for this case break; } } if (isAgoraBuySellList) { agoraOneshotTxs += 1; uniqueAgoraNfts.add(tokenId); // We have already processed this token tx continue; } // Check for ad prep tx let isAdPrep = false; for (const output of outputs) { if (typeof output.token !== 'undefined') { const { outputScript } = output; // We assume a p2sh token output is an ad setup tx // No other known use cases at the moment try { const { type } = cashaddr.getTypeAndHashFromOutputScript( outputScript, ); if (type === 'p2sh') { // Agora ONESHOT ad setup tx for collection groupTokenId initializeOrIncrementTokenData( nftAgoraActions, existingNftAgoraActions, groupTokenId, TrackedTokenAction.AdPrep, ); isAdPrep = true; break; // Stop iterating over outputs } - } catch (err) { + } catch { console.error( `Error in cashaddr.getTypeAndHashFromOutputScript(${outputScript}) for output from txid ${tx.txid}`, ); // Do not parse it as an agora tx } } } if (isAdPrep) { agoraOneshotTxs += 1; uniqueAgoraNfts.add(tokenId); // We have processed this tx as an Agora Ad setup tx // No further processing continue; } if (actualBurnAmount !== '0') { nftNonAgoraTokenEntries += 1; // Parse as burn // Note this is not currently supported in Cashtab initializeOrIncrementTokenData( nftActions, existingNftActions, groupTokenId, TrackedTokenAction.Burn, ); uniqueNonAgoraNfts.add(tokenId); // No further parsing continue; } // Parse as send initializeOrIncrementTokenData( nftActions, existingNftActions, groupTokenId, TrackedTokenAction.Send, ); nftNonAgoraTokenEntries += 1; uniqueNonAgoraNfts.add(tokenId); // No further parsing for this tokenEntry continue; } case 'MINT': { // We do not expect to see any MINT txs for NFT1 children // Some confusion as what crypto colloquially calls an "NFT Mint" // is NOT this type of mint, but a genesis tx // Run the map anyway in case we get it invalidTokenEntries += 1; // Log to console so if we see this tx, we can analyze it for parsing console.info( `Unparsed SLP_TOKEN_TYPE_NFT1_CHILD txType MINT tokenEntry in tx: ${tx.txid}`, ); const existingNftActions = nftActions.get(groupTokenId); initializeOrIncrementTokenData( nftActions, existingNftActions, tokenId, TrackedTokenAction.Mint, ); uniqueNonAgoraNfts.add(tokenId); // No further parsing for this tokenEntry continue; } case 'BURN': { const existingNftActions = nftActions.get(tokenId); initializeOrIncrementTokenData( nftActions, existingNftActions, tokenId, TrackedTokenAction.Burn, ); nftNonAgoraTokenEntries += 1; uniqueNonAgoraNfts.add(tokenId); // No further parsing for this tokenEntry continue; } default: // Can we get here? // Log for analysis if it happens invalidTokenEntries += 1; console.info( `Switch default token action for SLP_TOKEN_TYPE_NFT1_CHILD in tx: ${tx.txid}`, ); // No further analysis this tokenEntry continue; } } if (type === 'SLP_TOKEN_TYPE_FUNGIBLE') { slpFungibleTxs += 1; switch (txType) { case 'NONE': { invalidTokenEntries += 1; // Log to console so if we see this tx, we can analyze it for parsing console.info( `Unparsed SLP_TOKEN_TYPE_FUNGIBLE txType NONE tokenEntry in tx: ${tx.txid}`, ); // No other parsing for this tokenEntry continue; } case 'UNKNOWN': { invalidTokenEntries += 1; // Log to console so if we see this tx, we can analyze it for parsing console.info( `Unparsed SLP_TOKEN_TYPE_FUNGIBLE txType UNKNOWN tokenEntry in tx: ${tx.txid}`, ); // No other parsing for this tokenEntry continue; } case 'GENESIS': { const genesis = { amount: '0', hasBaton: false, }; // See if we already have tokenActions at this tokenId const existingActions = tokenActions.get(tokenId); for (const output of outputs) { if (typeof output.token !== 'undefined') { if (output.token.tokenId === tokenId) { // Per spec, SLP 1 genesis qty is always at output index 1 // But we iterate over all outputs to check for mint batons const { amount, isMintBaton } = output.token; if (isMintBaton) { newSlpTokensVariableSupply += 1; genesis.hasBaton = true; } else { newSlpTokensFixedSupply += 1; genesis.amount = amount; } } // We do not use initializeOrIncrementTokenData here // genesis does not follow the same structure // Count is not important but we have more info for genesis tokenActions.set( tokenId, typeof existingActions === 'undefined' ? { genesis, actionCount: 1 } : { ...existingActions, genesis, actionCount: existingActions.actionCount + 1, }, ); // No further parsing for this tokenEntry continue; } } break; } case 'SEND': { // SEND may be Agora or Burn const existingTokenActions = tokenActions.get(tokenId); const existingAgoraActions = agoraActions.get(tokenId); // For now, we assume that any p2sh token input is agora buy/cancel // and any p2sh token output is an ad setup tx // No other known cases of p2sh for token txs on ecash today // tho multisig is possible, no supporting wallets // mb parse for ad setup first, which is p2sh output? let isAgoraBuySellList = false; for (const input of inputs) { if (typeof input.token !== 'undefined') { const { outputScript, inputScript } = input; // A token input that is p2sh may be // a listing, an ad setup, a buy, or a cancel try { const { type } = cashaddr.getTypeAndHashFromOutputScript( outputScript!, ); if (type === 'p2sh') { // We are only parsing SLP agora txs here // A listing will have AGR0 lokad in input script const AGORA_LOKAD_STARTSWITH = '0441475230'; if ( inputScript.startsWith( AGORA_LOKAD_STARTSWITH, ) ) { // Agora tx // For now, we know all listing txs only have a single p2sh input if (inputs.length === 1) { // Agora listing initializeOrIncrementTokenData( agoraActions, existingAgoraActions, tokenId, TrackedTokenAction.List, ); isAgoraBuySellList = true; // Stop processing inputs for this tx break; } // Check if this is a cancellation // See agora.ts from ecash-agora lib // For now, I don't think it makes sense to have an 'isCanceled' method from ecash-agora // This is a pretty specific application const ops = scriptOps( new Script( fromHex(inputScript), ), ); // isCanceled is always the last pushop (before redeemScript) const opIsCanceled = ops[ops.length - 2]; const isCanceled = opIsCanceled === OP_0; if (isCanceled) { // Agora cancel initializeOrIncrementTokenData( agoraActions, existingAgoraActions, tokenId, TrackedTokenAction.Cancel, ); isAgoraBuySellList = true; // Stop processing inputs for this tx break; } else { // Agora purchase initializeOrIncrementTokenData( agoraActions, existingAgoraActions, tokenId, TrackedTokenAction.Buy, ); isAgoraBuySellList = true; // Stop processing inputs for this tx break; } } } - } catch (err) { + } catch { console.error( `Error in cashaddr.getTypeAndHashFromOutputScript(${outputScript}) from txid ${tx.txid}`, ); // Do not parse it as an agora tx } // We don't need to find any other inputs for this case break; } } if (isAgoraBuySellList) { agoraTxs += 1; // We have already processed this token tx continue; } // Check for ad prep tx let isAdPrep = false; for (const output of outputs) { if (typeof output.token !== 'undefined') { const { outputScript } = output; // We assume a p2sh token output is an ad setup tx // No other known use cases at the moment try { const { type } = cashaddr.getTypeAndHashFromOutputScript( outputScript, ); if (type === 'p2sh') { // Agora ad setup tx for SLP1 initializeOrIncrementTokenData( agoraActions, existingAgoraActions, tokenId, TrackedTokenAction.AdPrep, ); isAdPrep = true; break; // Stop iterating over outputs } - } catch (err) { + } catch { console.error( `Error in cashaddr.getTypeAndHashFromOutputScript(${outputScript}) for output from txid ${tx.txid}`, ); // Do not parse it as an agora tx } } } if (isAdPrep) { agoraTxs += 1; // We have processed this tx as an Agora Ad setup tx // No further processing continue; } // Parse as burn if (actualBurnAmount !== '0') { initializeOrIncrementTokenData( tokenActions, existingTokenActions, tokenId, TrackedTokenAction.Burn, ); // No further parsing continue; } // Parse as send initializeOrIncrementTokenData( tokenActions, existingTokenActions, tokenId, TrackedTokenAction.Send, ); // No further parsing for this tokenEntry continue; } case 'MINT': { const existingTokenActions = tokenActions.get(tokenId); initializeOrIncrementTokenData( tokenActions, existingTokenActions, tokenId, TrackedTokenAction.Mint, ); // No further parsing for this tokenEntry continue; } case 'BURN': { const existingTokenActions = tokenActions.get(tokenId); initializeOrIncrementTokenData( tokenActions, existingTokenActions, tokenId, TrackedTokenAction.Burn, ); // No further parsing for this tokenEntry continue; } default: // Can we get here? // Log for analysis if it happens invalidTokenEntries += 1; console.info( `Switch default token action in tx: ${tx.txid}`, ); // No further analysis this tokenEntry continue; } } } // No further action this tx continue; } const firstOutputScript = outputs[0].outputScript; 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, ); } } // 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>${new Date(now * 1000).toLocaleDateString('en-GB', { year: 'numeric', month: 'short', day: 'numeric', timeZone: 'UTC', })}</b>`, ); tgMsg.push( `${config.emojis.block}${blockCount.toLocaleString('en-US')} blocks`, ); tgMsg.push( `${config.emojis.arrowRight}${txs.length.toLocaleString('en-US')} txs`, ); tgMsg.push(''); // Market summary if (typeof priceInfo !== 'undefined') { const { usd_market_cap, usd_24h_vol, usd_24h_change } = priceInfo; tgMsg.push( `${ usd_24h_change > 0 ? config.emojis.priceUp : config.emojis.priceDown }<b>1 XEC = ${formatPrice( xecPriceUsd!, 'usd', )}</b> <i>(${usd_24h_change.toFixed(2)}%)</i>`, ); tgMsg.push( `Trading volume: $${usd_24h_vol.toLocaleString('en-US', { maximumFractionDigits: 0, })}`, ); tgMsg.push( `Market cap: $${usd_market_cap.toLocaleString('en-US', { maximumFractionDigits: 0, })}`, ); 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(''); } // Agora partials if (agoraTxs > 0) { // Zero out counters for sorting purposes agoraActions.forEach((agoraActionInfo, tokenId) => { // Note we do not check adPrep as any token with adPrep has listing const { buy, list, cancel } = agoraActionInfo; if (typeof buy === 'undefined') { agoraActionInfo.buy = { count: 0 }; } if (typeof list === 'undefined') { agoraActionInfo.list = { count: 0 }; } if (typeof cancel === 'undefined') { agoraActionInfo.cancel = { count: 0 }; } agoraActions.set(tokenId, agoraActionInfo); }); // Sort agoraActions by buys const sortedAgoraActions = new Map( [...agoraActions.entries()].sort( (keyValueArrayA, keyValueArrayB) => keyValueArrayB[1].buy.count - keyValueArrayA[1].buy.count, ), ); const agoraTokens = Array.from(sortedAgoraActions.keys()); const agoraTokenCount = agoraTokens.length; tgMsg.push( `${config.emojis.agora}${ config.emojis.token } <b><i>${agoraTxs.toLocaleString('en-US')} Agora token tx${ agoraTxs > 1 ? 's' : '' } from ${agoraTokenCount} token${ agoraTokenCount > 1 ? 's' : '' }</i></b>`, ); const AGORA_TOKENS_TO_SHOW = 10; // Handle case where we do not see as many agora tokens as our max const agoraTokensToShow = agoraTokenCount < AGORA_TOKENS_TO_SHOW ? agoraTokenCount : AGORA_TOKENS_TO_SHOW; const newsworthyAgoraTokens = agoraTokens.slice(0, agoraTokensToShow); if (agoraTokenCount > AGORA_TOKENS_TO_SHOW) { tgMsg.push(`<u>Top ${AGORA_TOKENS_TO_SHOW}</u>`); } // Emoji key tgMsg.push( `${config.emojis.agoraBuy}Buy, ${config.emojis.agoraList}List, ${config.emojis.agoraCancel}Cancel`, ); for (let i = 0; i < newsworthyAgoraTokens.length; i += 1) { const tokenId = newsworthyAgoraTokens[i]; const tokenActionInfo = sortedAgoraActions.get(tokenId); const genesisInfo = tokenInfoMap === false ? undefined : tokenInfoMap.get(tokenId); const { buy, list, cancel } = tokenActionInfo; tgMsg.push( `<a href="${config.blockExplorer}/tx/${tokenId}">${ typeof genesisInfo === 'undefined' ? `${tokenId.slice(0, 3)}...${tokenId.slice(-3)}` : genesisInfo.tokenName }</a>${ typeof genesisInfo === 'undefined' ? '' : genesisInfo.tokenTicker !== '' ? ` (${genesisInfo.tokenTicker})` : '' }: ${ buy.count > 0 ? `${config.emojis.agoraBuy}${ buy.count > 1 ? `x${buy.count}` : '' }` : '' }${ list.count > 0 ? `${config.emojis.agoraList}${ list.count > 1 ? `x${list.count}` : '' }` : '' }${ cancel.count > 0 ? `${config.emojis.agoraCancel}${ cancel.count > 1 ? `x${cancel.count}` : '' }` : '' }`, ); } // Newline after agora section tgMsg.push(''); } // Agora ONESHOT (NFTs) if (agoraOneshotTxs > 0) { // Zero out counters for sorting purposes nftAgoraActions.forEach((agoraActionInfo, tokenId) => { // Note we do not check adPrep as any token with adPrep has listing const { buy, list, cancel } = agoraActionInfo; if (typeof buy === 'undefined') { agoraActionInfo.buy = { count: 0 }; } if (typeof list === 'undefined') { agoraActionInfo.list = { count: 0 }; } if (typeof cancel === 'undefined') { agoraActionInfo.cancel = { count: 0 }; } nftAgoraActions.set(tokenId, agoraActionInfo); }); // Sort agoraActions by buys const sortedNftAgoraActions = new Map( [...nftAgoraActions.entries()].sort( (keyValueArrayA, keyValueArrayB) => keyValueArrayB[1].buy.count - keyValueArrayA[1].buy.count, ), ); const agoraNftCollections = Array.from(sortedNftAgoraActions.keys()); const agoraCollectionCount = agoraNftCollections.length; const agoraNftCount = uniqueAgoraNfts.size; tgMsg.push( `${config.emojis.agora}${ config.emojis.nft } <b><i>${agoraOneshotTxs.toLocaleString('en-US')} Agora NFT tx${ agoraTxs > 1 ? 's' : '' } from ${agoraNftCount} NFT${ agoraNftCount > 1 ? 's' : '' } in ${agoraCollectionCount} collection${ agoraCollectionCount > 1 ? 's' : '' }</i></b>`, ); const AGORA_COLLECTIONS_TO_SHOW = 10; // Handle case where we do not see as many agora tokens as our max const agoraCollectionsToShow = agoraCollectionCount < AGORA_COLLECTIONS_TO_SHOW ? agoraCollectionCount : AGORA_COLLECTIONS_TO_SHOW; const newsworthyAgoraCollections = agoraNftCollections.slice( 0, agoraCollectionsToShow, ); if (agoraCollectionCount > AGORA_COLLECTIONS_TO_SHOW) { tgMsg.push(`<u>Top ${AGORA_COLLECTIONS_TO_SHOW}</u>`); } // Repeat emoji key tgMsg.push( `${config.emojis.agoraBuy}Buy, ${config.emojis.agoraList}List, ${config.emojis.agoraCancel}Cancel`, ); for (let i = 0; i < newsworthyAgoraCollections.length; i += 1) { const tokenId = newsworthyAgoraCollections[i]; const tokenActionInfo = sortedNftAgoraActions.get(tokenId); const genesisInfo = tokenInfoMap === false ? undefined : tokenInfoMap.get(tokenId); const { buy, list, cancel } = tokenActionInfo; tgMsg.push( `<a href="${config.blockExplorer}/tx/${tokenId}">${ typeof genesisInfo === 'undefined' ? `${tokenId.slice(0, 3)}...${tokenId.slice(-3)}` : genesisInfo.tokenName }</a>${ typeof genesisInfo === 'undefined' ? '' : genesisInfo.tokenTicker !== '' ? ` (${genesisInfo.tokenTicker})` : '' }: ${ buy.count > 0 ? `${config.emojis.agoraBuy}${ buy.count > 1 ? `x${buy.count}` : '' }` : '' }${ list.count > 0 ? `${config.emojis.agoraList}${ list.count > 1 ? `x${list.count}` : '' }` : '' }${ cancel.count > 0 ? `${config.emojis.agoraCancel}${ cancel.count > 1 ? `x${cancel.count}` : '' }` : '' }`, ); } // Newline after agora section tgMsg.push(''); } // SLP 1 fungible summary if (slpFungibleTxs > 0) { // Sort tokenActions map by number of token actions const sortedTokenActions = new Map( [...tokenActions.entries()].sort( (keyValueArrayA, keyValueArrayB) => keyValueArrayB[1].actionCount - keyValueArrayA[1].actionCount, ), ); // nonAgoraTokens will probably include tokens with agora actions // It's just that we want to present how many tokens had non-agora actions const nonAgoraTokens = Array.from(sortedTokenActions.keys()); const nonAgoraTokenCount = nonAgoraTokens.length; tgMsg.push( `${config.emojis.token} <b><i>${slpFungibleTxs.toLocaleString( 'en-US', )} token tx${ slpFungibleTxs > 1 ? 's' : '' } from ${nonAgoraTokenCount} token${ nonAgoraTokenCount > 1 ? 's' : '' }</i></b>`, ); const NON_AGORA_TOKENS_TO_SHOW = 5; const nonAgoraTokensToShow = nonAgoraTokenCount < NON_AGORA_TOKENS_TO_SHOW ? nonAgoraTokenCount : NON_AGORA_TOKENS_TO_SHOW; const newsworthyTokens = nonAgoraTokens.slice(0, nonAgoraTokensToShow); if (nonAgoraTokenCount > NON_AGORA_TOKENS_TO_SHOW) { tgMsg.push(`<u>Top ${NON_AGORA_TOKENS_TO_SHOW}</u>`); } for (let i = 0; i < newsworthyTokens.length; i += 1) { const tokenId = newsworthyTokens[i]; const tokenActionInfo = sortedTokenActions.get(tokenId); const genesisInfo = tokenInfoMap === false ? undefined : tokenInfoMap.get(tokenId); const { send, genesis, burn, mint } = tokenActionInfo; tgMsg.push( `<a href="${config.blockExplorer}/tx/${tokenId}">${ typeof genesisInfo === 'undefined' ? `${tokenId.slice(0, 3)}...${tokenId.slice(-3)}` : genesisInfo.tokenName }</a>${ typeof genesisInfo === 'undefined' ? '' : genesisInfo.tokenTicker !== '' ? ` (${genesisInfo.tokenTicker})` : '' }: ${ typeof genesis !== 'undefined' ? config.emojis.tokenGenesis : '' }${ typeof send !== 'undefined' ? `${config.emojis.arrowRight}${ send.count > 1 ? `x${send.count}` : '' }` : '' }${ typeof burn !== 'undefined' ? `${config.emojis.tokenBurn}${ burn.count > 1 ? `x${burn.count}` : '' }` : '' }${ typeof mint !== 'undefined' ? `${config.emojis.tokenMint}${ mint.count > 1 ? `x${mint.count}` : '' }` : '' }`, ); } // Line break for new section tgMsg.push(''); } // NFT summary if (nftNonAgoraTokenEntries > 0) { // Sort tokenActions map by number of token actions const sortedNftActions = new Map( [...nftActions.entries()].sort( (keyValueArrayA, keyValueArrayB) => keyValueArrayB[1].actionCount - keyValueArrayA[1].actionCount, ), ); const collectionsWithNonAgoraActions = Array.from( sortedNftActions.keys(), ); const collectionsWithNonAgoraActionsCount = collectionsWithNonAgoraActions.length; // Note that uniqueNonAgoraNfts and uniqueAgoraNfts can have some of the same members // Some NFTs will have both agora actions and non-agora actions tgMsg.push( `${ config.emojis.nft } <b><i>${nftNonAgoraTokenEntries.toLocaleString('en-US')} NFT tx${ nftNonAgoraTokenEntries > 1 ? 's' : '' } from ${uniqueNonAgoraNfts.size} NFT${ uniqueNonAgoraNfts.size > 1 ? 's' : '' } in ${collectionsWithNonAgoraActionsCount} collection${ collectionsWithNonAgoraActionsCount > 1 ? 's' : '' }</i></b>`, ); const NON_AGORA_COLLECTIONS_TO_SHOW = 5; const nonAgoraCollectionsToShow = collectionsWithNonAgoraActionsCount < NON_AGORA_COLLECTIONS_TO_SHOW ? collectionsWithNonAgoraActionsCount : NON_AGORA_COLLECTIONS_TO_SHOW; const newsworthyCollections = collectionsWithNonAgoraActions.slice( 0, nonAgoraCollectionsToShow, ); if ( collectionsWithNonAgoraActionsCount > NON_AGORA_COLLECTIONS_TO_SHOW ) { tgMsg.push(`<u>Top ${NON_AGORA_COLLECTIONS_TO_SHOW}</u>`); } for (let i = 0; i < newsworthyCollections.length; i += 1) { const tokenId = newsworthyCollections[i]; const tokenActionInfo = sortedNftActions.get(tokenId); const genesisInfo = tokenInfoMap === false ? undefined : tokenInfoMap.get(tokenId); const { send, genesis, burn, mint } = tokenActionInfo; tgMsg.push( `<a href="${config.blockExplorer}/tx/${tokenId}">${ typeof genesisInfo === 'undefined' ? `${tokenId.slice(0, 3)}...${tokenId.slice(-3)}` : genesisInfo.tokenName }</a>${ typeof genesisInfo === 'undefined' ? '' : genesisInfo.tokenTicker !== '' ? ` (${genesisInfo.tokenTicker})` : '' }: ${ typeof genesis !== 'undefined' ? `${config.emojis.tokenGenesis}${ genesis.count > 1 ? `x${genesis.count}` : '' }` : '' }${ typeof send !== 'undefined' ? `${config.emojis.arrowRight}${ send.count > 1 ? `x${send.count}` : '' }` : '' }${ typeof burn !== 'undefined' ? `${config.emojis.tokenBurn}${ burn.count > 1 ? `x${burn.count}` : '' }` : '' }${ typeof mint !== 'undefined' ? `${config.emojis.tokenMint}${ mint.count > 1 ? `x${mint.count}` : '' }` : '' }`, ); } // Line break for new section tgMsg.push(''); } // Genesis and mints token summary const unparsedTokenEntries = alpTokenEntries > 0 || mintVaultTokenEntries > 0 || invalidTokenEntries > 0; const hasTokenSummaryLines = nftMints > 0 || newSlpTokensFixedSupply > 0 || newSlpTokensVariableSupply > 0 || unparsedTokenEntries; if (nftMints > 0) { tgMsg.push( `${config.emojis.nft} <b><i>${nftMints} NFT mint${ nftMints > 1 ? 's' : '' }</i></b>`, ); } if (newSlpTokensFixedSupply > 0) { tgMsg.push( `${ config.emojis.tokenFixed } <b><i>${newSlpTokensFixedSupply} new fixed-supply token${ newSlpTokensFixedSupply > 1 ? 's' : '' }</i></b>`, ); } if (newSlpTokensVariableSupply > 0) { tgMsg.push( `${ config.emojis.tokenMint } <b><i>${newSlpTokensVariableSupply} new variable-supply token${ newSlpTokensVariableSupply > 1 ? 's' : '' }</i></b>`, ); } // Unparsed token summary if (alpTokenEntries > 0) { tgMsg.push( `${config.emojis.alp} <b><i>${alpTokenEntries.toLocaleString( 'en-US', )} ALP tx${alpTokenEntries > 1 ? 's' : ''}</i></b>`, ); } if (mintVaultTokenEntries > 0) { tgMsg.push( `${ config.emojis.mintvault } <b><i>${mintVaultTokenEntries.toLocaleString( 'en-US', )} Mint Vault tx${mintVaultTokenEntries > 1 ? 's' : ''}</i></b>`, ); } if (invalidTokenEntries > 0) { tgMsg.push( `${ config.emojis.invalid } <b><i>${invalidTokenEntries.toLocaleString( 'en-US', )} invalid token tx${invalidTokenEntries > 1 ? 's' : ''}</i></b>`, ); } if (hasTokenSummaryLines) { tgMsg.push(''); } 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><i>${appTxs.toLocaleString( 'en-US', )} 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(''); } 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); }; - -/** - * Initialize action data for a token if not yet intialized - * Update action count if initialized - * @param tokenActionMap - * @param existingAction result from tokenActionMap.get(tokenId) - * @param tokenId - * @param action - */ -export const initializeOrIncrementTokenData = ( - tokenActionMap: Map<string, TokenActions>, - existingActions: undefined | TokenActions, - tokenId: string, - action: TrackedTokenAction, -) => { - tokenActionMap.set( - tokenId, - typeof existingActions === 'undefined' - ? { - [action]: { - count: 1, - }, - actionCount: 1, - } - : { - ...existingActions, - [action]: { - count: - action in existingActions - ? existingActions[action]!.count! + 1 - : 1, - }, - actionCount: existingActions.actionCount + 1, - }, - ); -}; diff --git a/apps/ecash-herald/src/telegram.ts b/apps/ecash-herald/src/telegram.ts index 9a5738cfa..65dafd6e1 100644 --- a/apps/ecash-herald/src/telegram.ts +++ b/apps/ecash-herald/src/telegram.ts @@ -1,155 +1,155 @@ // 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. import config from '../config'; import TelegramBot, { Message, SendMessageOptions, } from 'node-telegram-bot-api'; import { MockTelegramBot } from '../test/mocks/telegramBotMock'; import { SendMessageResponse } from './events'; // undocumented API behavior of HTML parsing mode, discovered through brute force const TG_MSG_MAX_LENGTH = 4096; export const prepareStringForTelegramHTML = (string: string): string => { /* See "HTML Style" at https://core.telegram.org/bots/api Replace < with < Replace > with > Replace & with & */ let tgReadyString = string; // need to replace the '&' characters first tgReadyString = tgReadyString.replace(/&/g, '&'); tgReadyString = tgReadyString.replace(/</g, '<'); tgReadyString = tgReadyString.replace(/>/g, '>'); return tgReadyString; }; export const splitOverflowTgMsg = (tgMsgArray: string[]): string[] => { /* splitOverflowTgMsg * * Params * tgMsgArray - an array of unjoined strings prepared by getBlockTgMessage * each string has length <= 4096 characters * * Output * tgMsgStrings - an array of ready-to-broadcast HTML-parsed telegram messages, all under * the 4096 character limit */ // Iterate over tgMsgArray to build an array of messages under the TG_MSG_MAX_LENGTH ceiling const tgMsgStrings = []; let thisTgMsgStringLength = 0; let sliceStartIndex = 0; for (let i = 0; i < tgMsgArray.length; i += 1) { const thisLine = tgMsgArray[i]; // Account for the .join('\n'), each line has an extra 2 characters // Note: this is undocumented behavior of telegram API HTML parsing mode // '\n' is counted as 2 characters and also is parsed as a new line in HTML mode thisTgMsgStringLength += thisLine.length + 2; console.assert(thisLine.length + 2 <= TG_MSG_MAX_LENGTH, '%o', { length: thisLine.length + 2, line: thisLine, error: 'Telegram message line is longer than 4096 characters', }); // If this particular message line pushes the message over TG_MSG_MAX_LENGTH // less 2 as there is no `\n` at the end of the last line of the msg if (thisTgMsgStringLength - 2 > TG_MSG_MAX_LENGTH) { // Build a msg string with preceding lines, i.e. do not include this i'th line const sliceEndIndex = i; // Note that the slice end index is not included tgMsgStrings.push( tgMsgArray.slice(sliceStartIndex, sliceEndIndex).join('\n'), ); // Reset sliceStartIndex and thisTgMsgStringLength for the next message sliceStartIndex = sliceEndIndex; // Reset thisTgMsgStringLength to thisLine.length + 2; // The line of the current index will go into the next batched slice thisTgMsgStringLength = thisLine.length + 2; } } // Build a tg msg of all unused lines, if you have them if (sliceStartIndex < tgMsgArray.length) { tgMsgStrings.push(tgMsgArray.slice(sliceStartIndex).join('\n')); } return tgMsgStrings; }; export const sendBlockSummary = async ( tgMsgStrings: string[], telegramBot: TelegramBot | MockTelegramBot, channelId: string, blockheightOrMsgDesc?: number | string, ) => { /* sendBlockSummary * * Params * tgMsgStrings - an array of ready-to-be broadcast HTML-parsed telegram messages, * all under the 4096 character length limit * telegramBot - a telegram bot instance * channelId - the channel where the messages will be broadcast * * Output * Message(s) will be broadcast by telegramBot to channelId * If there are multiple messages, each message will be sent as a reply to its * preceding message * Function returns 'false' if there is an error in sending any one message * Function returns an array of msgSuccess objects for each successfully send msg */ let msgReplyId; - let msgSuccessArray = []; + const msgSuccessArray = []; for (let i = 0; i < tgMsgStrings.length; i += 1) { const thisMsg = tgMsgStrings[i]; let msgSuccess: Message | SendMessageResponse; const thisMsgOptions: SendMessageOptions = typeof msgReplyId === 'number' ? { ...config.tgMsgOptions, reply_to_message_id: msgReplyId, } : config.tgMsgOptions; try { msgSuccess = (await telegramBot.sendMessage( channelId, thisMsg, thisMsgOptions, )) as SendMessageResponse; msgReplyId = msgSuccess.message_id; msgSuccessArray.push(msgSuccess); } catch (err) { console.log( `Error in sending msg in sendBlockSummary, telegramBot.send(${thisMsg}) for msg ${ i + 1 } of ${tgMsgStrings.length}`, err, ); return false; } } if (msgSuccessArray.length === tgMsgStrings.length) { if (typeof blockheightOrMsgDesc === 'number') { console.log('\x1b[32m%s\x1b[0m', `β ${blockheightOrMsgDesc}`); } else if (blockheightOrMsgDesc === 'daily') { console.log( '\x1b[32m%s\x1b[0m', `β Sent daily summary of last 24 hrs`, ); } return msgSuccessArray; } // Catch potential edge case console.log({ msgsSent: msgSuccessArray.length, msgsAttempted: tgMsgStrings.length, error: 'Failed to send all messages', }); return false; }; diff --git a/apps/ecash-herald/src/utils.ts b/apps/ecash-herald/src/utils.ts index 6f738af40..9ef3704b0 100644 --- a/apps/ecash-herald/src/utils.ts +++ b/apps/ecash-herald/src/utils.ts @@ -1,479 +1,490 @@ // 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. import axios from 'axios'; import config, { HeraldConfig, HeraldPriceApi, FiatCode } from '../config'; import BigNumber from 'bignumber.js'; import addressDirectory from '../constants/addresses'; import { consume } from 'ecash-script'; import { MemoryCache } from 'cache-manager'; export const returnAddressPreview = ( cashAddress: string, sliceSize = 3, ): string => { // Check known addresses for a tag const addrInfo = addressDirectory.get(cashAddress); if (typeof addrInfo?.tag !== 'undefined') { return addrInfo.tag; } const addressParts = cashAddress.split(':'); const unprefixedAddress = addressParts[addressParts.length - 1]; return `${unprefixedAddress.slice( 0, sliceSize, )}...${unprefixedAddress.slice(-sliceSize)}`; }; /** * Get the price API url herald would use for specified config * @param config ecash-herald config object * @returns expected URL of price API call */ export const getCoingeckoApiUrl = (config: HeraldConfig): string => { return `${config.priceApi.apiBase}?ids=${config.priceApi.cryptos .map(crypto => crypto.coingeckoSlug) .join(',')}&vs_currencies=${ config.priceApi.fiat }&precision=${config.priceApi.precision.toString()}`; }; -// CoinGeckoResponse: any export interface CoinGeckoPrice { fiat: FiatCode; price: number; ticker: string; } +export interface CoinGeckoResponse { + bitcoin: { usd: number }; + ethereum: { usd: number }; + ecash: { usd: number }; +} interface GetCoingeckPricesResponse { - coingeckoResponse: any; + coingeckoResponse: CoinGeckoResponse; coingeckoPrices: CoinGeckoPrice[]; } export const getCoingeckoPrices = async ( priceInfoObj: HeraldPriceApi, ): Promise<false | GetCoingeckPricesResponse> => { const { apiBase, cryptos, fiat, precision } = priceInfoObj; - let coingeckoSlugs = cryptos.map(crypto => crypto.coingeckoSlug); - let apiUrl = `${apiBase}?ids=${coingeckoSlugs.join( + const coingeckoSlugs = cryptos.map(crypto => crypto.coingeckoSlug); + const apiUrl = `${apiBase}?ids=${coingeckoSlugs.join( ',', )}&vs_currencies=${fiat}&precision=${precision.toString()}`; // https://api.coingecko.com/api/v3/simple/price?ids=ecash,bitcoin,ethereum&vs_currencies=usd&precision=8 let coingeckoApiResponse; try { coingeckoApiResponse = await axios.get(apiUrl); const { data } = coingeckoApiResponse; // Validate for expected shape // For each key in `cryptoIds`, data must contain {<fiat>: <price>} - let coingeckoPriceArray = []; + const coingeckoPriceArray = []; if (data && typeof data === 'object') { for (let i = 0; i < coingeckoSlugs.length; i += 1) { const thisCoingeckoSlug = coingeckoSlugs[i]; if ( !data[thisCoingeckoSlug] || !data[thisCoingeckoSlug][fiat] ) { return false; } // Create more useful output format const thisPriceInfo = { fiat, price: data[thisCoingeckoSlug][fiat], ticker: cryptos.filter( el => el.coingeckoSlug === thisCoingeckoSlug, )[0].ticker, }; if (thisPriceInfo.ticker === 'XEC') { coingeckoPriceArray.unshift(thisPriceInfo); } else { coingeckoPriceArray.push(thisPriceInfo); } } return { coingeckoResponse: data, coingeckoPrices: coingeckoPriceArray, }; } return false; } catch (err) { console.log( `Error fetching prices of ${coingeckoSlugs.join( ',', )} from ${apiUrl}`, err, ); } return false; }; export const formatPrice = (price: number, fiatCode: FiatCode): string => { // Get symbol let fiatSymbol = config.fiatReference[fiatCode]; // If you can't find the symbol, don't show one if (typeof fiatSymbol === 'undefined') { fiatSymbol = ''; } // No decimal points for prices greater than 100 if (price > 100) { return `${fiatSymbol}${price.toLocaleString('en-US', { maximumFractionDigits: 0, })}`; } // 2 decimal places for prices between 1 and 100 if (price > 1) { return `${fiatSymbol}${price.toLocaleString('en-US', { maximumFractionDigits: 2, })}`; } // All decimal places for lower prices // For now, these will only be XEC prices return `${fiatSymbol}${price.toLocaleString('en-US', { maximumFractionDigits: 8, })}`; }; /** * Return a formatted string for a telegram msg given an amount of satoshis * * @param xecAmount amount of XEC as a number */ export const formatXecAmount = (xecAmount: number): string => { // Initialize displayed string variables let displayedAmount, descriptor; // Initialize displayedDecimals as 0 let displayedDecimals = 0; // Build format string for fixed levels if (xecAmount < 10) { // If xecAmount is less than 10, return un-rounded displayedAmount = xecAmount; descriptor = ''; displayedDecimals = 2; } else if (xecAmount < 1000) { displayedAmount = xecAmount; descriptor = ''; // If xecAmount is between 10 and 1k, return rounded } else if (xecAmount < 1000000) { // If xecAmount is between 1k and 1 million, return formatted + rounded displayedAmount = xecAmount / 1000; // thousands descriptor = 'k'; } else if (xecAmount < 1000000000) { // If xecAmount is between 1 million and 1 billion, return formatted + rounded displayedAmount = xecAmount / 1000000; // millions descriptor = 'M'; } else if (xecAmount < 1000000000000) { // If xecAmount is between 1 billion and 1 trillion, return formatted + rounded displayedAmount = xecAmount / 1000000000; // billions descriptor = 'B'; } else if (xecAmount >= 1000000000000) { // If xecAmount is greater than 1 trillion, return formatted + rounded displayedAmount = xecAmount / 1000000000000; descriptor = 'T'; } return `${displayedAmount!.toLocaleString('en-US', { maximumFractionDigits: displayedDecimals, })}${descriptor} XEC`; }; /** * Return a formatted string of fiat if price info is available and > $1 * Otherwise return formatted XEC amount * @param {integer} satoshis * @param {array or false} coingeckoPrices [{fiat, price}...{fiat, price}] with xec price at index 0 */ export const satsToFormattedValue = ( satoshis: number, coingeckoPrices?: false | CoinGeckoPrice[], ) => { // Get XEC qty const xecAmount = satoshis / 100; if (!coingeckoPrices) { return formatXecAmount(xecAmount); } // Get XEC price from index 0 const { fiat, price } = coingeckoPrices[0]; // Get fiat price - let fiatAmount = xecAmount * price; + const fiatAmount = xecAmount * price; const fiatSymbol: string = config.fiatReference[fiat] as string; // Format fiatAmount for different tiers let displayedAmount; let localeOptions: Intl.NumberFormatOptions = { maximumFractionDigits: 0 }; let descriptor = ''; if (fiatAmount === 0) { // Txs that send nothing, e.g. a one-input tx of 5.46 XEC, should keep defaults above } else if (fiatAmount < 0.01) { // enough decimal places to show one significant digit localeOptions = { minimumFractionDigits: -Math.floor(Math.log10(fiatAmount)), }; } else if (fiatAmount < 1) { // TODO two decimal places localeOptions = { minimumFractionDigits: 2 }; } if (fiatAmount < 1000) { displayedAmount = fiatAmount; descriptor = ''; } else if (fiatAmount < 1000000) { // thousands displayedAmount = fiatAmount / 1000; descriptor = 'k'; } else if (fiatAmount < 1000000000) { // millions displayedAmount = fiatAmount / 1000000; descriptor = 'M'; } else if (fiatAmount >= 1000000000) { // billions or more displayedAmount = fiatAmount / 1000000000; descriptor = 'B'; } return `${fiatSymbol}${displayedAmount!.toLocaleString( 'en-US', localeOptions, )}${descriptor}`; }; -export const jsonReplacer = function (key: any, value: any) { + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const jsonReplacer = function (key: string, value: any) { if (value instanceof Map) { const keyValueArray = Array.from(value.entries()); for (let i = 0; i < keyValueArray.length; i += 1) { const thisKeyValue = keyValueArray[i]; // [key, value] // If this is not an empty map if (typeof thisKeyValue !== 'undefined') { // Note: this value is an array of length 2 // [key, value] // Check if value is a big number if (thisKeyValue[1] instanceof BigNumber) { // Replace it thisKeyValue[1] = { // Note, if you use dataType: 'BigNumber', it will not work // This must be reserved // Use a term that is definitely not reserved but also recognizable as // "the dev means BigNumber here" dataType: 'BigNumberReplacer', value: thisKeyValue[1].toString(), }; } } } return { dataType: 'Map', value: keyValueArray, }; } else if (value instanceof Set) { return { dataType: 'Set', value: Array.from(value.keys()), }; } else { return value; } }; -export const jsonReviver = (key: any, value: any) => { + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const jsonReviver = (key: string, value: any) => { if (typeof value === 'object' && value !== null) { if (value.dataType === 'Map') { // If the map is not empty if (typeof value.value[0] !== 'undefined') { /* value.value is an array of keyValue arrays * e.g. * [ * [key1, value1], * [key2, value2], * [key3, value3], * ] */ // Iterate over each keyValue of the map for (let i = 0; i < value.value.length; i += 1) { const thisKeyValuePair = value.value[i]; // [key, value] - let thisValue = thisKeyValuePair[1]; + const thisValue = thisKeyValuePair[1]; if ( thisValue && thisValue.dataType === 'BigNumberReplacer' ) { // If this is saved BigNumber, replace it with an actual BigNumber // note, you can't use thisValue = new BigNumber(thisValue.value) // Need to use this specific array entry value.value[i][1] = new BigNumber( value.value[i][1].value, ); } } } return new Map(value.value); } if (value.dataType === 'Set') { return new Set(value.value); } } return value; }; /** * Convert a map to a key value array * Useful to generate test vectors by `console.log(mapToKeyValueArray(someMap))` in a function * @param {map} map * @returns array */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export const mapToKeyValueArray = (map: Map<any, any>): Array<[any, any]> => { - let kvArray: Array<[any, any]> = []; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const kvArray: Array<[any, any]> = []; map.forEach((value, key) => { kvArray.push([key, value]); }); return kvArray; }; /** * Assign appropriate emoji based on a balance in satoshis * @param balanceSats * @returns emoji determined by thresholds set in config */ export const getEmojiFromBalanceSats = (balanceSats: number): string => { const { whaleSats, emojis } = config; if (balanceSats >= whaleSats.bigWhale) { return emojis.bigWhale; } if (balanceSats >= whaleSats.modestWhale) { return emojis.modestWhale; } if (balanceSats >= whaleSats.shark) { return emojis.shark; } if (balanceSats >= whaleSats.swordfish) { return emojis.swordfish; } if (balanceSats >= whaleSats.barracuda) { return emojis.barracuda; } if (balanceSats >= whaleSats.octopus) { return emojis.octopus; } if (balanceSats >= whaleSats.piranha) { return emojis.piranha; } if (balanceSats >= whaleSats.crab) { return emojis.crab; } return emojis.shrimp; }; /** * Convert an integer-stored number with known decimals into a formatted decimal string * Useful for converting token send quantities to a human-readable string * @param {string} bnString an integer value as a string, e.g 100000012 * @param {number} decimals // in practice 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9, * @returns {string} e.g. 1,000,000.12 */ export const bigNumberAmountToLocaleString = ( bnString: string, decimals: number, ): string => { const totalLength = bnString.length; // Get the values that come after the decimal place const decimalValues = decimals === 0 ? '' : bnString.slice(-1 * decimals); const decimalLength = decimalValues.length; // Get the values that come before the decimal place const intValue = bnString.slice(0, totalLength - decimalLength); // Use toLocaleString() to format the amount before the decimal place with commas return `${BigInt(intValue).toLocaleString('en-US', { maximumFractionDigits: 0, })}${decimals !== 0 ? `.${decimalValues}` : ''}`; }; /** * Determine if an OP_RETURN's hex values include characters outside of printable ASCII range * @param {string} hexString hex string containing an even number of characters */ export const containsOnlyPrintableAscii = (hexString: string): boolean => { if (hexString.length % 2 !== 0) { // If hexString has an odd number of characters, it is certainly not ascii return false; } // Values lower than 32 are control characters (127 also control char) // We could tolerate LF and CR which are in this range, but they make // the msg awkward in Telegram -- so they are left out const MIN_ASCII_PRINTABLE_DECIMAL = 32; const MAX_ASCII_PRINTABLE_DECIMAL = 126; const stack = { remainingHex: hexString }; while (stack.remainingHex.length > 0) { const thisByte = parseInt(consume(stack, 1), 16); if ( thisByte > MAX_ASCII_PRINTABLE_DECIMAL || thisByte < MIN_ASCII_PRINTABLE_DECIMAL ) { return false; } } return true; }; interface StakingRewardApiResponse { previousBlockHash: string; nextBlockHeight: number; address: string; minimumValue: number; scriptHex: string; } /** * Get the expected next staking reward winner and store it in the memory * cache if the returned value targets the expected next block height. * @param {number} nextBlockHeight The next block height * @param {object} memoryCache The cache to store the result */ export const getNextStakingReward = async ( nextBlockHeight: number, memoryCache: MemoryCache, ): Promise<boolean> => { let retries = 10; while (retries > 0) { try { const nextStakingReward: StakingRewardApiResponse = ( await axios.get(config.stakingRewardApiUrl) ).data; if (nextStakingReward.nextBlockHeight === nextBlockHeight) { const { address, scriptHex } = nextStakingReward; const cachedObject: { scriptHex: string; address?: string } = { scriptHex: scriptHex, }; // Note: address can be undefined if (typeof address !== 'undefined') { cachedObject.address = address; } memoryCache.set(`${nextBlockHeight}`, cachedObject); return true; } } catch (err) { // Fallthrough + console.error(`Error in getting next staking reward`, err); } retries -= 1; // Wait for 2 seconds before retrying await new Promise(resolve => setTimeout(resolve, 2000)); } console.log( `Failed to fetch the expected staking reward for block ${nextBlockHeight}`, ); return false; }; diff --git a/apps/ecash-herald/test/chronikWsHandler.test.ts b/apps/ecash-herald/test/chronikWsHandler.test.ts index 348f42ec3..743f6965b 100644 --- a/apps/ecash-herald/test/chronikWsHandler.test.ts +++ b/apps/ecash-herald/test/chronikWsHandler.test.ts @@ -1,407 +1,407 @@ // 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. import assert from 'assert'; import config from '../config'; import cashaddr from 'ecashaddrjs'; import unrevivedBlock from './mocks/block'; import { jsonReviver, getCoingeckoApiUrl } from '../src/utils'; import { blockInvalidedTgMsg } from './mocks/blockInvalidated'; import { initializeWebsocket, parseWebsocketMessage, } from '../src/chronikWsHandler'; import { MockChronikClient } from '../../../modules/mock-chronik-client'; import { MockTelegramBot, mockChannelId } from './mocks/telegramBotMock'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { caching, MemoryCache } from 'cache-manager'; import { WsMsgClient } from 'chronik-client'; import { StoredMock } from '../src/events'; const block: StoredMock = JSON.parse( JSON.stringify(unrevivedBlock), jsonReviver, ); describe('ecash-herald chronikWsHandler.js', async function () { let memoryCache: MemoryCache; before(async () => { const CACHE_TTL = config.cacheTtlMsecs; memoryCache = await caching('memory', { max: 100, ttl: CACHE_TTL, }); }); it('initializeWebsocket returns expected websocket object for a p2pkh address', async function () { // Initialize chronik mock const mockedChronik = new MockChronikClient(); const telegramBot = new MockTelegramBot(); const channelId = mockChannelId; const result = await initializeWebsocket( mockedChronik, telegramBot, channelId, memoryCache, ); // Confirm websocket opened assert.strictEqual(mockedChronik.wsWaitForOpenCalled, true); // Confirm subscribed to blocks assert.deepEqual(result.subs.blocks, true); }); it('initializeWebsocket returns expected websocket object for a p2sh address', async function () { // Initialize chronik mock const mockedChronik = new MockChronikClient(); const telegramBot = new MockTelegramBot(); const channelId = mockChannelId; const result = await initializeWebsocket( mockedChronik, telegramBot, channelId, memoryCache, ); // Confirm websocket opened assert.strictEqual(mockedChronik.wsWaitForOpenCalled, true); // Confirm subscribed to blocks assert.deepEqual(result.subs.blocks, true); }); it('parseWebsocketMessage returns false for a msg other than BLK_CONNECTED, BLK_FINALIZED or BLK_INVALIDATED', async function () { // Initialize chronik mock const mockedChronik = new MockChronikClient(); const thisBlock = block; // Tell mockedChronik what response we expect for chronik.block(thisBlockHash) mockedChronik.setTxHistoryByBlock( thisBlock.parsedBlock.height, thisBlock.blockTxs, ); const telegramBot = new MockTelegramBot(); const channelId = mockChannelId; const unsupportedWebsocketMsgs = [ { msgType: 'BLK_DISCONNECTED', }, ]; for (let i = 0; i < unsupportedWebsocketMsgs.length; i += 1) { const thisUnsupportedMsg = unsupportedWebsocketMsgs[i]; const result = await parseWebsocketMessage( mockedChronik, thisUnsupportedMsg as WsMsgClient, telegramBot, channelId, memoryCache, ); // Check that sendMessage was not called assert.strictEqual(telegramBot.messageSent, false); assert.deepEqual(result, false); } }); it('parseWebsocketMessage creates and sends a telegram msg with prices and token send info for mocked block on successful API calls', async function () { // Initialize chronik mock const mockedChronik = new MockChronikClient(); const thisBlock = block; // Tell mockedChronik what response we expect for chronik.block(thisBlockHash) mockedChronik.setTxHistoryByBlock( thisBlock.parsedBlock.height, thisBlock.blockTxs, ); // Tell mockedChronik what response we expect for chronik.script(type, hash).utxos const { outputScriptInfoMap } = thisBlock; outputScriptInfoMap.forEach((info, outputScript) => { - let { type, hash } = + const { type, hash } = cashaddr.getTypeAndHashFromOutputScript(outputScript); const { utxos } = info; mockedChronik.setScript(type, hash); mockedChronik.setUtxos(type, hash, { outputScript, utxos }); }); // Tell mockedChronik what response we expect for chronik.tx const { parsedBlock, tokenInfoMap } = thisBlock; const { tokenIds } = parsedBlock; // Will only have chronik call if the set is not empty if (tokenIds.size > 0) { // Instead of saving all the chronik responses as mocks, which would be very large // Just set them as mocks based on tokenInfoMap, which contains the info we need tokenIds.forEach(tokenId => { mockedChronik.setMock('token', { input: tokenId, output: { genesisInfo: tokenInfoMap.get(tokenId), }, }); }); } const thisBlockExpectedMsgs = thisBlock.blockSummaryTgMsgs; // Mock a chronik websocket msg of correct format const mockWsMsg = { msgType: 'BLK_FINALIZED', blockHash: thisBlock.parsedBlock.hash, blockHeight: thisBlock.parsedBlock.height, }; const telegramBot = new MockTelegramBot(); const channelId = mockChannelId; // Mock coingecko price response // onNoMatch: 'throwException' helps to debug if mock is not being used const mock = new MockAdapter(axios, { onNoMatch: 'throwException', }); const mockResult = thisBlock.coingeckoResponse; // Mock a successful API request mock.onGet(getCoingeckoApiUrl(config)).reply(200, mockResult); // Mock a successful staking reward API request mock.onGet(config.stakingRewardApiUrl).reply(200, { nextBlockHeight: thisBlock.parsedBlock.height + 1, scriptHex: thisBlock.blockTxs[0].outputs[2].outputScript, address: cashaddr.encodeOutputScript( thisBlock.blockTxs[0].outputs[2].outputScript, ), }); const result = await parseWebsocketMessage( mockedChronik, mockWsMsg as WsMsgClient, telegramBot, channelId, memoryCache, ); // Build expected array of successful msg returns - let msgSuccessArray = []; + const msgSuccessArray = []; for (let i = 0; i < thisBlockExpectedMsgs.length; i += 1) { msgSuccessArray.push({ success: true, channelId, msg: thisBlockExpectedMsgs[i], options: config.tgMsgOptions, }); } // Check that the correct msg info was sent assert.deepEqual(result, msgSuccessArray); }); it('parseWebsocketMessage creates and sends a telegram msg without prices or token send info for mocked block on failed API calls', async function () { // Initialize chronik mock with successful blockTxs call const mockedChronik = new MockChronikClient(); const thisBlock = block; // Tell mockedChronik what response we expect for chronik.block(thisBlockHash) mockedChronik.setTxHistoryByBlock( thisBlock.parsedBlock.height, thisBlock.blockTxs, ); // Tell mockedChronik what response we expect for chronik.tx const { parsedBlock, tokenInfoMap } = thisBlock; const { tokenIds } = parsedBlock; // Will only have chronik call if the set is not empty if (tokenIds.size > 0) { // Instead of saving all the chronik responses as mocks, which would be very large // Just set them as mocks based on tokenInfoMap, which contains the info we need let index = 0; tokenIds.forEach(tokenId => { // If this is the first one, set an error response if (index === 0) { mockedChronik.setMock('token', { input: tokenId, output: new Error('some error'), }); } else { index += 1; mockedChronik.setMock('tx', { input: tokenId, output: { genesisInfo: tokenInfoMap.get(tokenId), }, }); } }); } const thisBlockExpectedMsgs = thisBlock.blockSummaryTgMsgsApiFailure; // Mock a chronik websocket msg of correct format const mockWsMsg = { msgType: 'BLK_FINALIZED', blockHash: thisBlock.parsedBlock.hash, blockHeight: thisBlock.parsedBlock.height, }; const telegramBot = new MockTelegramBot(); const channelId = mockChannelId; // Mock coingecko price response // onNoMatch: 'throwException' helps to debug if mock is not being used const mock = new MockAdapter(axios, { onNoMatch: 'throwException', }); // Mock a failed API request mock.onGet(getCoingeckoApiUrl(config)).reply(500, { error: 'error' }); // Mock a successful staking reward API request mock.onGet(config.stakingRewardApiUrl).reply(200, { nextBlockHeight: thisBlock.parsedBlock.height + 1, scriptHex: thisBlock.blockTxs[0].outputs[2].outputScript, address: cashaddr.encodeOutputScript( thisBlock.blockTxs[0].outputs[2].outputScript, ), }); const result = await parseWebsocketMessage( mockedChronik, mockWsMsg as WsMsgClient, telegramBot, channelId, memoryCache, ); // Build expected array of successful msg returns - let msgSuccessArray = []; + const msgSuccessArray = []; for (let i = 0; i < thisBlockExpectedMsgs.length; i += 1) { msgSuccessArray.push({ success: true, channelId, msg: thisBlockExpectedMsgs[i], options: config.tgMsgOptions, }); } // Check that sendMessage was called successfully assert.strictEqual(telegramBot.messageSent, true); // Check that the correct msg info was sent assert.deepEqual(result, msgSuccessArray); }); it('parseWebsocketMessage returns false if telegram msg fails to send', async function () { // Initialize chronik mock with successful blockTxs call const mockedChronik = new MockChronikClient(); const thisBlock = block; // Tell mockedChronik what response we expect for chronik.block(thisBlockHash) mockedChronik.setTxHistoryByBlock( thisBlock.parsedBlock.height, thisBlock.blockTxs, ); // Tell mockedChronik what response we expect for chronik.tx const { parsedBlock, tokenInfoMap } = thisBlock; const { tokenIds } = parsedBlock; // Will only have chronik call if the set is not empty if (tokenIds.size > 0) { // Instead of saving all the chronik responses as mocks, which would be very large // Just set them as mocks based on tokenInfoMap, which contains the info we need let index = 0; tokenIds.forEach(tokenId => { // If this is the first one, set an error response if (index === 0) { mockedChronik.setMock('token', { input: tokenId, output: new Error('some error'), }); } else { index += 1; mockedChronik.setMock('token', { input: tokenId, output: { genesisInfo: tokenInfoMap.get(tokenId), }, }); } }); } // Mock a chronik websocket msg of correct format const mockWsMsg = { type: 'BLK_FINALIZED', blockHash: thisBlock.parsedBlock.hash, blockHeight: thisBlock.parsedBlock.height, }; const telegramBot = new MockTelegramBot(); telegramBot.setExpectedError( 'sendMessage', 'Error: message failed to send', ); const channelId = mockChannelId; // Mock a successful staking reward API request const mock = new MockAdapter(axios, { onNoMatch: 'throwException', }); mock.onGet(config.stakingRewardApiUrl).reply(200, { nextBlockHeight: thisBlock.parsedBlock.height + 1, scriptHex: thisBlock.blockTxs[0].outputs[2].outputScript, address: cashaddr.encodeOutputScript( thisBlock.blockTxs[0].outputs[2].outputScript, ), }); const result = await parseWebsocketMessage( mockedChronik, mockWsMsg as WsMsgClient, telegramBot, channelId, memoryCache, ); // Check that the function returns false assert.strictEqual(result, false); }); it('parseWebsocketMessage creates and sends a telegram msg for invalidated blocks', async function () { // Initialize chronik mock const mockedChronik = new MockChronikClient(); const thisBlock = block; // Mock a chronik websocket msg of correct format const mockWsMsg = { msgType: 'BLK_INVALIDATED', blockHash: thisBlock.blockTxs[0].block!.hash, blockHeight: thisBlock.blockTxs[0].block!.height, blockTimestamp: thisBlock.blockTxs[0].block!.timestamp, coinbaseData: { scriptsig: thisBlock.blockTxs[0].inputs[0].inputScript, outputs: thisBlock.blockTxs[0].outputs, }, }; const telegramBot = new MockTelegramBot(); const channelId = mockChannelId; const result = await parseWebsocketMessage( mockedChronik, mockWsMsg as WsMsgClient, telegramBot, channelId, memoryCache, ); assert.strictEqual(telegramBot.messageSent, true); - let msgSuccess = { + const msgSuccess = { success: true, channelId, msg: blockInvalidedTgMsg, options: config.tgMsgOptions, }; // Check that the correct msg info was sent assert.deepEqual(result, msgSuccess); }); }); diff --git a/apps/ecash-herald/test/events.test.ts b/apps/ecash-herald/test/events.test.ts index e3ab06610..4c04b28a4 100644 --- a/apps/ecash-herald/test/events.test.ts +++ b/apps/ecash-herald/test/events.test.ts @@ -1,342 +1,342 @@ // 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. import assert from 'assert'; import config from '../config'; import unrevivedBlock from './mocks/block'; import { jsonReviver, getCoingeckoApiUrl } from '../src/utils'; import { blockInvalidedTgMsg } from './mocks/blockInvalidated'; import cashaddr from 'ecashaddrjs'; import { handleBlockFinalized, handleBlockInvalidated, StoredMock, } from '../src/events'; import { MockChronikClient } from '../../../modules/mock-chronik-client'; import { MockTelegramBot, mockChannelId } from './mocks/telegramBotMock'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { caching, MemoryCache } from 'cache-manager'; -import FakeTimers from '@sinonjs/fake-timers'; +import FakeTimers, { InstalledClock } from '@sinonjs/fake-timers'; const block: StoredMock = JSON.parse( JSON.stringify(unrevivedBlock), jsonReviver, ); describe('ecash-herald events.js', async function () { let memoryCache: MemoryCache; before(async () => { const CACHE_TTL = config.cacheTtlMsecs; memoryCache = await caching('memory', { max: 100, ttl: CACHE_TTL, }); }); - let clock: any; + let clock: InstalledClock; beforeEach(() => { clock = FakeTimers.install(); }); afterEach(() => { // Restore timers clock.uninstall(); }); it('handleBlockFinalized creates and sends a telegram msg with price and token send info for mocked block if api call succeeds', async function () { // Initialize chronik mock const mockedChronik = new MockChronikClient(); const thisBlock = block; // Tell mockedChronik what response we expect for chronik.block(thisBlockHash) mockedChronik.setTxHistoryByBlock( thisBlock.parsedBlock.height, thisBlock.blockTxs, ); // Tell mockedChronik what response we expect for chronik.script(type, hash).utxos const { outputScriptInfoMap } = thisBlock; outputScriptInfoMap.forEach((info, outputScript) => { - let { type, hash } = + const { type, hash } = cashaddr.getTypeAndHashFromOutputScript(outputScript); const { utxos } = info; mockedChronik.setScript(type, hash); mockedChronik.setUtxos(type, hash, { outputScript, utxos }); }); // Tell mockedChronik what response we expect for chronik.tx const { parsedBlock, tokenInfoMap } = thisBlock; const { tokenIds } = parsedBlock; // Will only have chronik call if the set is not empty if (tokenIds.size > 0) { // Instead of saving all the chronik responses as mocks, which would be very large // Just set them as mocks based on tokenInfoMap, which contains the info we need tokenIds.forEach(tokenId => { mockedChronik.setMock('token', { input: tokenId, output: { genesisInfo: tokenInfoMap.get(tokenId), }, }); }); } const thisBlockExpectedMsgs = thisBlock.blockSummaryTgMsgs; const telegramBot = new MockTelegramBot(); const channelId = mockChannelId; // Mock coingecko price response // onNoMatch: 'throwException' helps to debug if mock is not being used const mock = new MockAdapter(axios, { onNoMatch: 'throwException', }); const mockResult = thisBlock.coingeckoResponse; // Mock a successful API request mock.onGet(getCoingeckoApiUrl(config)).reply(200, mockResult); const result = await handleBlockFinalized( mockedChronik, telegramBot, channelId, thisBlock.parsedBlock.hash, thisBlock.parsedBlock.height, memoryCache, ); // Check that sendMessage was called successfully assert.strictEqual(telegramBot.messageSent, true); // Build expected array of successful msg returns - let msgSuccessArray = []; + const msgSuccessArray = []; for (let i = 0; i < thisBlockExpectedMsgs.length; i += 1) { msgSuccessArray.push({ success: true, channelId, msg: thisBlockExpectedMsgs[i], options: config.tgMsgOptions, }); } // Check that the correct msg info was sent assert.deepEqual(result, msgSuccessArray); }); it('handleBlockFinalized creates and sends a telegram msg without price or token info for mocked block if api calls fail', async function () { // Initialize chronik mock const mockedChronik = new MockChronikClient(); const thisBlock = block; // Tell mockedChronik what response we expect for chronik.block(thisBlockHash) mockedChronik.setTxHistoryByBlock( thisBlock.parsedBlock.height, thisBlock.blockTxs, ); // Tell mockedChronik what response we expect for chronik.tx const { parsedBlock, tokenInfoMap } = thisBlock; const { tokenIds } = parsedBlock; // Will only have chronik call if the set is not empty if (tokenIds.size > 0) { // Instead of saving all the chronik responses as mocks, which would be very large // Just set them as mocks based on tokenInfoMap, which contains the info we need let index = 0; tokenIds.forEach(tokenId => { // If this is the first one, set an error response if (index === 0) { mockedChronik.setMock('token', { input: tokenId, output: new Error('some error'), }); } else { index += 1; mockedChronik.setMock('token', { input: tokenId, output: { genesisInfo: tokenInfoMap.get(tokenId), }, }); } }); } const thisBlockExpectedMsgs = thisBlock.blockSummaryTgMsgsApiFailure; const telegramBot = new MockTelegramBot(); const channelId = mockChannelId; // Mock coingecko price response // onNoMatch: 'throwException' helps to debug if mock is not being used const mock = new MockAdapter(axios, { onNoMatch: 'throwException', }); // Mock a failed API request mock.onGet(getCoingeckoApiUrl(config)).reply(500, { error: 'error' }); const result = await handleBlockFinalized( mockedChronik, telegramBot, channelId, thisBlock.parsedBlock.hash, thisBlock.parsedBlock.height, memoryCache, ); // Check that sendMessage was called successfully assert.strictEqual(telegramBot.messageSent, true); // Build expected array of successful msg returns - let msgSuccessArray = []; + const msgSuccessArray = []; for (let i = 0; i < thisBlockExpectedMsgs.length; i += 1) { msgSuccessArray.push({ success: true, channelId, msg: thisBlockExpectedMsgs[i], options: config.tgMsgOptions, }); } // Check that the correct msg info was sent assert.deepEqual(result, msgSuccessArray); }); it('handleBlockFinalized sends desired backup msg if it encounters an error in message creation', async function () { // Initialize chronik mock const mockedChronik = new MockChronikClient(); const thisBlock = block; // Tell mockedChronik what response we expect for chronik.block(thisBlockHash) mockedChronik.setTxHistoryByBlock( thisBlock.parsedBlock.height, new Error('error getting block'), ); const telegramBot = new MockTelegramBot(); const channelId = mockChannelId; const result = await handleBlockFinalized( mockedChronik, telegramBot, channelId, thisBlock.parsedBlock.hash, thisBlock.parsedBlock.height, memoryCache, ); // Check that sendMessage was called successfully assert.strictEqual(telegramBot.messageSent, true); // Expect the backup msg const expectedMsg = `New Block Found\n\n${thisBlock.parsedBlock.height.toLocaleString( 'en-US', )}\n\n${ thisBlock.parsedBlock.hash }\n\n<a href="https://explorer.e.cash/block/${ thisBlock.parsedBlock.hash }">explorer</a>`; // Check that the correct msg info was sent assert.deepEqual(result, { success: true, channelId, msg: expectedMsg, options: config.tgMsgOptions, }); }); it('handleBlockFinalized returns false if it encounters an error in telegram bot sendMessage routine', async function () { // Initialize chronik mock const mockedChronik = new MockChronikClient(); const thisBlock = block; // Tell mockedChronik what response we expect for chronik.block(thisBlockHash) mockedChronik.setTxHistoryByBlock( thisBlock.parsedBlock.height, thisBlock.blockTxs, ); // Tell mockedChronik what response we expect for chronik.token const { parsedBlock, tokenInfoMap } = thisBlock; const { tokenIds } = parsedBlock; // Will only have chronik call if the set is not empty if (tokenIds.size > 0) { // Instead of saving all the chronik responses as mocks, which would be very large // Just set them as mocks based on tokenInfoMap, which contains the info we need let index = 0; tokenIds.forEach(tokenId => { // If this is the first one, set an error response if (index === 0) { mockedChronik.setMock('token', { input: tokenId, output: new Error('some error'), }); } else { index += 1; mockedChronik.setMock('token', { input: tokenId, output: { genesisInfo: tokenInfoMap.get(tokenId), }, }); } }); } const telegramBot = new MockTelegramBot(); telegramBot.setExpectedError( 'sendMessage', 'Error: message failed to send', ); const channelId = mockChannelId; const result = await handleBlockFinalized( mockedChronik, telegramBot, channelId, thisBlock.parsedBlock.hash, thisBlock.parsedBlock.height, memoryCache, ); // Check that the correct msg info was sent assert.deepEqual(result, false); }); it('handleBlockInvalidated creates and sends a telegram msg upon invalidated blocks', async function () { // Initialize chronik mock const mockedChronik = new MockChronikClient(); const thisBlock = block; const telegramBot = new MockTelegramBot(); const channelId = mockChannelId; const result = await handleBlockInvalidated( mockedChronik, telegramBot, channelId, thisBlock.blockTxs[0].block!.hash, thisBlock.blockTxs[0].block!.height, thisBlock.blockTxs[0].block!.timestamp, { scriptsig: thisBlock.blockTxs[0].inputs[0].inputScript, outputs: thisBlock.blockTxs[0].outputs, }, memoryCache, ); // Check that sendMessage was called successfully assert.strictEqual(telegramBot.messageSent, true); - let msgSuccess = { + const msgSuccess = { success: true, channelId, msg: blockInvalidedTgMsg, options: config.tgMsgOptions, }; // Check that the correct msg info was sent assert.deepEqual(result, msgSuccess); }); }); diff --git a/apps/ecash-herald/test/mocks/appTxSamples.ts b/apps/ecash-herald/test/mocks/appTxSamples.ts index 3a073032a..8576802d8 100644 --- a/apps/ecash-herald/test/mocks/appTxSamples.ts +++ b/apps/ecash-herald/test/mocks/appTxSamples.ts @@ -1,734 +1,733 @@ -// Copyright (c) 2023 The Bitcoin developers +// 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. -// Disable as these are "used" to match the expected tg format -/* eslint no-useless-escape: 0 */ + import { GenesisInfo } from 'chronik-client'; import opReturn from '../../constants/op_return'; import { CoinGeckoPrice } from '../../src/utils'; interface SwapMock { hex: string; msg: string; stackArray: string[]; tokenId: false | string; tokenInfo: false | GenesisInfo; } type RecipientEntry = [string, number]; // Define the type for the entire array type RecipientsArray = RecipientEntry[]; interface AirdropMock { txid: string; hex: string; stackArray: string[]; airdropSendingAddress: string; airdropRecipientsKeyValueArray: RecipientsArray; tokenId: false | string; tokenInfo: false | GenesisInfo; coingeckoPrices: CoinGeckoPrice[]; msg: string; msgApiFailure: string; } interface CashtabMsgMock { txid: string; hex: string; stackArray: string[]; msg: string; } interface EncryptedCashtabMsgMock { txid: string; hex: string; sendingAddress: string; xecReceivingOutputsKeyValueArray: RecipientsArray; stackArray: string[]; coingeckoPrices: CoinGeckoPrice[]; msg: string; msgApiFailure: string; } interface SlpTwoPushVector { push: string; msg: string; } interface SlpTwoTxVector { txid: string; hex: string; emppStackArray: string[]; msg: string; } interface AliasRegistrationVector { txid: string; hex: string; stackArray: string[]; msg: string; } interface PayButtonTx { txid: string; hex: string; stackArray: string[]; msg: string; } interface PaywallTxVector { txid: string; hex: string; stackArray: string[]; msg: string; } interface AuthTxVector { txid: string; hex: string; stackArray: string[]; msg: string; } interface AppTxSamples { swaps: SwapMock[]; airdrops: AirdropMock[]; cashtabMsgs: CashtabMsgMock[]; encryptedCashtabMsgs: EncryptedCashtabMsgMock[]; slp2PushVectors: SlpTwoPushVector[]; slp2TxVectors: SlpTwoTxVector[]; aliasRegistrations: AliasRegistrationVector[]; payButtonTxs: PayButtonTx[]; paywallTxs: PaywallTxVector[]; authenticationTxs: AuthTxVector[]; } const BASE_GENESIS_INFO: GenesisInfo = { tokenName: 'test', tokenTicker: 'test', url: 'https://cashtab.com/', decimals: 0, }; const appTxSamples: AppTxSamples = { // https://github.com/vinarmani/swap-protocol/blob/master/swap-protocol-spec.md swaps: [ // 0101 https://explorer.e.cash/tx/b03883ca0b106ea5e7113d6cbe46b9ec37ac6ba437214283de2d9cf2fbdc997f { hex: '045357500001010101204de69e374a8ed21cbddd47f2338cc0f479dc58daa2bbe11cd604ca488eca0ddf0453454c4c02025801002090dfb75fef5f07e384df4703b853a2741b8e6f3ef31ef8e5187a17fb107547f801010100', msg: 'Signal|SLP Atomic Swap|<a href="https://explorer.e.cash/tx/4de69e374a8ed21cbddd47f2338cc0f479dc58daa2bbe11cd604ca488eca0ddf">SPICE</a>|SELL for 6 XEC|Min trade: 0 XEC', stackArray: [ '53575000', '01', '01', '4de69e374a8ed21cbddd47f2338cc0f479dc58daa2bbe11cd604ca488eca0ddf', '53454c4c', '0258', '00', '90dfb75fef5f07e384df4703b853a2741b8e6f3ef31ef8e5187a17fb107547f8', '01', '00', ], tokenId: '4de69e374a8ed21cbddd47f2338cc0f479dc58daa2bbe11cd604ca488eca0ddf', tokenInfo: { ...BASE_GENESIS_INFO, tokenTicker: 'SPICE' }, }, // 0101 ascii example https://explorer.e.cash/tx/2308e1c36d8355edd86dd7d643da41994ab780c852fdfa8d032b1a337bf18bb6 // Sell price is hex, min price is ascii { hex: '04535750000101010120fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa0453454c4c01320100202b08df65b0b265be60fbc3346c70729d1378ddfca66da8e6645b74e26d75e61501010831303030303030300100', msg: `Signal|SLP Atomic Swap|<a href="https://explorer.e.cash/tx/fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa">GRP</a>|SELL for 0.5 XEC|Min trade: 100,000 XEC`, stackArray: [ '53575000', '01', '01', 'fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa', '53454c4c', '32', '00', '2b08df65b0b265be60fbc3346c70729d1378ddfca66da8e6645b74e26d75e615', '01', '3130303030303030', // ASCII for 10000000 or hex for 3,544,385,890,265,608,000, greater than total XEC supply '00', // Unknown extra info, seems like mb they mean for this to be 0 hex as the min sell amount ], tokenId: 'fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa', tokenInfo: { ...BASE_GENESIS_INFO, tokenTicker: 'GRP' }, }, // 0101 ascii 2, https://explorer.e.cash/tx/dfad6b85a8f0e4b338f4f3bc67d2b7f73fb27f82b6d71ad3e2be955643fe6e42 // Both are ascii { hex: '04535750000101010120b46c6e0a485f0fade147696e54d3b523071860fd745fbfa97a515846bd3019a60453454c4c0434343030010020c2e13f79c49f8825832f57df10985ecdd6e28253cf589ffe28e4e95ece174629010204343430300100', msg: 'Signal|SLP Atomic Swap|<a href="https://explorer.e.cash/tx/b46c6e0a485f0fade147696e54d3b523071860fd745fbfa97a515846bd3019a6">BTCinu</a>|SELL for 44 XEC|Min trade: 44 XEC', stackArray: [ '53575000', '01', '01', 'b46c6e0a485f0fade147696e54d3b523071860fd745fbfa97a515846bd3019a6', '53454c4c', '34343030', // ASCII 4400 '00', 'c2e13f79c49f8825832f57df10985ecdd6e28253cf589ffe28e4e95ece174629', '02', '34343030', // ASCII 4400 '00', // Unknown extra info, seems like mb they mean for this to be 0 hex as the min sell amount ], tokenId: 'b46c6e0a485f0fade147696e54d3b523071860fd745fbfa97a515846bd3019a6', tokenInfo: { ...BASE_GENESIS_INFO, tokenTicker: 'BTCinu' }, }, // 0101 ascii 3, https://explorer.e.cash/tx/e52daad4006ab27b9e103c7ca0e58bd483f8c6c377ba5075cf7f412fbb272971 // Recent gorbeious tx { hex: '04535750000101010120aebcae9afe88d61d8b8ed7b8c83c7c2a555583bf8f8591c94a2c9eb82f34816c0453454c4c093130303030303030300100206338e4674afaa2ef153187ae774ca5e26f0f3447e4dd398c9945b467056a28cf010201000566616c7365', msg: 'Signal|SLP Atomic Swap|<a href="https://explorer.e.cash/tx/aebcae9afe88d61d8b8ed7b8c83c7c2a555583bf8f8591c94a2c9eb82f34816c">GORB</a>|SELL for 1,000,000 XEC|Min trade: 0 XEC', stackArray: [ '53575000', '01', '01', 'aebcae9afe88d61d8b8ed7b8c83c7c2a555583bf8f8591c94a2c9eb82f34816c', '53454c4c', '313030303030303030', // ASCII 100,000,000 '00', '6338e4674afaa2ef153187ae774ca5e26f0f3447e4dd398c9945b467056a28cf', '02', '00', // hex 00 '66616c7365', // ASCII for 'false' ... does not match spec, mb used for something. Weird to do this in ASCII ], tokenId: 'aebcae9afe88d61d8b8ed7b8c83c7c2a555583bf8f8591c94a2c9eb82f34816c', tokenInfo: { ...BASE_GENESIS_INFO, tokenTicker: 'GORB' }, }, // 0102 https://explorer.e.cash/tx/70c2842e1b2c7eb49ee69cdecf2d6f3cd783c307c4cbeef80f176159c5891484 // Note, this example uses faulty pushdata at the end { hex: '045357500001010102202ee326cabee15bab127baad3aadbe39f18877933ea064203de5d08bba9654e69056a65746f6e0e657363726f772d706172656a617301002102f5515a2e17826c72011f608d2e8458580ea8cbaba3128abe7f4ae2df4d51572920b6919ed649c4710799cb01e2e66bf0fdb2eccee219fd8c4775d3a85431a9984f0101222102188904278ebf33059093f596a2697cf3668b3bec9a3a0c6408a455147ab3db934c000100', msg: 'Signal|Multi-Party Escrow', stackArray: [ '53575000', '01', '02', '2ee326cabee15bab127baad3aadbe39f18877933ea064203de5d08bba9654e69', '6a65746f6e', '657363726f772d706172656a6173', '00', '02f5515a2e17826c72011f608d2e8458580ea8cbaba3128abe7f4ae2df4d515729', 'b6919ed649c4710799cb01e2e66bf0fdb2eccee219fd8c4775d3a85431a9984f', '01', '2102188904278ebf33059093f596a2697cf3668b3bec9a3a0c6408a455147ab3db93', '00', ], tokenId: false, tokenInfo: false, }, // 0103 https://explorer.e.cash/tx/565c84990aacfbd006d4ed2ee14bfb0f3bb27a84a6c9adcabccb6fb8e17e64c5 { hex: '0453575000010101032668747470733a2f2f7377617063726f776466756e642e636f6d2f736f6d6563616d706169676e4502a0860100000000001976a914da74026d67264c0acfede38e8302704ef7d8cfb288acf0490200000000001976a914ac656e2dd5378ca9c45fd5cd44aa7da87c7bfa8288ac', msg: 'Signal|Threshold Crowdfunding', stackArray: [ '53575000', '01', '03', '68747470733a2f2f7377617063726f776466756e642e636f6d2f736f6d6563616d706169676e', '02a0860100000000001976a914da74026d67264c0acfede38e8302704ef7d8cfb288acf0490200000000001976a914ac656e2dd5378ca9c45fd5cd44aa7da87c7bfa8288ac', ], tokenId: false, tokenInfo: false, }, // 0201 | Payment - SLP Atomic Swap { hex: '045357500001020101206350c611819b7e84a2afd9611d33a98de5b3426c33561f516d49147dc1c4106b', msg: 'Payment|SLP Atomic Swap', stackArray: [ '53575000', '02', '01', '6350c611819b7e84a2afd9611d33a98de5b3426c33561f516d49147dc1c4106b', ], tokenId: false, tokenInfo: false, }, // 0202 N/A in spec, pending spotting in the wild // 0203 N/A in spec, pending spotting in the wild // Malformed swap { hex: '045357500001010105204de69e374a8ed21cbddd47f2338cc0f479dc58daa2bbe11cd604ca488eca0ddf0453454c4c02025801002090dfb75fef5f07e384df4703b853a2741b8e6f3ef31ef8e5187a17fb107547f801010100', msg: 'Signal|Invalid SWaP', stackArray: [ '53575000', '01', '05', // instead of 01 '4de69e374a8ed21cbddd47f2338cc0f479dc58daa2bbe11cd604ca488eca0ddf', '53454c4c', '0258', '00', '90dfb75fef5f07e384df4703b853a2741b8e6f3ef31ef8e5187a17fb107547f8', '01', '00', ], tokenId: false, tokenInfo: { ...BASE_GENESIS_INFO, tokenTicker: 'SPICE' }, }, // Mod 0101 https://explorer.e.cash/tx/b03883ca0b106ea5e7113d6cbe46b9ec37ac6ba437214283de2d9cf2fbdc997f { hex: '0453575000', msg: 'Invalid SWaP', stackArray: ['53575000'], tokenId: false, tokenInfo: false, }, // Mod 0101 with bad tokenId { hex: '0453575000010101011fe69e374a8ed21cbddd47f2338cc0f479dc58daa2bbe11cd604ca488eca0ddf0453454c4c02025801002090dfb75fef5f07e384df4703b853a2741b8e6f3ef31ef8e5187a17fb107547f801010100', msg: 'Signal|SLP Atomic Swap|Invalid tokenId|SELL for 6 XEC|Min trade: 0 XEC', stackArray: [ '53575000', '01', '01', 'e69e374a8ed21cbddd47f2338cc0f479dc58daa2bbe11cd604ca488eca0ddf', // 63 char, invalid tokenId '53454c4c', '0258', '00', '90dfb75fef5f07e384df4703b853a2741b8e6f3ef31ef8e5187a17fb107547f8', '01', '00', ], tokenId: false, tokenInfo: false, }, ], airdrops: [ // With a cashtab msg, non-empp { txid: 'f86c75efd60be3e46c032dfd45125f90d1730852141a7e742266473d12cad116', hex: '0464726f70207c06091e745037b46c5ea60def8ad526274c2caabb1fae6c4ac89fad02fedf9a04007461624643736f6e676f72206261627920686173206265656e20626f726e2e2054616b652074686973206c6974746c6520676966742066726f6d20612070726f75642066617468657221', stackArray: [ '64726f70', '7c06091e745037b46c5ea60def8ad526274c2caabb1fae6c4ac89fad02fedf9a', '00746162', '43736f6e676f72206261627920686173206265656e20626f726e2e2054616b652074686973206c6974746c6520676966742066726f6d20612070726f75642066617468657221', ], airdropSendingAddress: 'ecash:qqft3ujdpn45h0phqkwdw0nnxfu8y7zm7qdwlh5yd0', airdropRecipientsKeyValueArray: [ [ '6a0464726f70207c06091e745037b46c5ea60def8ad526274c2caabb1fae6c4ac89fad02fedf9a04007461624643736f6e676f72206261627920686173206265656e20626f726e2e2054616b652074686973206c6974746c6520676966742066726f6d20612070726f75642066617468657221', 0, ], ['76a9142ec5281864fc989dab543b054631c9703809689e88ac', 892857], ['76a914efa3a87fc4022378a5f7e8e0a5c112094f3fb9be88ac', 892857], ['76a9142a6572780193dbcb3422773c2e353048805c2cb888ac', 892857], ['76a914ce9650c2d64f487739798d2815ab9e0a38fe8f9b88ac', 89286], ['76a91473ef17c5b9f551eae3f3b4fadf61f93cae5e6aea88ac', 89286], ['76a9143efd4899993b5c6e554238187577b81ed1f6bb4188ac', 89286], ['76a914198d8c7a32b750cbdbe1f97103d404f5e6a9465788ac', 892857], ['76a91457499920b99c483d745f9925adf9eecbe46c583d88ac', 535714], ['76a9140d17fb5b181b676fc5ed2825c0b2b25cc578f3ea88ac', 89286], ['76a9142a96944d06700882bbd984761d9c9e4215f2d78e88ac', 446429], ['76a91469003998c2c32ac81951b88416a9a15df3a1992988ac', 89286], ], tokenId: '7c06091e745037b46c5ea60def8ad526274c2caabb1fae6c4ac89fad02fedf9a', tokenInfo: { tokenTicker: 'ePLK', tokenName: 'ePalinka', url: 'http://www.hungarikum.hu/en', hash: '', decimals: 3, }, coingeckoPrices: [ { fiat: 'usd', price: 0.00003333, ticker: 'XEC' }, { fiat: 'usd', price: 25000, ticker: 'BTC' }, { fiat: 'usd', price: 1900, ticker: 'ETH' }, ], msg: 'qqf...yd0 airdropped $2 to 12 holders of <a href="https://explorer.e.cash/tx/7c06091e745037b46c5ea60def8ad526274c2caabb1fae6c4ac89fad02fedf9a">ePLK</a>|Csongor baby has been born. Take this little gift from a proud father!', msgApiFailure: 'qqf...yd0 airdropped 50k XEC to 12 holders of <a href="https://explorer.e.cash/tx/7c06091e745037b46c5ea60def8ad526274c2caabb1fae6c4ac89fad02fedf9a">7c0...f9a</a>|Csongor baby has been born. Take this little gift from a proud father!', }, // no cashtab msg, non-empp { txid: '4403b0cc00ca159b64f219a7cc7cccd2e4440ddecbbcb6a0b82e78e350f8f72e', hex: '0464726f70201c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e0400746162', stackArray: [ '64726f70', '1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e', '00746162', // prefix for msg is there but no msg ], airdropSendingAddress: 'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48', airdropRecipientsKeyValueArray: [ [ '6a0464726f70201c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e0400746162', 0, ], ['76a9147ab07df481649eb27c7ad9afda52b2a93d2f722a88ac', 2000], ['76a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac', 1000], ['76a914b82361c5851f4ec48b995175a2e1c3646338e07688ac', 2000], ], tokenId: '1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e', tokenInfo: { tokenTicker: 'DET', tokenName: 'Dividend eToken', url: 'https://cashtab.com/', hash: '', decimals: 8, }, coingeckoPrices: [ { fiat: 'usd', price: 0.00003333, ticker: 'XEC' }, { fiat: 'usd', price: 25000, ticker: 'BTC' }, { fiat: 'usd', price: 1900, ticker: 'ETH' }, ], msg: 'qrm...r48 airdropped $0.002 to 4 holders of <a href="https://explorer.e.cash/tx/1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e">DET</a>', msgApiFailure: 'qrm...r48 airdropped 50 XEC to 4 holders of <a href="https://explorer.e.cash/tx/1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e">1c6...f5e</a>', }, // Token id is not 64 char { txid: 'mod of 4403b0cc00ca159b64f219a7cc7cccd2e4440ddecbbcb6a0b82e78e350f8f72e', hex: '0464726f701f6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e0400746162', stackArray: [ '64726f70', '6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e', // 63 chars '00746162', // prefix for msg is there but no msg ], airdropSendingAddress: 'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48', airdropRecipientsKeyValueArray: [ [ '6a0464726f701f6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e0400746162', 0, ], ['76a9147ab07df481649eb27c7ad9afda52b2a93d2f722a88ac', 2000], ['76a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac', 1000], ['76a914b82361c5851f4ec48b995175a2e1c3646338e07688ac', 2000], ], tokenId: false, tokenInfo: false, coingeckoPrices: [ { fiat: 'usd', price: 0.00003333, ticker: 'XEC' }, { fiat: 'usd', price: 25000, ticker: 'BTC' }, { fiat: 'usd', price: 1900, ticker: 'ETH' }, ], msg: 'Invalid Airdrop', msgApiFailure: 'Invalid Airdrop', }, // No stackArray[1] { txid: 'mod of 4403b0cc00ca159b64f219a7cc7cccd2e4440ddecbbcb6a0b82e78e350f8f72e', hex: '0464726f70', stackArray: ['64726f70'], airdropSendingAddress: 'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48', airdropRecipientsKeyValueArray: [ ['6a0464726f70', 0], ['76a9147ab07df481649eb27c7ad9afda52b2a93d2f722a88ac', 2000], ['76a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac', 1000], ['76a914b82361c5851f4ec48b995175a2e1c3646338e07688ac', 2000], ], tokenId: false, tokenInfo: false, coingeckoPrices: [ { fiat: 'usd', price: 0.00003333, ticker: 'XEC' }, { fiat: 'usd', price: 25000, ticker: 'BTC' }, { fiat: 'usd', price: 1900, ticker: 'ETH' }, ], msg: 'Invalid Airdrop', msgApiFailure: 'Invalid Airdrop', }, ], cashtabMsgs: [ { txid: 'b9c95c8cb8436be0574946071932aed3b82e5a8631d4de7a64ea29f7fba84759', hex: '04007461623165436173684461793a2058454346617563657420546f70757020666f722045617374657220456767206769766561776179', stackArray: [ '00746162', '65436173684461793a2058454346617563657420546f70757020666f722045617374657220456767206769766561776179', ], msg: 'eCashDay: XECFaucet Topup for Easter Egg giveaway', }, // no stackArray[1] { txid: 'N/A, mod of b9c95c8cb8436be0574946071932aed3b82e5a8631d4de7a64ea29f7fba84759', hex: '0400746162', stackArray: ['00746162'], msg: 'Invalid Cashtab Msg', }, ], encryptedCashtabMsgs: [ { txid: 'c9abffe59ef104408bf2fd4f602f76096a7ab78f2801ae320cadac53e1be4c5d', hex: '04657461624c810281d8b3db5585bf24903022d9c5f3b8cafed757f254840c0f7bc872fda070745cb6cef3d645fc7e4403e2bc212e616db6691ab415cd1f7e9abcebdd8738e775a05ebeb14fadbdbf5941e0e4804e0c075239d0906ca5d5c00a93ebae11df7770c4aeeaef5b804abca08c10520fa47a6dc3df018378334a15f7ea3075bc9b8840a8', sendingAddress: 'ecash:qq4fd9zdqecq3q4mmxz8v8vunepptukh3czav3gjyt', xecReceivingOutputsKeyValueArray: [ [ '6a04657461624c810281d8b3db5585bf24903022d9c5f3b8cafed757f254840c0f7bc872fda070745cb6cef3d645fc7e4403e2bc212e616db6691ab415cd1f7e9abcebdd8738e775a05ebeb14fadbdbf5941e0e4804e0c075239d0906ca5d5c00a93ebae11df7770c4aeeaef5b804abca08c10520fa47a6dc3df018378334a15f7ea3075bc9b8840a8', 0, ], [ '76a914f5f740bc76e56b77bcab8b4d7f888167f416fc6888ac', 24242424, ], ], stackArray: [ '65746162', '0281d8b3db5585bf24903022d9c5f3b8cafed757f254840c0f7bc872fda070745cb6cef3d645fc7e4403e2bc212e616db6691ab415cd1f7e9abcebdd8738e775a05ebeb14fadbdbf5941e0e4804e0c075239d0906ca5d5c00a93ebae11df7770c4aeeaef5b804abca08c10520fa47a6dc3df018378334a15f7ea3075bc9b8840a8', ], coingeckoPrices: [ { fiat: 'usd', price: 0.00003333, ticker: 'XEC' }, { fiat: 'usd', price: 25000, ticker: 'BTC' }, { fiat: 'usd', price: 1900, ticker: 'ETH' }, ], msg: 'qq4...jyt sent an encrypted message and $8 to qr6...xkv', msgApiFailure: 'qq4...jyt sent an encrypted message and 242k XEC to qr6...xkv', }, { txid: 'c43c01feb0563ed7293e86bccbc695fdce88807c4bf6dfb0b48134eb16d076b7', hex: '04657461624cd1034e582d3789a6ff5119c9ef243b4e126eacc51a922504439b2edba9cbb313a170a34e9e763943a59af3345867aa364ba8f089f992d1072cefedf02ce0dbb9b56023aa115ab180cc521b25553735f5ae58fbd2ff6bb750c710792dcbb356cf816604deec3ee9767e7a76fd757f617d174b127cef0b3162dc888075e99147b25014ab61d86108abae51421efbc060f6eff7edffff9d54f17f64ced6b0f973a8f44164b58337470a5a60d9f8ecb43a82dd8c989f409285e97ec3417426ceaca6a919a7b3bb8cdcfa679c9af950972ee43160', sendingAddress: 'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx', xecReceivingOutputsKeyValueArray: [ [ '6a04657461624cd1026d3ebca7776500d72ee640e56509cefcedee044b25584f0cc32d15c54766bc8960b179d07838f6ffb221c49c7f74d9a9bf4101cdb4a78d5507620ca020eab052d24995bcca37e9dd5b1baa210045b2942438e31a43062ef35c019250cef35dff2fd4b6999b98a103344d05c70847aa5124ac76d8528f737f4a504e96b46dbbe05b8a80bdc4b98bb0bb0f12ad12a3271550e79524ebae01dece0a231bfd546dab7714167bc73989613b73d94a5b48fbeda4913dbf42daedd52a3239a1654e4d3ded120b714eecffc3f3b1a37aed9e2d3b', 0, ], ['76a914f627e51001a51a1a92d8927808701373cf29267f88ac', 600], // manually give it another output to test ['76a914f5f740bc76e56b77bcab8b4d7f888167f416fc6888ac', 100], ], stackArray: [ '65746162', '034e582d3789a6ff5119c9ef243b4e126eacc51a922504439b2edba9cbb313a170a34e9e763943a59af3345867aa364ba8f089f992d1072cefedf02ce0dbb9b56023aa115ab180cc521b25553735f5ae58fbd2ff6bb750c710792dcbb356cf816604deec3ee9767e7a76fd757f617d174b127cef0b3162dc888075e99147b25014ab61d86108abae51421efbc060f6eff7edffff9d54f17f64ced6b0f973a8f44164b58337470a5a60d9f8ecb43a82dd8c989f409285e97ec3417426ceaca6a919a7b3bb8cdcfa679c9af950972ee43160', ], coingeckoPrices: [ { fiat: 'usd', price: 0.00003333, ticker: 'XEC' }, { fiat: 'usd', price: 25000, ticker: 'BTC' }, { fiat: 'usd', price: 1900, ticker: 'ETH' }, ], msg: 'qq9...fgx sent an encrypted message and $0.0002 to qrm...r48 and 1 other', msgApiFailure: 'qq9...fgx sent an encrypted message and 7 XEC to qrm...r48 and 1 other', }, ], slp2PushVectors: [ { push: '534c503200044d494e5445e1f25de444e399b6d46fa66e3424c04549a85a14b12bc9a4ddc9cdcdcdcdcd0150c30000000000', msg: 'MINT|<a href="https://explorer.e.cash/tx/cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145">CRD</a>|0.0', }, { push: '534c5032000453454e4445e1f25de444e399b6d46fa66e3424c04549a85a14b12bc9a4ddc9cdcdcdcdcd03204e00000000cc7400000000640000000000', msg: 'SEND|<a href="https://explorer.e.cash/tx/cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145">CRD</a>|0.142', }, { push: '534c503200044255524e45e1f25de444e399b6d46fa66e3424c04549a85a14b12bc9a4ddc9cdcdcdcdcd204e00000000', msg: 'BURN', }, ], slp2TxVectors: [ { txid: '05fbc4dcea9cc73e298b9f7bfe58de7b11dbbb3917c2bbdc5c9c93035e84b9fa', hex: '5032534c503200044d494e5445e1f25de444e399b6d46fa66e3424c04549a85a14b12bc9a4ddc9cdcdcdcdcd0150c30000000000', emppStackArray: [ '50', '534c503200044d494e5445e1f25de444e399b6d46fa66e3424c04549a85a14b12bc9a4ddc9cdcdcdcdcd0150c30000000000', ], msg: `${opReturn.knownApps.slp2.app}:MINT|<a href="https://explorer.e.cash/tx/cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145">CRD</a>|0.0`, }, { txid: '6f907d8d0f31315fbac8f860052e92899866869326f726393fd4fd4b5f7b8a7f', hex: '503d534c5032000453454e4445e1f25de444e399b6d46fa66e3424c04549a85a14b12bc9a4ddc9cdcdcdcdcd03204e00000000cc7400000000640000000000', emppStackArray: [ '50', '534c5032000453454e4445e1f25de444e399b6d46fa66e3424c04549a85a14b12bc9a4ddc9cdcdcdcdcd03204e00000000cc7400000000640000000000', ], - msg: `${opReturn.knownApps.slp2.app}:SEND|<a href=\"https://explorer.e.cash/tx/cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145\">CRD</a>|0.142`, + msg: `${opReturn.knownApps.slp2.app}:SEND|<a href="https://explorer.e.cash/tx/cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145">CRD</a>|0.142`, }, { txid: 'f0548510095dfbbe31cbeb27e3c0a340aabaad12f98d4ec6f563602a9f3f5499', hex: '5030534c503200044255524e45e1f25de444e399b6d46fa66e3424c04549a85a14b12bc9a4ddc9cdcdcdcdcd204e00000000', emppStackArray: [ '50', '534c503200044255524e45e1f25de444e399b6d46fa66e3424c04549a85a14b12bc9a4ddc9cdcdcdcdcd204e00000000', ], msg: `${opReturn.knownApps.slp2.app}:BURN`, }, { txid: 'c60db447e7eabee94100567953985ec245a02368e604dd8436733624af38aa3c', hex: '5030534c5032c8044255524e0748dae47347c2cf32838eaddedc60866160f0772a022b17463aa435809ac635102700000000', emppStackArray: [ '50', '534c5032c8044255524e0748dae47347c2cf32838eaddedc60866160f0772a022b17463aa435809ac635102700000000', ], msg: `${opReturn.knownApps.slp2.app}:Unknown token type|BURN`, }, { txid: 'e3e24259c06b6cc61647239f5bab24d4433747ab80456c72a641dc5219d81b94', hex: '503d534c5032c80453454e440748dae47347c2cf32838eaddedc60866160f0772a022b17463aa435809ac63503102700000000584d00000000640000000000', emppStackArray: [ '50', '534c5032c80453454e440748dae47347c2cf32838eaddedc60866160f0772a022b17463aa435809ac63503102700000000584d00000000640000000000', ], msg: `${opReturn.knownApps.slp2.app}:Unknown token type|SEND|<a href="https://explorer.e.cash/tx/35c69a8035a43a46172b022a77f060618660dcdead8e8332cfc24773e4da4807">35c...807</a>`, }, // Manually add a cashtab msg push { txid: 'e3e24259c06b6cc61647239f5bab24d4433747ab80456c72a641dc5219d81b94', hex: '503d534c5032c80453454e440748dae47347c2cf32838eaddedc60866160f0772a022b17463aa435809ac63503102700000000584d000000006400000000002e04007461622846726f6d20467265657865632c207468616e6b20796f7520666f7220796f757220737570706f7274', emppStackArray: [ '50', '534c5032c80453454e440748dae47347c2cf32838eaddedc60866160f0772a022b17463aa435809ac63503102700000000584d00000000640000000000', '04007461622846726f6d20467265657865632c207468616e6b20796f7520666f7220796f757220737570706f7274', ], - msg: `${opReturn.knownApps.slp2.app}:Unknown token type|SEND|<a href=\"https://explorer.e.cash/tx/35c69a8035a43a46172b022a77f060618660dcdead8e8332cfc24773e4da4807\">35c...807</a>|Unknown App:\u0004\u0000tab(From Freexec, thank you for your support`, + msg: `${opReturn.knownApps.slp2.app}:Unknown token type|SEND|<a href="https://explorer.e.cash/tx/35c69a8035a43a46172b022a77f060618660dcdead8e8332cfc24773e4da4807">35c...807</a>|Unknown App:\u0004\u0000tab(From Freexec, thank you for your support`, }, ], aliasRegistrations: [ { txid: 'dafea3b4ace4d56aec6aed106c6a654d7a1b0bc2f5bfa0599f679da77825e165', hex: '042e78656300046c616d6215000b7d35fda03544a08e65464d54cfae4257eb6db7', stackArray: [ '2e786563', '00', '6c616d62', '000b7d35fda03544a08e65464d54cfae4257eb6db7', ], msg: 'lamb', }, { txid: '79372d596c1dd14189720b5dc205350d46edfd0fffb108c717b9d0afbcf5869f', hex: '042e78656300046d6f6f6e15000b7d35fda03544a08e65464d54cfae4257eb6db7', stackArray: [ '2e786563', '00', '6d6f6f6e', '000b7d35fda03544a08e65464d54cfae4257eb6db7', ], msg: 'moon', }, { txid: '65c6afcf8a90d8b69729a0f048d736fda1a40451c3e83867c5a5f5a4c5226694', hex: '042e786563000670616e67616915000b7d35fda03544a08e65464d54cfae4257eb6db7', stackArray: [ '2e786563', '00', '70616e676169', '000b7d35fda03544a08e65464d54cfae4257eb6db7', ], msg: 'pangai', }, // Invalid alias tx with correct protocol identifier but incomplete stack { txid: 'N/A', hex: '042e78656305426f6f6d21', stackArray: ['2e786563', '426f6f6d21'], msg: 'Invalid alias registration', }, // Invalid alias tx with correct protocol identifier but empty stack after protocol identifier { txid: 'N/A', hex: '042e786563', stackArray: ['2e786563'], msg: 'Invalid alias registration', }, // Different version # for whatever reason { txid: 'N/A', hex: '042e78656301010670616e67616915000b7d35fda03544a08e65464d54cfae4257eb6db7', stackArray: [ '2e786563', '01', '70616e676169', '000b7d35fda03544a08e65464d54cfae4257eb6db7', ], msg: 'Invalid alias registration', }, ], payButtonTxs: [ // on spec tx with no data { txid: 'd1e7036e920ac9c2f50495641a4b9771c6c8f1e932304a5865096a6d3a514303', hex: '04504159000000089057dd10be17a66a', stackArray: ['50415900', '00', '00', '9057dd10be17a66a'], msg: 'no data', }, // on spec tx with data { txid: 'd1e7036e920ac9c2f50495641a4b9771c6c8f1e932304a5865096a6d3a514303', hex: '045041590000087465737464617461089057dd10be17a66a', stackArray: [ '50415900', '00', '7465737464617461', '9057dd10be17a66a', ], msg: 'testdata', }, // Unsupported version { txid: 'd1e7036e920ac9c2f50495641a4b9771c6c8f1e932304a5865096a6d3a514303', hex: '04504159000101087465737464617461089057dd10be17a66a', stackArray: [ '50415900', '01', '7465737464617461', '9057dd10be17a66a', ], msg: 'Unsupported version: 0x01', }, // Tx does not have enough pushes to parse { txid: 'd1e7036e920ac9c2f50495641a4b9771c6c8f1e932304a5865096a6d3a514303', hex: '04504159000101', stackArray: ['50415900', '01'], msg: '[off spec]', }, ], paywallTxs: [ // on spec paywall tx with a valid article txid { txid: 'f86d0b60e66888f8d30ea67167e5e4370fee29f13201cf5f32727d41c8a41868', hex: '0470617977200729318a128ee8f11d18b28237c8ae7ffa4e95c88ec69ebce716758e1973c5d4', stackArray: [ '70617977', '0729318a128ee8f11d18b28237c8ae7ffa4e95c88ec69ebce716758e1973c5d4', ], msg: '<a href="https://explorer.e.cash/tx/0729318a128ee8f11d18b28237c8ae7ffa4e95c88ec69ebce716758e1973c5d4">Article paywall payment</a>', }, // on spec paywall tx with an invalid article txid { txid: 'f86d0b60e66888f8d30ea67167e5e4370fee29f13201cf5f32727d41c8a41868', hex: '04706179771636343934633165663161316435666333398393832655', stackArray: [ '70617977', '36343934633165663161316435666333398393832655', ], msg: 'Invalid paywall article txid', }, // off spec paywall tx { txid: 'f86d0b60e66888f8d30ea67167e5e4370fee29f13201cf5f32727d41c8a41868', hex: '0470617977', stackArray: ['70617977'], msg: '[off spec paywall payment]', }, ], authenticationTxs: [ // on spec tx { txid: 'e7a67443354b4df14dcb779de6291ea89e7c0e4fce83d56e87b9694e8f4cb8a9', hex: '04617574681414b9b4971454d1a83c33119f3914a0b747f2f7a7', stackArray: [ '61757468', '14b9b4971454d1a83c33119f3914a0b747f2f7a7', ], msg: 'eCashChat authentication via dust tx', }, // on spec tx with an empty authentication identifier { txid: 'e7a67443354b4df14dcb779de6291ea89e7c0e4fce83d56e87b9694e8f4cb8a9', hex: '046175746800', stackArray: ['61757468', '00'], msg: 'Invalid eCashChat authentication identifier', }, // off spec tx { txid: 'e7a67443354b4df14dcb779de6291ea89e7c0e4fce83d56e87b9694e8f4cb8a9', hex: '0461757468', stackArray: ['61757468'], msg: '[off spec eCashChat authentication]', }, ], }; export default appTxSamples; diff --git a/apps/ecash-herald/test/mocks/block.ts b/apps/ecash-herald/test/mocks/block.ts index 70d723dcd..c34ab0e58 100644 --- a/apps/ecash-herald/test/mocks/block.ts +++ b/apps/ecash-herald/test/mocks/block.ts @@ -1,6105 +1,6106 @@ // 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. +// eslint-disable-next-line @typescript-eslint/no-explicit-any const mockedBlock: any = { blockTxs: [ { txid: '0bf6e9cd974cd5fc6fbbf739a42447d41a301890e2db242295c64df63dc3ee7e', version: 1, inputs: [ { prevOut: { txid: '0000000000000000000000000000000000000000000000000000000000000000', outIdx: 4294967295, }, inputScript: '0392800c04904c5d650cfabe6d6d2a5055cb96fc034feb64a6533f9ba428768f019b0efc92797bb1eeae3bda05e410000000000000000800002bed8efca61700000015643839366564326466356633353334353432323837', value: 0, sequenceNo: 0, }, ], outputs: [ { value: 362500000, outputScript: '76a914ce8c8cf69a922a607e8e03e27ec014fbc24882e088ac', spentBy: { txid: '2e3399f02280def3908afc561157cbaa159bbacee47dbcdebac15a668d009fc0', outIdx: 1, }, }, { value: 200000000, outputScript: 'a914d37c4c809fe9840e7bfa77b86bd47163f6fb6c6087', spentBy: { txid: '2df7650a9ebebd998d0dc756650144c21d84722c60fe6389c538d272f134d365', outIdx: 226, }, }, { value: 62500000, outputScript: '76a914c36941af4c8cdf6e3156f7fe1426d05d6177890e88ac', spentBy: { txid: '6da0fa092de6c985365eb40ebe8a9112a62e48a1375dc348b2f2fc9fc27664d1', outIdx: 27, }, }, ], lockTime: 0, timeFirstSeen: 0, size: 239, isCoinbase: true, tokenEntries: [], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NON_TOKEN', block: { height: 819346, hash: '00000000000000001d985578bc11edf9bbfee8daad0f39500e3f429c72fcf282', timestamp: 1700613264, }, }, { txid: '010114b9bbe776def1a512ad1e96a4a06ec4c34fc79bcb5d908845f5102f6b0f', version: 2, inputs: [ { prevOut: { txid: '74bc6dd34b94ff3f0e398ef88e0df3b8c04457db274f9f8d098c9471f35593c6', outIdx: 456, }, inputScript: '48304502210086860e8ee3721d2ebc919dca21e44ff96a2adc287528e46e12665dc1a5af75ec02206dd0c593becad3d4055ed011f9d61468a378090e1fe4246eeb34b68744ec5e93412103bc01efabf76dafe666a98c88fe72915c4cceb26cacf6772904b3fa1fa5629765', value: 5285, sequenceNo: 4294967294, outputScript: '76a914104e67d912a7aab2a159bba141477e5867c04bfd88ac', }, ], outputs: [ { value: 0, outputScript: '6a04534c500001010747454e45534953054c6f6c6c79054c4f4c4c591468747470733a2f2f636173687461622e636f6d2f4c0001084c00080162ea854d0fc000', }, { value: 546, outputScript: '76a914104e67d912a7aab2a159bba141477e5867c04bfd88ac', token: { tokenId: '010114b9bbe776def1a512ad1e96a4a06ec4c34fc79bcb5d908845f5102f6b0f', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '99900000000000000', isMintBaton: false, entryIdx: 0, }, }, { value: 4284, outputScript: '76a914104e67d912a7aab2a159bba141477e5867c04bfd88ac', }, ], lockTime: 0, timeFirstSeen: 0, size: 299, isCoinbase: false, tokenEntries: [ { tokenId: '010114b9bbe776def1a512ad1e96a4a06ec4c34fc79bcb5d908845f5102f6b0f', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, txType: 'GENESIS', isInvalid: false, burnSummary: '', failedColorings: [], actualBurnAmount: '0', intentionalBurn: '0', burnsMintBatons: false, }, ], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NORMAL', block: { height: 784969, hash: '0000000000000000071b57edd35439249dce297842995f4940eaefc6e88d0a9d', timestamp: 1679836351, }, }, { txid: '004e018dd98520aa722ee76c608771dd578a044f38103a8298f25e6ffbc7c3ba', version: 2, inputs: [ { prevOut: { txid: '825bd04c60b27ef652a5ec706cecaf9cec4dc10ce0010f468f8f3bbef6e1539a', outIdx: 2, }, inputScript: '47304402202edcaf6fad2b7789b54ae9283db93f8028249dab78455653f4a1765aae29ca48022060f44bb9fcc82233ba64b4ba1725bfd1d451babb7f96d13c2ef8d833e972946941210353f81d61d41d6e22c73ab449476113dea124afe3972991cd237e654f15950b7c', value: 546, sequenceNo: 4294967295, token: { tokenId: 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '92940000', isMintBaton: false, entryIdx: 0, }, outputScript: '76a914821407ac2993f8684227004f4086082f3f801da788ac', }, { prevOut: { txid: '825bd04c60b27ef652a5ec706cecaf9cec4dc10ce0010f468f8f3bbef6e1539a', outIdx: 3, }, inputScript: '483045022100917c49035cd87aea0c004121561ec5c7488badbfc9ee51e9c7684c0717306bea02206a197976706fc6d6d05590f14482264e90ca080e561978257ba7df451debffd541210353f81d61d41d6e22c73ab449476113dea124afe3972991cd237e654f15950b7c', value: 14274406, sequenceNo: 4294967295, outputScript: '76a914821407ac2993f8684227004f4086082f3f801da788ac', }, ], outputs: [ { value: 0, outputScript: '6a04534c500001010453454e4420aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb108000000000000271008000000000589ffd0', }, { value: 546, outputScript: '76a914dcc535261a43835ca12352d0926ba06cf07cbe8388ac', 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: '92930000', isMintBaton: false, entryIdx: 0, }, spentBy: { txid: '4852929bc3809bb1b6fa5b607f4856df1d0cf13816e01c93a3b32f6a59647f73', outIdx: 0, }, }, { value: 14273379, outputScript: '76a914821407ac2993f8684227004f4086082f3f801da788ac', spentBy: { txid: '4852929bc3809bb1b6fa5b607f4856df1d0cf13816e01c93a3b32f6a59647f73', outIdx: 1, }, }, ], lockTime: 0, timeFirstSeen: 1713483767, size: 480, 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: 840667, hash: '00000000000000000292a87c1b0a07399bbeceec747e15b63b3d4b44837d0e3f', timestamp: 1713484694, }, }, { txid: '0110cd886ecd2d9570e98b7501cd039f4e5352d69659a46f1a49cc19c1869701', version: 2, inputs: [ { prevOut: { txid: '76cf25e029ad119042f956605f9386d82b640b2786fd19a8be22383e71c63066', outIdx: 2, }, inputScript: '4830450221008295a1f9391cdcd4b6ce64e3667e50fbd4c2ce37abc15840cc686bb2ad9970bf022006a16801d509b6f72eca4393731604ee0f28ba0d6ce55dcd4c3b706c374eca8341210353f81d61d41d6e22c73ab449476113dea124afe3972991cd237e654f15950b7c', value: 546, sequenceNo: 4294967295, token: { tokenId: 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '92640000', isMintBaton: false, entryIdx: 0, }, outputScript: '76a914821407ac2993f8684227004f4086082f3f801da788ac', }, { prevOut: { txid: '76cf25e029ad119042f956605f9386d82b640b2786fd19a8be22383e71c63066', outIdx: 3, }, inputScript: '47304402204fd42db620084ff54f32c60fb5cded7040255fdef7b6ba80a1a5a3b9f7c4fef1022042acf826a81e39c4224de432e92a24c1ee11c2a85d6f6f45f8e88cdbb081f4c441210353f81d61d41d6e22c73ab449476113dea124afe3972991cd237e654f15950b7c', value: 14243596, sequenceNo: 4294967295, outputScript: '76a914821407ac2993f8684227004f4086082f3f801da788ac', }, ], outputs: [ { value: 0, outputScript: '6a04534c500001010453454e4420aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1080000000000002710080000000005856bf0', }, { value: 546, outputScript: '76a91469724b96df46096cc95b1a6d408a4240ea80d85588ac', 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: '92630000', isMintBaton: false, entryIdx: 0, }, spentBy: { txid: 'a998c6bdd2d4755b4be7537a5ba064cc19428ce3a47d0c069ee4241a1a83058e', outIdx: 0, }, }, { value: 14242569, outputScript: '76a914821407ac2993f8684227004f4086082f3f801da788ac', spentBy: { txid: 'a998c6bdd2d4755b4be7537a5ba064cc19428ce3a47d0c069ee4241a1a83058e', outIdx: 1, }, }, ], lockTime: 0, timeFirstSeen: 1713484515, size: 480, 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: 840667, hash: '00000000000000000292a87c1b0a07399bbeceec747e15b63b3d4b44837d0e3f', timestamp: 1713484694, }, }, { txid: '327101f6f3b740280a6e9fbd8edc41f4f0500633672975a5974a4147c94016a5', version: 2, inputs: [ { prevOut: { txid: 'ddb98796d80acad3a291913e015cfbe30612be00d921533c67513e5d61e8bda5', outIdx: 2, }, inputScript: '47304402207223b7e969380eb1c83569a6c217f2d6350f2b3f241d30af9446c04bc36d109f022049bcc4d0a9327f839618a8174af837ac61d6cea1d30962f2866c1dfb4a9d3e8041210353f81d61d41d6e22c73ab449476113dea124afe3972991cd237e654f15950b7c', value: 546, sequenceNo: 4294967295, token: { tokenId: 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '92560000', isMintBaton: false, entryIdx: 0, }, outputScript: '76a914821407ac2993f8684227004f4086082f3f801da788ac', }, { prevOut: { txid: 'ddb98796d80acad3a291913e015cfbe30612be00d921533c67513e5d61e8bda5', outIdx: 3, }, inputScript: '483045022100f5b0ca73d4d81cba5abf91d9d3531085768ef71b173b1a1701bbefc6cefad65202201540deb6e8c7c3ca96889ef6965abcc61c9c1e3e3ada3d70be1bed146f48bfe341210353f81d61d41d6e22c73ab449476113dea124afe3972991cd237e654f15950b7c', value: 14235380, sequenceNo: 4294967295, outputScript: '76a914821407ac2993f8684227004f4086082f3f801da788ac', }, ], outputs: [ { value: 0, outputScript: '6a04534c500001010453454e4420aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1080000000000002710080000000005843370', }, { value: 546, outputScript: '76a91458cddba2449285814dae43d4ed4a1c9998f3693e88ac', 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: '92550000', isMintBaton: false, entryIdx: 0, }, spentBy: { txid: '0a77eb6a5b08bc91a60a8ed8752ac2d3dc477e0c94624c486fcef7429be47d0d', outIdx: 0, }, }, { value: 14234353, outputScript: '76a914821407ac2993f8684227004f4086082f3f801da788ac', spentBy: { txid: '0a77eb6a5b08bc91a60a8ed8752ac2d3dc477e0c94624c486fcef7429be47d0d', outIdx: 1, }, }, ], lockTime: 0, timeFirstSeen: 1713484590, size: 480, 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: 840667, hash: '00000000000000000292a87c1b0a07399bbeceec747e15b63b3d4b44837d0e3f', timestamp: 1713484694, }, }, { txid: 'aa13c6f214ff58f36ed5e108a7f36d8f98729c50186b27a53b989c7f36fbf517', version: 2, inputs: [ { prevOut: { txid: 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', outIdx: 1, }, inputScript: '483045022100f7f2eac186605f5d37a038b17367a4b6fc5458ca7485ce6b77baf19b4160bcd8022029b5ef41a2ebb4642e9802d32a1649d84c7daf2e978c32ebc7342b90e9427cc1412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', value: 546, sequenceNo: 4294967295, token: { tokenId: 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '10000000', isMintBaton: false, entryIdx: 0, }, outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', }, { prevOut: { txid: 'd8a081bed886b085194410fd879286393734f428c9f64d9ece1c0afffb2695a2', outIdx: 1, }, inputScript: '483045022100b0404d5d553867df9ed190ce52ec13565aaf6e3c8986b712c150acac6d3853f70220727abe6d27a333f72249a08f3b40cd15346c6096466b6118248f92279201b5f7412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', value: 3899, sequenceNo: 4294967295, outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', }, ], outputs: [ { value: 0, outputScript: '6a04534c500001010453454e4420aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1080000000000002710080000000000986f70', }, { value: 546, outputScript: '76a914821407ac2993f8684227004f4086082f3f801da788ac', token: { tokenId: 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '10000', isMintBaton: false, entryIdx: 0, }, spentBy: { txid: '80baab3dc64a3922c8d3ca11bacc6af4f05b103e15e18e9ea7592d926612c829', outIdx: 0, }, }, { value: 546, outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', token: { tokenId: 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '9990000', isMintBaton: false, entryIdx: 0, }, spentBy: { txid: '4fb3b37c25c8a5cb43f0130435eb33c19b2fdaf4be98b113e580a66ec9340435', outIdx: 3, }, }, { value: 2872, outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', spentBy: { txid: 'fb6086e1e98f88fdef7abab312dfb68449d1b43d511e1f15c488a8cb804f1c51', outIdx: 0, }, }, ], lockTime: 0, timeFirstSeen: 1713384505, size: 481, 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: 840499, hash: '000000000000000035b621834b4408d0b1a8da7d975cb14c0b9330d1e2398d8b', timestamp: 1713384866, }, }, { txid: '6ffcc83e76226bd32821cc6862ce9b363b22594247a4e73ccf3701b0023592b2', version: 2, inputs: [ { prevOut: { txid: 'b07e3e6a696b7de3c4474107c5a5691d759713060832b2489c353a0718cf8a78', outIdx: 2, }, inputScript: '48304502210081ba28d95e619fbc5997299a03a0ae2ffa0bf0af66277b6d57087ac45a1a300502202c8cac931e6e58aac9c318f59e31df3b149d240244f6d74fc1b5aa19fad742c6412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', value: 15177819, sequenceNo: 4294967295, outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', }, { prevOut: { txid: '07ad02a4477d02ee5007e32fdc576769aa3a0e501f978549eb746c83e41fe52f', outIdx: 2, }, inputScript: '483045022100d117557506158821beb623a0a9c6ecbc88011a1eca397afe910067a994ad35fd022003355bd201f7f21edeedc2a4f3991530043ee9981021887702152418c2b28b7d412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', value: 546, sequenceNo: 4294967295, token: { tokenId: '98183238638ecb4ddc365056e22de0e8a05448c1e6084bae247fae5a74ad4f48', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '999977691', isMintBaton: false, entryIdx: 0, }, outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', }, ], outputs: [ { value: 0, outputScript: '6a04534c500001010453454e442098183238638ecb4ddc365056e22de0e8a05448c1e6084bae247fae5a74ad4f4808000000000000003708000000003b9a72a4', }, { value: 546, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', token: { tokenId: '98183238638ecb4ddc365056e22de0e8a05448c1e6084bae247fae5a74ad4f48', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '55', isMintBaton: false, entryIdx: 0, }, }, { value: 546, outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', token: { tokenId: '98183238638ecb4ddc365056e22de0e8a05448c1e6084bae247fae5a74ad4f48', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '999977636', isMintBaton: false, entryIdx: 0, }, }, { value: 15176136, outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', spentBy: { txid: '5f06207dea4762524dbe2d84900cc78711d079f2b2e909867ec5e9abdeb850aa', outIdx: 0, }, }, ], lockTime: 0, timeFirstSeen: 0, size: 481, isCoinbase: false, tokenEntries: [ { tokenId: '98183238638ecb4ddc365056e22de0e8a05448c1e6084bae247fae5a74ad4f48', 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: 800716, hash: '0000000000000000140fb9e3f5ba1d76a1902ede29aec1b6c93edf9a4e11e44b', timestamp: 1689261128, }, }, { txid: 'fb70df00c07749082756054522d3f08691fd9caccd0e0abf736df23d22845a6e', version: 2, inputs: [ { prevOut: { txid: 'b8ed29af5e165f26062bc553406fe642a5a8e9e52dec1f281112f4f16af717b9', outIdx: 2, }, inputScript: '47304402206f260f779e10e44d290d7092b4a4f627c5387e30c2e2080e1ac3a726adb33f850220562e7e32bba69f4dad19bf267f1721b9d80e7504135edbb50ff5f1ec1ebf99e8412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', value: 17421473, sequenceNo: 4294967295, outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', }, { prevOut: { txid: '9c0c01c1e8cc3c6d816a3b41d09d65fda69de082b74b6ede7832ed05527ec744', outIdx: 2, }, inputScript: '47304402204f6ed41e291f0ad846be2516e7626ed0adbcf64f8a13a05897f61f7ce7f7afba0220559546ea121ad78ae6b2ff91f33e0a083299c0657fb3122a251eb6d05e2a6269412103771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba6', value: 546, sequenceNo: 4294967295, token: { tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '5235120638765433', isMintBaton: false, entryIdx: 0, }, outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', }, ], outputs: [ { value: 0, outputScript: '6a04534c500001010453454e44207443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d0800000000068c953f08001299507b7b143a', }, { value: 546, outputScript: '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', token: { tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '109876543', isMintBaton: false, entryIdx: 0, }, }, { value: 546, outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', token: { tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '5235120528888890', isMintBaton: false, entryIdx: 0, }, }, { value: 17419790, outputScript: '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', spentBy: { txid: '8f6676b602a9f074f10a7561fb7256bbce3b103a119f809a05485e42489d2233', outIdx: 0, }, }, ], lockTime: 0, timeFirstSeen: 0, size: 479, isCoinbase: false, tokenEntries: [ { tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', 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: 800716, hash: '0000000000000000140fb9e3f5ba1d76a1902ede29aec1b6c93edf9a4e11e44b', timestamp: 1689261128, }, }, { txid: '25345b0bf921a2a9080c647768ba440bbe84499f4c7773fba8a1b03e88ae7fe7', version: 2, inputs: [ { prevOut: { txid: '1f5f4350b93708ca60b94c51ce3135dcaeef5ce64bb7dbc2934a442917ccad1a', outIdx: 3, }, inputScript: '483045022100889c5bc4aac2b8fba02f2414c596f5458d47acc3f21f8893a8fc5c367ca2559702205fe45c504ed024740df74811f8a75b40831cbdbfdad72aa332112fe0f759f0f2412103632f603f43ae61afece65288d7d92e55188783edb74e205be974b8cd1cd36a1e', value: 1528001, sequenceNo: 4294967294, outputScript: '76a9141c13ddb8dd422bbe02dc2ae8798b4549a67a3c1d88ac', }, { prevOut: { txid: '5ca2cb70c3c351da6fff27d06dec6271449e52e37c38bbf1a5cfb64dd6dde161', outIdx: 2, }, inputScript: '473044022016f9ad02f956cb7160099c80a5899bca83e92965665c9b75f2719f4432ab8dcf02206d7b8f1e29eb2761798cb76f96efc623ec72764f79f8d85320c1c4566fbc08b9412103632f603f43ae61afece65288d7d92e55188783edb74e205be974b8cd1cd36a1e', value: 546, sequenceNo: 4294967294, token: { tokenId: 'fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '34443689000', isMintBaton: false, entryIdx: 0, }, outputScript: '76a9141c13ddb8dd422bbe02dc2ae8798b4549a67a3c1d88ac', }, ], outputs: [ { value: 0, outputScript: '6a04534c500001010453454e4420fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa08000000001dcd65000800000007e7339728', }, { value: 546, outputScript: '76a914dadf34cde9c774fdd6340cd2916a9b9c5d57cf4388ac', token: { tokenId: 'fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '500000000', isMintBaton: false, entryIdx: 0, }, spentBy: { txid: '9b4cad218d7743f1610d73577e2c3c4bcd97a2e70a61e69aea67088277dad936', outIdx: 2, }, }, { value: 546, outputScript: '76a9141c13ddb8dd422bbe02dc2ae8798b4549a67a3c1d88ac', token: { tokenId: 'fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '33943689000', isMintBaton: false, entryIdx: 0, }, spentBy: { txid: 'd28244a5f79ed2323c543294d901fc0fe6ecc3c08f2ab4224ac141289daa4da9', outIdx: 1, }, }, { value: 1526318, outputScript: '76a9141c13ddb8dd422bbe02dc2ae8798b4549a67a3c1d88ac', spentBy: { txid: '660d23a32becd5fbca89e87a15981953c1ad092ec148f2f04661b3c54d8b5e25', outIdx: 0, }, }, ], lockTime: 0, timeFirstSeen: 0, size: 480, isCoinbase: false, tokenEntries: [ { tokenId: 'fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa', 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: 782571, hash: '000000000000000003a43161c1d963b1df57f639a4621f56d3dbf69d5a8d0561', timestamp: 1678358652, }, }, { txid: '0167e881fcb359cdfc82af5fc6c0821daf55f40767694eea2f23c0d42a9b1c17', version: 1, inputs: [ { prevOut: { txid: '581464b01626d7ad867f93970338ec2840ce1c97aed658884474e6cb16a02807', outIdx: 1, }, inputScript: '4153405b57f5a1969c45891448e99bb69376490bea5ce29240a1152168c72dee5adfb09b84c90b0d4f0e590ba1127b94e2f3ff36877224c1779e04743f2b64d308c121039764908e0122ca735c3470ff3c805b265e54589901fcee0d610f0d31b356f7f3', value: 546, sequenceNo: 4294967295, token: { tokenId: '7e7dacd72dcdb14e00a03dd3aff47f019ed51a6f1f4e4f532ae50692f62bc4e5', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '526349', isMintBaton: false, entryIdx: 0, }, outputScript: '76a9146d69b5cbe7c85d87628473c43620c0daa9a8102988ac', }, { prevOut: { txid: 'afd08abc17c78d3f0449f2393e0db9e5266099fca21c141b67879bd7c9330708', outIdx: 1, }, inputScript: '41e3558233c98f31574ac950c322f43e45f3fd7c4e5462aeeaf034e7263115ddad77cd37e834a1c5c942e552028e17077ef9ea146fdc18986ccf8449efe8ac9d44c121039764908e0122ca735c3470ff3c805b265e54589901fcee0d610f0d31b356f7f3', value: 546, sequenceNo: 4294967295, token: { tokenId: '7e7dacd72dcdb14e00a03dd3aff47f019ed51a6f1f4e4f532ae50692f62bc4e5', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '420181', isMintBaton: false, entryIdx: 0, }, outputScript: '76a9146d69b5cbe7c85d87628473c43620c0daa9a8102988ac', }, { prevOut: { txid: 'f94fc719a8d037cf2df3d8aac753d9b606ca2a60c60dbb80c21a7ae7a6281508', outIdx: 1, }, inputScript: '4102b9d0890ef77f2078e1b6899210039480d66bdef4fdc91c740ecaeec5583f55a731717a32e0dd9252d5bdef096b337ad3ecd57636f6bac8067fc3a78d3c0a94c121039764908e0122ca735c3470ff3c805b265e54589901fcee0d610f0d31b356f7f3', value: 546, sequenceNo: 4294967295, token: { tokenId: '7e7dacd72dcdb14e00a03dd3aff47f019ed51a6f1f4e4f532ae50692f62bc4e5', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '312605', isMintBaton: false, entryIdx: 0, }, outputScript: '76a9146d69b5cbe7c85d87628473c43620c0daa9a8102988ac', }, { prevOut: { txid: '8d2a0286607ee744c8890c161da4dd083049fff20e23d721702a47a5b139410e', outIdx: 1, }, inputScript: '41a81656ffe952c34a011aa55653846abe1db05de068f2e6a6b91de7b5500d72762a8d37b041c2f9a451f58196e7045aaf0a4bb957768395b37b4f4759c823d1e1c121039764908e0122ca735c3470ff3c805b265e54589901fcee0d610f0d31b356f7f3', value: 546, sequenceNo: 4294967295, token: { tokenId: '7e7dacd72dcdb14e00a03dd3aff47f019ed51a6f1f4e4f532ae50692f62bc4e5', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '526877', isMintBaton: false, entryIdx: 0, }, outputScript: '76a9146d69b5cbe7c85d87628473c43620c0daa9a8102988ac', }, { prevOut: { txid: 'b4ba6aea60657f80fbf86c73389ea49c5c95817ac2468a2600635bdcb6143310', outIdx: 1, }, inputScript: '4112461349af15cabe257ef0290f2a8e923e33cbfcd7f8d34923e95d5afacfff2407a2549f5819760e3c1ece84b20d3276893638ef8636f366338c8c4a0e2b0841c121039764908e0122ca735c3470ff3c805b265e54589901fcee0d610f0d31b356f7f3', value: 546, sequenceNo: 4294967295, token: { tokenId: '7e7dacd72dcdb14e00a03dd3aff47f019ed51a6f1f4e4f532ae50692f62bc4e5', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '1780906', isMintBaton: false, entryIdx: 0, }, outputScript: '76a9146d69b5cbe7c85d87628473c43620c0daa9a8102988ac', }, ], outputs: [ { value: 0, outputScript: '6a04534c500001010453454e44207e7dacd72dcdb14e00a03dd3aff47f019ed51a6f1f4e4f532ae50692f62bc4e50800000000002737100800000000000f3636', }, { value: 546, outputScript: '76a9146d69b5cbe7c85d87628473c43620c0daa9a8102988ac', token: { tokenId: '7e7dacd72dcdb14e00a03dd3aff47f019ed51a6f1f4e4f532ae50692f62bc4e5', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '2570000', isMintBaton: false, entryIdx: 0, }, spentBy: { txid: 'ea54f221be5c17dafc852f581f0e20dea0e72d7f0b3c691b4333fc1577bf0724', outIdx: 0, }, }, { value: 546, outputScript: '76a9146d69b5cbe7c85d87628473c43620c0daa9a8102988ac', token: { tokenId: '7e7dacd72dcdb14e00a03dd3aff47f019ed51a6f1f4e4f532ae50692f62bc4e5', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '996918', isMintBaton: false, entryIdx: 0, }, spentBy: { txid: 'f490c4dd2b2a7cf14a04af6efaba9851cd233e753e239ff021296aae4b71ad88', outIdx: 3, }, }, ], lockTime: 0, timeFirstSeen: 0, size: 856, isCoinbase: false, tokenEntries: [ { tokenId: '7e7dacd72dcdb14e00a03dd3aff47f019ed51a6f1f4e4f532ae50692f62bc4e5', 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: 782571, hash: '000000000000000003a43161c1d963b1df57f639a4621f56d3dbf69d5a8d0561', timestamp: 1678358652, }, }, { txid: '6b139007a0649f99a1a099c7c924716ee1920f74ea83111f6426854d4c3c3c79', version: 2, inputs: [ { prevOut: { txid: 'b8c9089f0991676d768920225c8614eabdb8c715e79b22411fe69d1916dcf3a7', outIdx: 1, }, inputScript: '483045022100a7e929b6748902fe6896d21d6f542994f594c96b50f33963ee967011d6bcae9e02203e11d0cd2ac4c4f5efb56e1b2f86ed53d995164e08e3ca65a0b60118a7dd6b114121032f047c5282b9f24806f6bae65d1505ad60b555c2456004301f6253f14240b0ce', value: 5000, sequenceNo: 4294967295, outputScript: '76a9144bb6f659b8dafd99527e0c0a3289f121b0a0209f88ac', }, { prevOut: { txid: '794c366a60d864ffc5fddf1ffeedf11091b5845657d433a03f4fedd302bd2a3b', outIdx: 2, }, inputScript: '483045022100ae739cec070e17a943aea0b59b74aa2320e6223a90191e598a0695fde4300bab02207889e7530838df5fa792bcbe062cc3b3c6f5378dff4723bf0ee80755af6c82964121032f047c5282b9f24806f6bae65d1505ad60b555c2456004301f6253f14240b0ce', value: 546, sequenceNo: 4294967295, token: { tokenId: 'fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '205000000', isMintBaton: false, entryIdx: 0, }, outputScript: '76a9144bb6f659b8dafd99527e0c0a3289f121b0a0209f88ac', }, ], outputs: [ { value: 0, outputScript: '6a04534c500001010453454e4420fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa08000000000c380cdc', }, { value: 546, outputScript: '76a9144bb6f659b8dafd99527e0c0a3289f121b0a0209f88ac', token: { tokenId: 'fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, amount: '204999900', isMintBaton: false, entryIdx: 0, }, }, { value: 546, outputScript: '76a9144bb6f659b8dafd99527e0c0a3289f121b0a0209f88ac', }, { value: 3317, outputScript: '76a9144bb6f659b8dafd99527e0c0a3289f121b0a0209f88ac', }, ], lockTime: 0, timeFirstSeen: 0, size: 472, isCoinbase: false, tokenEntries: [ { tokenId: 'fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa', tokenType: { protocol: 'SLP', type: 'SLP_TOKEN_TYPE_FUNGIBLE', number: 1, }, txType: 'SEND', isInvalid: false, burnSummary: 'Unexpected burn: Burns 100 base tokens', failedColorings: [], actualBurnAmount: '100', intentionalBurn: '0', burnsMintBatons: false, }, ], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NOT_NORMAL', block: { height: 800162, hash: '0000000000000000135015e9513693999b669e5b024c83a4cc3b4db5dea7e414', timestamp: 1688933698, }, }, { txid: 'd5be7a4b483f9fdbbe3bf46cfafdd0100d5dbeee0b972f4dabc8ae9d9962fa55', version: 1, inputs: [ { prevOut: { txid: '025e232886adbd347cdbbfbe53ab8291aca66b3e0fec35e13367260572b1b14a', outIdx: 9, }, inputScript: '41976761151559c9edf23b21b314d1003ee8562bce946f3cc56261245354f4536e93320d5f01c16f3efedfd71d8a32798f16ae4ef562ff173297b95ba863bd22df412103b28690ae5178fef9a75901f6c0974e5d5554dcd62ef1962ee26b55d613f0da6b', value: 789283, sequenceNo: 4294967295, outputScript: '76a91412934a7a99b69a60c3b99f991cd79d257104f5a688ac', }, { prevOut: { txid: '025e232886adbd347cdbbfbe53ab8291aca66b3e0fec35e13367260572b1b14a', outIdx: 67, }, inputScript: '41343ae6b5573d542ce7fc5c1ad9d3b3982437f9d3d29fb359ff5c725fb379d73f1e09dc0fb01a9e87ebd21f1cc7c1bf5f9605bef90603489f03845b32b851b75f412102facaf89e3fb087741aea79247dcd947765c07cc7a3b61dd1e00a108e7f09c363', value: 19661976, sequenceNo: 4294967295, outputScript: '76a91415c0b62c9f73847ca9a686561216c20b487a0aad88ac', }, { prevOut: { txid: '282e423192b69ad2fd21b07f2bbc28bd0e48659e8b76c8a7cb494e9632d7bd58', outIdx: 2, }, inputScript: '419560a571383df383cc335fe89791675f9e45e00c8fc452c85698d6654822a01dfea76cde4ea4411f9a7a5e3a150c7f0f3fde46d7b2bb1f9446d27d9b911dfe32412102b74d3a97c688764abe5e77aa21784881aa98724f10a323af9e7aff6f5dbf31ef', value: 236812, sequenceNo: 4294967295, outputScript: '76a914a4e299724b8e81474df916c25c7a816a43c8748888ac', }, { prevOut: { txid: '282e423192b69ad2fd21b07f2bbc28bd0e48659e8b76c8a7cb494e9632d7bd58', outIdx: 9, }, inputScript: '41c7cf7bd61687724127d21a05ae950a88570475f1433fa3a2477407700624d4785b5cf530422de3f461a009b4ac1806cf8ae2e4938613fc412253b5d8f0137435412102f54d7c16ad99d58a1c2118d71584498055247735eddf494b84f5311d1575bced', value: 1481924, sequenceNo: 4294967295, outputScript: '76a9147afa62562b93fecaff30190ee3a2836dcb95d42f88ac', }, { prevOut: { txid: '282e423192b69ad2fd21b07f2bbc28bd0e48659e8b76c8a7cb494e9632d7bd58', outIdx: 12, }, inputScript: '41eeeff8f9f55d7a9106346ca430cb15ab38e3ae49518a9bb0377f614f64e1679c6218a4b60cb086ce406fd0eb298a3ccb7dd09fca20d96dcbbb489acb5ec82d37412102e1065480d2c5df584ee53b6a121103c4f084d37d8932dbf04d10fa674b4d258c', value: 1923633, sequenceNo: 4294967295, outputScript: '76a91468e15e8bfe2d969b7963181b976e6833e294661288ac', }, { prevOut: { txid: '282e423192b69ad2fd21b07f2bbc28bd0e48659e8b76c8a7cb494e9632d7bd58', outIdx: 47, }, inputScript: '41670d46d9042b979fdbc2ccb50df231dc8f8dfc8c9ea66180a41ca60ad498a05936b8683daa93281bcf46a18ad838f80f284cccc1de04931381d3279c93e109cb4121020be1664f1cc506d056017b7072633452b3571724560bb73dce68a160cd9182e6', value: 12566124, sequenceNo: 4294967295, outputScript: '76a914f941b2e03f973ce5b13766159eef190963e2393488ac', }, { prevOut: { txid: '282e423192b69ad2fd21b07f2bbc28bd0e48659e8b76c8a7cb494e9632d7bd58', outIdx: 63, }, inputScript: '412b5195fe17713edc3b58102ef3e60ef06fe50229e65dd143f23a9a6edcd7956a7148c9d038891a866b0e98627bb1f66f1c9f43ab7716bc5455ed1cf599b553f6412103da9dc1e5ff5116e6d8b4535b9e565e0c5323316b240043ede4f9bf8ae6927bf4', value: 20033202, sequenceNo: 4294967295, outputScript: '76a9146e3430f87a128ac4509fb0547f07ba0e3e8cea7688ac', }, { prevOut: { txid: '282e423192b69ad2fd21b07f2bbc28bd0e48659e8b76c8a7cb494e9632d7bd58', outIdx: 69, }, inputScript: '41c07981287684a57d6dff05fe35bb9cf49682be07b51fc9bd1aecdb50dfeaf5646d6bcbf04e45d711ef229fa2564197bc7c21994180491218c063cde76f733de64121020c2f45d704ca5ef65d16520512184601411e4704da88ccaa21ae5d116dd62e27', value: 30370886, sequenceNo: 4294967295, outputScript: '76a914c72166790bc8c779163e17b11939a6bd6022a7e188ac', }, { prevOut: { txid: '282e423192b69ad2fd21b07f2bbc28bd0e48659e8b76c8a7cb494e9632d7bd58', outIdx: 72, }, inputScript: '41628d99c6f5ffefb2e8b33874caa20b26a9b2b26a3a353738cbe4f82babb6800f13aa0eef1575cbda249a5488407d6f34614c610613e3e27fcb20b93316e0be2c4121021e0eda5f4d41e5388cae8ed899fcde2571a155b23e8d25199eae7b674f8a3335', value: 37898355, sequenceNo: 4294967295, outputScript: '76a91483c54d6ec805f4db16c935f5bb89da791f971ac888ac', }, { prevOut: { txid: '33160635670dab2a6e89425f2be9a1b1fb742c75ff2684a1e62a44f82c1dae6d', outIdx: 10, }, inputScript: '412c5a59a5176563765df132213db2d7767112dfc45df859091d8336dc472df44809449bc9bfcdd29dca69d5784976f04401d4910483f6150b955adc08faa7adeb412102d8ba67b96c5a0371d96d5270f85ddb02b6e9fc4801bd1e85b1877edb52ffbda6', value: 2489718, sequenceNo: 4294967295, outputScript: '76a914336fb64b7e98221f82aced275440c29e7e1d11b388ac', }, { prevOut: { txid: '33160635670dab2a6e89425f2be9a1b1fb742c75ff2684a1e62a44f82c1dae6d', outIdx: 18, }, inputScript: '41581270a283d4512a3ffc4179ba1c6650534740b2f8c115c6348d029850d00a5cf3acb70cdb059acf3d6dff94753f8f574acc1e3019df797275be79d912709a294121023353579f3a7d6b492db0132190e675f92564aa23d2b9c3d79654bfab0bba4830', value: 5710023, sequenceNo: 4294967295, outputScript: '76a914b114a9d636ac7558c04e902c3a1f7c1fd9008bcd88ac', }, { prevOut: { txid: '33160635670dab2a6e89425f2be9a1b1fb742c75ff2684a1e62a44f82c1dae6d', outIdx: 25, }, inputScript: '4181ae55a349cc2864b2839d67764c8a88d9f5f8e322d16465df763529cc56238b4ad990c617431d17607c43421030c3bb83758da3023846ff5f1a425179311d6b412102c69259026f5ad94372a1d98de97374adda25aebc6858dca8511a9ac1cb95287f', value: 8237826, sequenceNo: 4294967295, outputScript: '76a91411667c453097adf3e71d08986df7766c26f3399088ac', }, { prevOut: { txid: '33160635670dab2a6e89425f2be9a1b1fb742c75ff2684a1e62a44f82c1dae6d', outIdx: 26, }, inputScript: '41d7f92d59288eff61e959f9c59cda2b33ca15dbececb2d632f08026aae5608167b401f5e39f3e35a812eca83310ec06c89606eb053eabef78b6838f3306584963412102916d2b0bedeef5c35659f8ea8e54871cf3a2241b85e696dfaea797fb3ac19d93', value: 8485409, sequenceNo: 4294967295, outputScript: '76a914a125966da9024acea37f867323778641ff0e891888ac', }, { prevOut: { txid: '33160635670dab2a6e89425f2be9a1b1fb742c75ff2684a1e62a44f82c1dae6d', outIdx: 44, }, inputScript: '41b2f767347acd9142d5f0f9754a2dbf79575eaf9f29e124b15b3536d0ceade8bcdd31d04656ba63f44cd144d66ff724e602c3080b66329b29536e4f9c1fae922941210278ea288a9f62d52ac4d9301779ce177a9d8efa2c650205dd80e895c8f10bec4d', value: 24067273, sequenceNo: 4294967295, outputScript: '76a914e03ba5757763a00aaa8aa9eda71da51610d5ef2788ac', }, { prevOut: { txid: '33160635670dab2a6e89425f2be9a1b1fb742c75ff2684a1e62a44f82c1dae6d', outIdx: 46, }, inputScript: '41b2c968dfd3653975ede62f15eb0925cad47d06ec2e01a597efe8aa0db73f9af79090dbc3adad9bcc11a9bdb323240ea178cbe8641907a3c9dfa5e01652bceaee412103d0d7f54b4cf2be2f19d4eceac703f445e1223a134fed95fee7d7d6fedaf7f1fe', value: 25912582, sequenceNo: 4294967295, outputScript: '76a914b13b05d51174d91381b0ea6fb07a6345eea1abf788ac', }, { prevOut: { txid: '33160635670dab2a6e89425f2be9a1b1fb742c75ff2684a1e62a44f82c1dae6d', outIdx: 51, }, inputScript: '4195760d04133191dce89bf872b61ad771f9b33db8f36c249418d0cea3e1c7f73e4bcaf151103effd88f82911a831f2e552961df731f7cb4d87db42f97f9ef4d11412103dbd5c06a2afaeef2240ba22bb6c7650d51d18ec16e4ea3edf4ebd795760f96d8', value: 32513005, sequenceNo: 4294967295, outputScript: '76a914349c3f91c2782b235ae0d1a2c3acf053d554170788ac', }, { prevOut: { txid: '33160635670dab2a6e89425f2be9a1b1fb742c75ff2684a1e62a44f82c1dae6d', outIdx: 56, }, inputScript: '416e3713337d09659305d797115c9281dc060d65f45a491828ae6e6676db691d4f9d0c473000806d2254303b99effab78ace1f85da921bf22c8a47fe89c465a7dd412102258cedf84db0614de15c53a92010e0bf2371c386403457385ed0c1ab8ba38e29', value: 70247919, sequenceNo: 4294967295, outputScript: '76a9143afafec322ef1a4f70a6ca68dd9090182716181888ac', }, { prevOut: { txid: '36fe871b850030281c9325d67ddc3aad32f179f2cfddbfc6f92e1923a4027587', outIdx: 4, }, inputScript: '4198de475fa1ce6eaf983ea0a021ed49ef35c3a96cbd4ba88769b1db92c0455b40e50261eca6c7d7a0edf8a8f5fec1fcd405c5cc9c19c2db691ee7652866ec704541210268a9995c00a0588bada4e48264f7cd0fc1c139bc8ee1b009d1672a5700689c14', value: 1199454, sequenceNo: 4294967295, outputScript: '76a914cb74cf87cd355cd01505645eaf165646a4eb1ce988ac', }, { prevOut: { txid: '36fe871b850030281c9325d67ddc3aad32f179f2cfddbfc6f92e1923a4027587', outIdx: 8, }, inputScript: '41d735894ba83cdf74b971b1ae8903ac72215378941798b3f98389c845f1092edd186648e1108632bb98ad4b85a5f3aeaaf1468498e8a61c29043f978acba2713e412102c6a66170358d332c880609845feba09445468dbca3977f8243b71f7708a38931', value: 3496387, sequenceNo: 4294967295, outputScript: '76a914c42245ebeb7fea2996e5e0f65537b56fb58ea97d88ac', }, { prevOut: { txid: '36fe871b850030281c9325d67ddc3aad32f179f2cfddbfc6f92e1923a4027587', outIdx: 48, }, inputScript: '4127e265aaa3ffb1188d61c01f48597045e0b20cf03d6c0a6d261b825759c1402e8a81ed03d6b7f02dd9d433931d8d56e8c4c3c929bdfe6166864ed13fa6a14c2041210247e436fe91fd245894bdc61f01fac054f2c2a9e14e3b16584d28d0396546b208', value: 30653501, sequenceNo: 4294967295, outputScript: '76a91447d7bc2240955fd18d53c67c4b814e166b152ec388ac', }, { prevOut: { txid: '36fe871b850030281c9325d67ddc3aad32f179f2cfddbfc6f92e1923a4027587', outIdx: 61, }, inputScript: '4132fab3b2ee76ff4f2a9608029ff01a499f04b048a53238d09f2ee5545667e5d76053ac9a018530aded8f06e07b096caed77d6d8b436e9325deca58ec33381f1b412102677e892b57954785abea57b508662752d134e1b85b0cf8c924c382e957b424f5', value: 54383530, sequenceNo: 4294967295, outputScript: '76a91410b45d95195a71957b43bb82762e6cb48e67888f88ac', }, { prevOut: { txid: '38aea1067bc178c13d2498b573dd13136d0bbbd59bdd4174d8323efa7925d709', outIdx: 44, }, inputScript: '418c33f23f296bd45cc26514ca2acb394e76e0c0085af0f5bb72fe94192f9f81d3cb3eca750aa64d8b73c0ff11d3788b46a08b308de793789a0db203fcaae7f4334121025754d300a9c992376c28aeb2f711579e072ced8a6a9f8f6f5046e2dfb34773ef', value: 48782413, sequenceNo: 4294967295, outputScript: '76a914894e84afe4b07413c99087067292aca67d286fbf88ac', }, { prevOut: { txid: '3e6a7a945ee1141be605f62cd7a36d94532340c736a5db4e822ebca4b0548b57', outIdx: 9, }, inputScript: '419585e642c12308cb16dc820f8432ca140ce85050711a2d0ddab19836248a6e8c7327c8256af217010b812593753105959f3b9d957c77f7ae81b1798cbe1322b1412103a3325e9436167659795eb6984a33b890c7e31a2d2b860300a892bd1f6d186220', value: 25031767, sequenceNo: 4294967295, outputScript: '76a91473b804181c01f16cbf63fe262e9a0c8de929af1e88ac', }, { prevOut: { txid: '47162a965a1b9baa086b90427a4dc73ed100e88aa3419fd675cc9c15d7a2264d', outIdx: 50, }, inputScript: '4167257a33b15c13d334a2d69bb9b466c3dbac7a9017b1bcf461eb07a3443a6adba372908235a3262685d9d634dd2341547bc086c617ea3df0412452a67b0b291c41210248db002b83e2c614ae5b956b686961823edf5bb0db2bfa4964a24bfbcfea2c7b', value: 29615068, sequenceNo: 4294967295, outputScript: '76a9147b1a9441467759f8693bdac3e356ab6110bebe1c88ac', }, { prevOut: { txid: '5646ba9af331a3d4e66ef46ae34a09be90a101fe6f94efba2a612122f3dbe057', outIdx: 9, }, inputScript: '41de35e2cdc2e176b24d8f519d84a27c9b13ac3f01ecfb850c92e9a7c2969f2bb1d86d8e00572785bde21d6da669fa131c20e368ffeb0157349cd609bcde748b6e412103a302b269baec427ad945dcef291a2b9bb5f91ae1d287899a66bb34b3d4d19f95', value: 3563255, sequenceNo: 4294967295, outputScript: '76a914443f7cf9987b921c10d888c3d617c54aba5e8fb088ac', }, { prevOut: { txid: '5646ba9af331a3d4e66ef46ae34a09be90a101fe6f94efba2a612122f3dbe057', outIdx: 21, }, inputScript: '41dcb57eb57157c7ae624a100e5f4c71fc2173eb81befff2f15b521105ee553f31118d2eeec770c8e43a6a2ff15e689d81128239184ae7d844a9b382a84906e446412102321799e9dc1c2dc6c9ddfae967c7bb81bb2e64d2c59d57d35c3ca8292de56291', value: 11787007, sequenceNo: 4294967295, outputScript: '76a91490de1562e4aadc991dc13d28a9d112461fea9cb888ac', }, { prevOut: { txid: '5646ba9af331a3d4e66ef46ae34a09be90a101fe6f94efba2a612122f3dbe057', outIdx: 40, }, inputScript: '4157367017cd6dc848750f538e5fd32b0d7b1f69bd7b2bca0e43f772374d65b8e1558acf8544c577e2ebc4368b320f07c25f146fa004fb30e45fb8c9ae608b8afd41210360955914b784f0100bce4935f6f17c1417387598b0bebd1d7c15fc2ebb27403f', value: 23443485, sequenceNo: 4294967295, outputScript: '76a914273808f74a845b9b77345d43cb679ca793c5e9e688ac', }, { prevOut: { txid: '5646ba9af331a3d4e66ef46ae34a09be90a101fe6f94efba2a612122f3dbe057', outIdx: 44, }, inputScript: '414d146e2e20940c99323f0502114c2afbad68e1d772cd20bdf8a7d7894c5775952af95dcea59dc8e91ac4cde30af03cd308e4092c5d6a0a7ccd7a131599448856412102b7f55d64e8ba20077f2c9e629c312e2da2667689cc7835d6b5f9fde0245d1cbc', value: 26370841, sequenceNo: 4294967295, outputScript: '76a91402a6a02a8bbdc6a9ebeb74bf5d8b9f7d20ad386688ac', }, { prevOut: { txid: '5932b2af9cb1226b9bc59233427afe37c9c7f88f650c5a834e343022bc40bc5b', outIdx: 18, }, inputScript: '41ea0603fcf7d14ccdc4efffc0f3c651c4e3ce57c404b2bc9fc5f71fd652a7ce2ba3cb1895206ed3b59ae0d58071912b3ab3f46a1f0dd5539b254ae8b0740a0065412102b7fc7453a54a1ba3f31046d9ec78e102f640cade834efe5edd3a0d0a947844e4', value: 3053762, sequenceNo: 4294967295, outputScript: '76a914fcc200903ed9167def3df599c599d0c98b2cea0588ac', }, { prevOut: { txid: '5932b2af9cb1226b9bc59233427afe37c9c7f88f650c5a834e343022bc40bc5b', outIdx: 22, }, inputScript: '41e80a5eba60db24a51c0599b6b2e721cf9a46bf818fe0e9cec40b855ea5a928e24ff25767d3bd34d6d6c184d50590f20dcf73a73f9ee56ecc7a5cdfed65e5f710412102f6553541f1d9cd9faaeaf53342ac09a2c7c6b5a598c112060a6f3f894ca50851', value: 3278623, sequenceNo: 4294967295, outputScript: '76a914692a38590fe1786dca47d2cbcc0ee30d969ca0c788ac', }, { prevOut: { txid: '5932b2af9cb1226b9bc59233427afe37c9c7f88f650c5a834e343022bc40bc5b', outIdx: 26, }, inputScript: '415bea41b13af76e10f4807c43fb577363399b369b1d83bf2382fdef48235a7c32a2ef9d3a98156458ce3e85df259b5351d37bf9a144f2eb736fe562542bd0479c41210285cdb2a7fb877c0dde24ab437ae152ee7e8e32e9c850f16fc5f9ed23a95bb53c', value: 3534883, sequenceNo: 4294967295, outputScript: '76a91486b2a4458787245715865c9ea5e42f8d68e8828988ac', }, { prevOut: { txid: '5932b2af9cb1226b9bc59233427afe37c9c7f88f650c5a834e343022bc40bc5b', outIdx: 48, }, inputScript: '41751e7046792b1f4961d3c6369d24fad477f0be0120a3b89afe6768d6e4dfed8b24634f020178308fc07065f1c75552277611241313aea2174e355a3a395aecbf412102051de8523c2910874441e60fb9216be126effc875a7fe94bb427fb5c6fa353d6', value: 7546746, sequenceNo: 4294967295, outputScript: '76a914c472cd5ea7282180aa6a663498e98c2b781afa0488ac', }, { prevOut: { txid: '5932b2af9cb1226b9bc59233427afe37c9c7f88f650c5a834e343022bc40bc5b', outIdx: 58, }, inputScript: '415750a10a4d6d697b0e7a69c69b5ea5ebc2c382153dafed928cbe1427a9c50bee62dcb3623317b4eec2d1563eab85f8bf7b9c1bc72e981dd4e546e6588ab864b9412102d9e8928fa33d190ff0dad48989804494016914fa7ace7461793a95b4ea4b7928', value: 11875440, sequenceNo: 4294967295, outputScript: '76a914457a8a10ca1b8ab373c7e5e9ea7d784e8ce2efd188ac', }, { prevOut: { txid: '5932b2af9cb1226b9bc59233427afe37c9c7f88f650c5a834e343022bc40bc5b', outIdx: 60, }, inputScript: '412dbd961304300e86d8589586f5553757ff2ad49ad7f5f044c4f4b73a95a81d6b853d35f21de4b058743be38b0d3f239690088897006658591294a875f5400f2841210203553bdf5e4c0406661b10b9bba39ae1920144eec88414acd18bd5ec838f31ec', value: 12066672, sequenceNo: 4294967295, outputScript: '76a91406cbe837f5a8b81ec8fddcf6e46c15b84b43965788ac', }, { prevOut: { txid: '5932b2af9cb1226b9bc59233427afe37c9c7f88f650c5a834e343022bc40bc5b', outIdx: 70, }, inputScript: '41d6eb014368a0f5afc239a5524ba482f04fbf9f93e5604a60fbf8de342f02f6af70433dd065b9d6c879442d32a1370de5c623796f492f62f703a502f0723bf36f4121029007d7023dd0a6b7bcd1c9ca995d0b707ee727ddf4fa035e93d245554f9dea34', value: 31042739, sequenceNo: 4294967295, outputScript: '76a9145ab8a85ea3f6bf3a69b15b9f7570aeb021df77b488ac', }, { prevOut: { txid: '5932b2af9cb1226b9bc59233427afe37c9c7f88f650c5a834e343022bc40bc5b', outIdx: 71, }, inputScript: '41bb8e694016dfb2b475a93fd6478ba4d97ce55b4753b4552e0ce46bf477a66d28f7f8bf63ef1fe770acc281c8305b7579648c60de4b1f4cf7d2ac783f09af5e1341210217c675b14a2e262379af4407533eb2ffb11df17394feb380be4f272b2c3c9b63', value: 34725141, sequenceNo: 4294967295, outputScript: '76a9149704c9d13afb31a9b84ea5cb56140499e54743bd88ac', }, { prevOut: { txid: '5ba0d328f4e845887fab783234db98aa1c4a73cb5cdd14a2503af78f520cba8b', outIdx: 21, }, inputScript: '4157fa15e1e46502edabc7a33472cfafd75fd88ff75331cdd6e1cdb28384e69cac7b4529ae34cf37f3e68699d7921da7354bbd9ade45c2487a81b7935d9a46817c4121036e3cf1f1fe4d0be25ab9cfd2acaa0617ad06a6ab6cbffd2ee01380fed51db99f', value: 4898437, sequenceNo: 4294967295, outputScript: '76a91423dab92affaa336ae18cab2669d116fbfa55b0bf88ac', }, { prevOut: { txid: '5ba0d328f4e845887fab783234db98aa1c4a73cb5cdd14a2503af78f520cba8b', outIdx: 24, }, inputScript: '4160979fb8cb7cdb2ebf7e3fe9f8d9cd0d287cd574c0192b10d795c9e57f69135250e8bca7cc024c6d3392e6f6e5445d78bfbade6d84633fa88e3bb404a3ec3767412103bf8958e3824e6ef7710b8a212ccf1ad13488ec8951d4efba45e836e921b15da2', value: 5379161, sequenceNo: 4294967295, outputScript: '76a914c6a520edfedb88ae478c1fdb309739d62d47dbd088ac', }, { prevOut: { txid: '5ba0d328f4e845887fab783234db98aa1c4a73cb5cdd14a2503af78f520cba8b', outIdx: 32, }, inputScript: '41b79dfb5bf4c291742010c1e0b8972665c1a8e583bff07c26bb2f35125a43f9362fc88f7713547c3b19085eeb3b9933aaeb1820168fb7fb9a1173dd6d9ca011cb4121039060518676dea799bc646eaf5a86e2c6e38e6a48d8c809e125e6ddb29ed63941', value: 8316321, sequenceNo: 4294967295, outputScript: '76a914388d048805daa142def4833f5cb1e02db7013a6f88ac', }, { prevOut: { txid: '5ba0d328f4e845887fab783234db98aa1c4a73cb5cdd14a2503af78f520cba8b', outIdx: 60, }, inputScript: '41c2793e60ee18ac376d860fe3a6abbe5e1c8b562c661a5888c24ccb5def8f9fd56b1db4cd28ffd2becba4acce97061cd85432ee7482ba239200a929ada7acf960412103f2e23426245b7e8a64af3b3dfceb9dd6459467b72a58ff7f2fa7f3d885b3861e', value: 35352936, sequenceNo: 4294967295, outputScript: '76a914cf55018839d8ab8b93de655551357d081f8120c788ac', }, { prevOut: { txid: '5ba0d328f4e845887fab783234db98aa1c4a73cb5cdd14a2503af78f520cba8b', outIdx: 62, }, inputScript: '414b06e85ca333742e179aa5f2088b44bd40cd403625e6cb5cf8f0e80afe13fa058890656c653a6d6f2a9aa1b22af346928424330e9f701f8214c4409bc2e25495412103efddc9a5ddb955265e7006ddac64c2eb46bafdd882dc65dcaf276c1d0def176a', value: 40175305, sequenceNo: 4294967295, outputScript: '76a9147eb48844af0ceae69879fd66456a5afffed24cb788ac', }, { prevOut: { txid: '5ba0d328f4e845887fab783234db98aa1c4a73cb5cdd14a2503af78f520cba8b', outIdx: 64, }, inputScript: '41b0949073aa877f7fa76933c4fd68f9c8b482a844cd6362cfa10fefd782ec74a00a9cb268faa693aeb6861ca8a74a842f1b5b58279314421fa4714688883e94f8412103f0ba973ac5827cfb13ff7015ad2bdc5fbf322e369c71fd1798a9ee3c1faea606', value: 40956943, sequenceNo: 4294967295, outputScript: '76a914e94c40d02b7860a76057a48b826ef847372eb74388ac', }, { prevOut: { txid: '62234103fb01e526293bb3b88696a62dedcc830eac4118265980937471197b11', outIdx: 11, }, inputScript: '410e57d52ac09032c8d9b78973af542c65301d16c7c9af11d7e8c5a5ef600bbde36dfa545c88490ce88ddc300658263541132a765895d51deab392f31c95fc2d21412103ec54521d0f77db84614164b9f294e8db801fcbf5c5681192d9b1479cf88287c2', value: 4594328, sequenceNo: 4294967295, outputScript: '76a9148fddf18aecb230772dec7d9fa6ec5c2eae1303bf88ac', }, { prevOut: { txid: '62234103fb01e526293bb3b88696a62dedcc830eac4118265980937471197b11', outIdx: 22, }, inputScript: '415a307caf8836205fb11e2464b77ae02919ac3a9dcfcfdc18d9d79860a4c495301389fecf65ea97723717b38d489d71e4db3e53bca9f1c6bd5fdba3204e5421d54121025b736a838ac5bceb5b40988e49c32902d5989f3fbc9051ec98ba0b6808ef073c', value: 7254551, sequenceNo: 4294967295, outputScript: '76a914687b26740360cae141c61c9e5dcb03b6100dc42b88ac', }, { prevOut: { txid: '62234103fb01e526293bb3b88696a62dedcc830eac4118265980937471197b11', outIdx: 30, }, inputScript: '415088aa133d722e072f1a090eb676c025de7154a2d2e2bdd6812b58b1101ceb53e6c0b27e24412045044dcdb06e88276f4f4a63e28f98c39b3d67453e5dde9d5741210271ba59b7662f1f7a346065cfa4738cf05521933841761b9cfa31f5e72349f494', value: 9563229, sequenceNo: 4294967295, outputScript: '76a914c9fd6f67f21b1970264ba239e82d4a3c40e2063188ac', }, { prevOut: { txid: '62234103fb01e526293bb3b88696a62dedcc830eac4118265980937471197b11', outIdx: 31, }, inputScript: '415a220e9c760930cfceec2c3d60958e313cb5cecf99ef7fb70c6e26dca5c5b13fe4a64db7fbc2cb26b9b619964fd76f35a2f35f0c4c78c68d8b1705737141e26c412102d190001af9db94b3de57d023ac2d17d0d62d7b5b6c351fd25b77ba2be0223539', value: 9731469, sequenceNo: 4294967295, outputScript: '76a914cfbdaf0aaed19c7fc5e2a39e77cc780db5e333b588ac', }, { prevOut: { txid: '62234103fb01e526293bb3b88696a62dedcc830eac4118265980937471197b11', outIdx: 40, }, inputScript: '415bd5dd0c5198a32c904eeaf61bcc02e473895b5e7e2f5f8e86486eb9a53f7608981be475a2dd42964d4fca04bca1e29d5b18fe6ebf0d4ebaebd7687b8327e8a4412102822ab05d098e4f0f455263f970104eeb0942ccd5f3e36415af2b202b5ace86f9', value: 15786585, sequenceNo: 4294967295, outputScript: '76a914a17017d5f758fcc1372746bce8509c3d23f218a788ac', }, { prevOut: { txid: '62234103fb01e526293bb3b88696a62dedcc830eac4118265980937471197b11', outIdx: 49, }, inputScript: '415f3acedc835b8fceec445222063ca869d4260d89746c16746f237ea244c4412011ede5b644040ecd62e0761216924226b985217ce56cec35054fdf20ab288b6d412103d6fdcf7626fe46d001e5bb777d92423a056054084d61a2c0ffc91a0c0cef8df1', value: 21867579, sequenceNo: 4294967295, outputScript: '76a914d179b30a22db1d4aa04c163f7c1474fc1fbb5c5588ac', }, { prevOut: { txid: '62234103fb01e526293bb3b88696a62dedcc830eac4118265980937471197b11', outIdx: 58, }, inputScript: '41fed29d22902a017b75ec8e04c708f964145e791f5c368c318951f9f23b063c4c4777fbfc07c0107fef490fc4d6495a64de759a384f478bf1c12b84d26d21986c41210351b5cbd17fddc51a2add3d88a1d9fabc29c56ca6d3e912bccc71e69a6342728b', value: 71746009, sequenceNo: 4294967295, outputScript: '76a914f3f590529240d25b82fe10c18efbb64a64f9625988ac', }, { prevOut: { txid: '65dbfe80fdf94e042b0e775a66baf02d0ee4e2148fea4b8388500847bb5c982f', outIdx: 0, }, inputScript: '41dd8c68989718bf445ab5d7809b6516cdec095eab8acfc2e34d8ca9670c1502b8d1a677ede2d4000f4588a82b78282912aa27f83338f69cbf3fad727e81b80da141210300570f3c1121d37167795f36dbe7bf4bfbfbea4b04507f5ca42d2dfdaa85983b', value: 1688043, sequenceNo: 4294967295, outputScript: '76a9143856ed1d33df771934e14e0446518fa21c8ef6f188ac', }, { prevOut: { txid: '754f2405fe479549e4c51ab41811f5a864fa39e5a9f877265dd0e74d6dad47ec', outIdx: 2, }, inputScript: '41319c8dff13ebeec17d74d83a8c728c8ce913e10f4b2291d2a99457b404833558f591d3c174aef7e5c637e0aaf6d2ab3a250af3549ddd6b52d4232ade8aed48b4412103387c765fda65b283de425b4fa1388727056c325bd22fa698a4ace6df5ba6fe91', value: 8049989, sequenceNo: 4294967295, outputScript: '76a914d26a1fac6b5c02e98e839956f3a7547d0c1b5c0088ac', }, { prevOut: { txid: '7fe346109f69368dea5581448d6bd0046bfcb57892ce8757534c05113c7cab3f', outIdx: 45, }, inputScript: '41260777d685ff3b0552995d998880509ef6af55383d352cedc88854c40e243832a2dbdd86b1503b93ebb5e2e3f1f6da1dae27a0cefca73d40a8995ad69ee5d033412103ca8f1e6ef5fa63f97fd1b05a6421c1d768df37c2b6b28764c1ecd73bab20a13a', value: 15469696, sequenceNo: 4294967295, outputScript: '76a9147cf1203b978724009018c3a5e6a605590f6e9fed88ac', }, { prevOut: { txid: '961956635297554fd150048a6e2adaf1441caeb8a8f7cb824fdb79329e7915ce', outIdx: 8, }, inputScript: '41d50570bf2a07db56c2e28faaa9299ea251b66a0388d0c816c7591c7c8c3b90e013560d12266b45c589e2badcd1753e35bc7caf88db1e80d119c9ad77c73044184121036abcc7db8ffa1dc62c2c0ee5f87011e819e4f15f40d70186cd7acc9e2b705f2f', value: 3192502, sequenceNo: 4294967295, outputScript: '76a9146e56ad4a85fa5e2d03f3bc16b52cfcab65c5e74188ac', }, { prevOut: { txid: '961956635297554fd150048a6e2adaf1441caeb8a8f7cb824fdb79329e7915ce', outIdx: 48, }, inputScript: '4149623ef1ee7ace2c8f33db67c9b2ba5d720e47b95242afb1aef62c9d7e4bf7de91cb4236daaced175a55f2946e13b76b7a403c90f77082ed74c922058c2826494121026292302c75da128dfdf92827bad355bb00f677176e630c2fc7f4b8e8e4144177', value: 93002901, sequenceNo: 4294967295, outputScript: '76a914d17e89b26be59dfdbbd2582afdbf785cc11ad56388ac', }, { prevOut: { txid: '9da7a74dd4066e8444630cb3d4557ca8a7f786098733b4ec6d39ade509c6a947', outIdx: 23, }, inputScript: '4136f2d23997838269068038af56408bea30e58d2aaa24eac9798cd7fbd544fd2afbb021178360ec86383f54ea6de1c6e32ea83e96acbbbbb87319614e4aed04ce412102d1dad4b5d20dca9c748452c3a3d64e8d589fe31edc8cdef66c6b083a34733af3', value: 2523800, sequenceNo: 4294967295, outputScript: '76a914888bdff661832d406351713b49c683776b90e7b088ac', }, { prevOut: { txid: '9da7a74dd4066e8444630cb3d4557ca8a7f786098733b4ec6d39ade509c6a947', outIdx: 31, }, inputScript: '41dce1809ca31e4db35d4406f14d1f7da2810a2c662e281342f64e68315f400428c9d7d574faca017044f616b82c63a5c00016212e85cdf1ed4298a2c8db3d8eea41210279aca93bb100bdc842f277f032c8854d089381350609cf5980f904e994201c52', value: 4330481, sequenceNo: 4294967295, outputScript: '76a914e58661c82c66669cbaa2d1e813009d0b5d2fafb888ac', }, { prevOut: { txid: '9da7a74dd4066e8444630cb3d4557ca8a7f786098733b4ec6d39ade509c6a947', outIdx: 46, }, inputScript: '4178d0f9b72584bf409e1c72aa30ef0cbf4449e5d8ecb74d730045bc8397cc870c64af6918f62ce39b4b51f01b56c24c9bbaed750649625ec3eb5383738fb0b5ca4121027ed4bb82bd6ac94dc17035738a21565115f92b842682c9c7fcd6108b767ead7c', value: 7600077, sequenceNo: 4294967295, outputScript: '76a91463b5940d2fd7d998b343a54986ca375ff8cd2bbd88ac', }, { prevOut: { txid: 'b41a4d09e492f66f611bba3ca2cf2c3eaedce811e9cca9a1706ede3b4ae594f1', outIdx: 0, }, inputScript: '41569f9dbdf60f88d56bebbc24ebc8a48ffc3e504af9a4fc8d027d2aa30da0113f30a042028a631ed87333b10f988c49af8db233812019e63f0d4892674de2c3d8412102128780d9d337449c3b2b9cd1008a25acf895fedb6f2706e74916943e3c2d33c2', value: 66688896, sequenceNo: 4294967295, outputScript: '76a91490b66329b172fd43feacbbb461c54183eed1bd5d88ac', }, { prevOut: { txid: 'b9a5cb585cba98a1b13d698e0c19d332e8532119de7e1410e9ff1dfd26ac0516', outIdx: 0, }, inputScript: '417c5e22211868b30521f5421a1ccaf00e0ae2bbf393f9b33de9e126b4481575eb2baaa6f47bd74c204bdebf8a0c0a522c3f500c92f3df2fcc539dfa2fde91a605412102128780d9d337449c3b2b9cd1008a25acf895fedb6f2706e74916943e3c2d33c2', value: 5668556, sequenceNo: 4294967295, outputScript: '76a91490b66329b172fd43feacbbb461c54183eed1bd5d88ac', }, { prevOut: { txid: 'c76711c1bdeae356b492ed630b0e8044d28458581171e1bccbfb92f2960974c8', outIdx: 33, }, inputScript: '413803bd25ba0ff5cd0414441dfc96cf7efaa7b6b944b4611845c4c60e075cf212c57706830e19b4007c7f7ae17c4be3ab20210662bfeb3102ba844ff22f1259c04121020f29f7a7d46ee6c29de9dec33226b4600a83a00a44ac085278c9b7ff3c8fb3b5', value: 9521209, sequenceNo: 4294967295, outputScript: '76a9142fd4bdafad85abcb999c4bab8a2901f92caf487988ac', }, { prevOut: { txid: 'c76711c1bdeae356b492ed630b0e8044d28458581171e1bccbfb92f2960974c8', outIdx: 39, }, inputScript: '41f9f621a78ec30bdd9ef8502039d4a6f97732b31f39b591d96b1c2562f951e41cc89f0aebddc39b532a1255951556fbf5bf28544a7f9c85620303bc620dfa99d1412103d3c212b78eeaa67599c99479c11259100f8d44f5e93a2620b1e7264ab4cd222f', value: 13527166, sequenceNo: 4294967295, outputScript: '76a914979d66b56061bc4b7ac2118f54aecbf86ae5773888ac', }, { prevOut: { txid: 'cb3d57f259b0cab6c9a40a8f19b659a96809bafb7fbbf87bea6e9fc10e2c12e9', outIdx: 16, }, inputScript: '4154b81b0cad31762ded80ab3f5e159fe0245553e1b6e69d153b8c3cb1d4f9064b89d9d8f29b09be3c8191e93ddc7f45c42c016d9b41859a8da851087e1a73a0be4121032870664b4cf912df5171a4a76b0c7c89bc3f9422070e380bc6ce93e02018d886', value: 76789054, sequenceNo: 4294967295, outputScript: '76a9144724b6e46690d083ece0390ced609aeb0488486988ac', }, { prevOut: { txid: 'cfe05ee47ffbb2a60d93c94d1b2f1cb055e3503f43838d6cbc5565dccb5f1a19', outIdx: 15, }, inputScript: '413f34f797ec73f8fc8579008566f790a95da2c02311f1da1f6bfc4a21c72e8bd56bf8b134d2d1e409a53e372825b9c5267d23a87a8599b56129694f25c24a1cf44121030d1c53703449c09a10a12ad03997d2874052f53746f4436d1a108cc20f528407', value: 35013098, sequenceNo: 4294967295, outputScript: '76a9142342542a4947b9bfcedffa803b369ec8c108b0b488ac', }, { prevOut: { txid: 'dbc28e87b613c97c6cdb9c646a257a27a7a5c9ac663d4a049ddfc34163cccbbe', outIdx: 10, }, inputScript: '41f6107a78455d9b3db251d5c3e2478ab346c0876c66c96378a05c38eceec88263098b0f704881d6cf3456aa7be47a6894bfcd121c26742e765cc037f37744b2664121036033a99e5fd9bfe41940c466ab043eb27ce45d2f28753559894f84114c34c51d', value: 4158314, sequenceNo: 4294967295, outputScript: '76a9140f18bea6bafd89a55997c72703006ec7a83d6e6988ac', }, { prevOut: { txid: 'dbc28e87b613c97c6cdb9c646a257a27a7a5c9ac663d4a049ddfc34163cccbbe', outIdx: 42, }, inputScript: '418b6fcd73acbaaef9a063d64fcd86597e490315edfe709aa302d429c6438b52dcc6e7d324b59612b202d4239bef09d8dff1e42363a0ca4716bf1329d8441b01714121020238ff720ccd27a92f0bb0ea63d0c08b73291cf283bb422fdcb63bd9b0a5254f', value: 17803274, sequenceNo: 4294967295, outputScript: '76a914a7bf09e5099224ead64cb27cc9eb38283c3cde4288ac', }, ], outputs: [ { value: 0, outputScript: '6a0446555a0020ab3267b0b667ea2252d414b3714d6f08b5fbf16c0026ce454c903dc6ff002255', }, { value: 506531, outputScript: '76a914d7fc1d156d8ec4384623bb8ceb135df93f2bd93188ac', spentBy: { txid: 'c5f288c020ec4e8701d2114d0f4d7970e9e01e4396abd10ddaebd6e4b44c3d5f', outIdx: 0, }, }, { value: 1175076, outputScript: '76a91447da14cfad47a7971dd345821ac7a81e194e474588ac', spentBy: { txid: '9086ca2908df5f06b61ca2ec2040fc3e7bd39843e35b934f23f89ea7196c7036', outIdx: 39, }, }, { value: 1557619, outputScript: '76a914d6b7baf14352dd9769a9a8bdb1f69cf700766aca88ac', spentBy: { txid: '021b600e1425c69c1977daf2e72a13b83fe40414061641011573eef88834dec1', outIdx: 46, }, }, { value: 1685802, outputScript: '76a914bc53fc8620ece064004a7bb72f0613a0045f6ae488ac', spentBy: { txid: '021b600e1425c69c1977daf2e72a13b83fe40414061641011573eef88834dec1', outIdx: 47, }, }, { value: 1957993, outputScript: '76a914ea34af00f2585bddc37607af492a7d5b35d431fe88ac', spentBy: { txid: 'b3368d0b4495896c4e057a0be8df58cdead551057c0103f35a5e3a4dce7cf4b5', outIdx: 43, }, }, { value: 2280297, outputScript: '76a914dab80f23ec17efe39e3167ac47575f5b102855d288ac', spentBy: { txid: 'b3368d0b4495896c4e057a0be8df58cdead551057c0103f35a5e3a4dce7cf4b5', outIdx: 44, }, }, { value: 2804591, outputScript: '76a914f10147fbbff24aa9f4f9a9f3726760a4abad6a9688ac', spentBy: { txid: '24863ec1bc8eca7d449a37a5bd3bd85e7ccbd2d77ee51c84e1b5b8ade8bada01', outIdx: 45, }, }, { value: 2810950, outputScript: '76a9140b8b9344a473853830f3657c7247e4834171d6fd88ac', spentBy: { txid: 'd3942acaefee091f6bf0a9d34282988b31458bb6b10b7cfc3fcd3471be3c2ea7', outIdx: 50, }, }, { value: 2862208, outputScript: '76a914a0737c0938d04eff2d5074513ee5fd3fd41de38488ac', spentBy: { txid: '9086ca2908df5f06b61ca2ec2040fc3e7bd39843e35b934f23f89ea7196c7036', outIdx: 40, }, }, { value: 2880530, outputScript: '76a914b5d94938a3665b01fc0afee6b6179bb2b9e46b2e88ac', spentBy: { txid: '9086ca2908df5f06b61ca2ec2040fc3e7bd39843e35b934f23f89ea7196c7036', outIdx: 41, }, }, { value: 2894084, outputScript: '76a914dbb0e87717a034774a2435db6c9d4791f58bd43f88ac', spentBy: { txid: '1d385f3ca974110728849913177291fc4e303bdb03481b9bef7adba15734d18f', outIdx: 0, }, }, { value: 3104218, outputScript: '76a9144e3bebebb3ac2785181534094eadccad4ea8dc4688ac', spentBy: { txid: 'ae14b96fc44c7f43c3c2fd268e484b26d8f4794afef5767392fb1c246d7d3e0f', outIdx: 5, }, }, { value: 3122421, outputScript: '76a91458c2d76cd32e1d30d0e62b641d50bdd89200a7f188ac', spentBy: { txid: 'ae14b96fc44c7f43c3c2fd268e484b26d8f4794afef5767392fb1c246d7d3e0f', outIdx: 7, }, }, { value: 3419974, outputScript: '76a9142980d02fa9a25306f3dd195ab9c82a2e2877f67e88ac', spentBy: { txid: 'e59775f4ca2828c87a5b31e415e657d571184891c62860acd5a23523830e38a9', outIdx: 3, }, }, { value: 3594078, outputScript: '76a91451331eca38c944f17ee6354a3ee48193c7eb1b6b88ac', spentBy: { txid: '24863ec1bc8eca7d449a37a5bd3bd85e7ccbd2d77ee51c84e1b5b8ade8bada01', outIdx: 46, }, }, { value: 3794311, outputScript: '76a914755b984555fcd6305583c21d996a8dea7faa67d488ac', spentBy: { txid: '9086ca2908df5f06b61ca2ec2040fc3e7bd39843e35b934f23f89ea7196c7036', outIdx: 42, }, }, { value: 4241488, outputScript: '76a914e245bab4243bd6a8f3932c9dab9df496f003eae488ac', spentBy: { txid: '0ec20eea27fcc5eb12211157a64eb34c58b6df3911d5158faa5824e5fd3002a0', outIdx: 0, }, }, { value: 5771042, outputScript: '76a9147901f7c02a7fb7de87c373c143e15e87989f764b88ac', spentBy: { txid: '021b600e1425c69c1977daf2e72a13b83fe40414061641011573eef88834dec1', outIdx: 48, }, }, { value: 5801672, outputScript: '76a9149db2a709e1f26df987ecd5a5dcb8db0b36a449ef88ac', spentBy: { txid: '493dd3339fca03c94dd0e9b53359630fa8dc2aaef404a6a2328229ae64eb8721', outIdx: 4, }, }, { value: 6529646, outputScript: '76a9141c5dd21c29a653e6922c2058852d9f56e483170188ac', spentBy: { txid: '021b600e1425c69c1977daf2e72a13b83fe40414061641011573eef88834dec1', outIdx: 49, }, }, { value: 6536855, outputScript: '76a9143510f0c92f8b26e26de575140a084773e95f439a88ac', spentBy: { txid: '24863ec1bc8eca7d449a37a5bd3bd85e7ccbd2d77ee51c84e1b5b8ade8bada01', outIdx: 47, }, }, { value: 7742026, outputScript: '76a914ee542bd41bb07264cf9f6e824e45d3446a26077c88ac', spentBy: { txid: 'b3368d0b4495896c4e057a0be8df58cdead551057c0103f35a5e3a4dce7cf4b5', outIdx: 45, }, }, { value: 8072753, outputScript: '76a914c4131be628403d70a62e46dfc13b576af05aa5f088ac', spentBy: { txid: 'dbc41978baa1e1b0d1b098a34722eadf351e19383dcc1266118333060847a8e5', outIdx: 35, }, }, { value: 8820534, outputScript: '76a914f5ffa38db9ffac77b5a1a6c35eebf2415fedf87c88ac', spentBy: { txid: '24863ec1bc8eca7d449a37a5bd3bd85e7ccbd2d77ee51c84e1b5b8ade8bada01', outIdx: 48, }, }, { value: 9000450, outputScript: '76a914b3e42f44a3dff21f72c90555d0ec62b273f0d4a588ac', spentBy: { txid: 'a96de5afa4eee4b098ff8b7423e90d0131673862cb79e7b02a06e084146d5dfe', outIdx: 56, }, }, { value: 11771919, outputScript: '76a91463a7fe1eff49be76e18538f3ed380b7386af1c8f88ac', spentBy: { txid: 'a96de5afa4eee4b098ff8b7423e90d0131673862cb79e7b02a06e084146d5dfe', outIdx: 57, }, }, { value: 13144002, outputScript: '76a91457f118d5f5eecebc88f711a80018bececbeb86e088ac', spentBy: { txid: '262832d24a3b26fd1af1b24f0a7d019579b7ed1f040777d3374c62305c5f4415', outIdx: 39, }, }, { value: 13393930, outputScript: '76a9148d2a8ce8e95b3047b918d8bd24db8c3e39d906cc88ac', spentBy: { txid: '9086ca2908df5f06b61ca2ec2040fc3e7bd39843e35b934f23f89ea7196c7036', outIdx: 43, }, }, { value: 13691033, outputScript: '76a914d6a0a87a3a5ea254ed4a2665ac328a7ef769747688ac', spentBy: { txid: 'c2409334c0c33750529e2f9b762e7dab7ca2fb4e67883cd3244b7cbbdc9add14', outIdx: 36, }, }, { value: 14490346, outputScript: '76a914810c66b72d769d1fefd2c5bb26d20024e25fd35088ac', spentBy: { txid: '493dd3339fca03c94dd0e9b53359630fa8dc2aaef404a6a2328229ae64eb8721', outIdx: 10, }, }, { value: 15649462, outputScript: '76a914b3f036ee778de53049e0152a140bcba4952081f788ac', spentBy: { txid: '262832d24a3b26fd1af1b24f0a7d019579b7ed1f040777d3374c62305c5f4415', outIdx: 40, }, }, { value: 16885611, outputScript: '76a9144dbd06c9f304601d8fe89199ee7afa0afc3e5de688ac', spentBy: { txid: '5beda1f52503457c3e2bd93357ad7510e16e69021c589ce91b092215eb37fce5', outIdx: 2, }, }, { value: 17311755, outputScript: '76a91435cf783dd7fc1a919c5a92d73feedcab1d3e4dd588ac', spentBy: { txid: 'a96de5afa4eee4b098ff8b7423e90d0131673862cb79e7b02a06e084146d5dfe', outIdx: 58, }, }, { value: 19229444, outputScript: '76a914c570835edbc0de4a525a9ba9501eb0b123b8ab1c88ac', spentBy: { txid: 'd3942acaefee091f6bf0a9d34282988b31458bb6b10b7cfc3fcd3471be3c2ea7', outIdx: 51, }, }, { value: 19612475, outputScript: '76a9142368a5b973c7d48fa8343b71cfb51b5a4ccfcb2488ac', spentBy: { txid: '9086ca2908df5f06b61ca2ec2040fc3e7bd39843e35b934f23f89ea7196c7036', outIdx: 44, }, }, { value: 20857697, outputScript: '76a9149163b5cb6618d7d67562270de630da0d62896c1e88ac', spentBy: { txid: '021b600e1425c69c1977daf2e72a13b83fe40414061641011573eef88834dec1', outIdx: 50, }, }, { value: 21475345, outputScript: '76a91464be00bf5c68a60ae520cfa81d051225457572a788ac', spentBy: { txid: '493dd3339fca03c94dd0e9b53359630fa8dc2aaef404a6a2328229ae64eb8721', outIdx: 9, }, }, { value: 21879959, outputScript: '76a9148bc944201dec7391def49db52202a009c6a81f2088ac', spentBy: { txid: 'd3942acaefee091f6bf0a9d34282988b31458bb6b10b7cfc3fcd3471be3c2ea7', outIdx: 52, }, }, { value: 21900743, outputScript: '76a914af6ae4c996d1ab51dd344b1f491c01163169053588ac', spentBy: { txid: '9086ca2908df5f06b61ca2ec2040fc3e7bd39843e35b934f23f89ea7196c7036', outIdx: 45, }, }, { value: 22276723, outputScript: '76a914c1f421d009c6b36b205721c064c2ae5ea3272a4688ac', spentBy: { txid: 'a96de5afa4eee4b098ff8b7423e90d0131673862cb79e7b02a06e084146d5dfe', outIdx: 59, }, }, { value: 22828111, outputScript: '76a9146454f4696e5bbb5eb4d368c162b35f6fcc861e6b88ac', spentBy: { txid: 'd3942acaefee091f6bf0a9d34282988b31458bb6b10b7cfc3fcd3471be3c2ea7', outIdx: 53, }, }, { value: 22829710, outputScript: '76a9142a8af09882e0b5dd047b03e61eb3630e0678325e88ac', spentBy: { txid: '021b600e1425c69c1977daf2e72a13b83fe40414061641011573eef88834dec1', outIdx: 51, }, }, { value: 23106927, outputScript: '76a9147eec957f14c8c35b491f487a8d777cf3b427f47688ac', spentBy: { txid: 'd3942acaefee091f6bf0a9d34282988b31458bb6b10b7cfc3fcd3471be3c2ea7', outIdx: 54, }, }, { value: 25043923, outputScript: '76a9148f41a4d08d01a574210a0d99784248d7b718a6b388ac', spentBy: { txid: '021b600e1425c69c1977daf2e72a13b83fe40414061641011573eef88834dec1', outIdx: 52, }, }, { value: 25946731, outputScript: '76a9149fbf277434a5a0582ffe774693c343e95c442a8188ac', spentBy: { txid: 'c2409334c0c33750529e2f9b762e7dab7ca2fb4e67883cd3244b7cbbdc9add14', outIdx: 37, }, }, { value: 26216189, outputScript: '76a914d35d6706484afdc79bbaab9ce1f84fed4939317f88ac', spentBy: { txid: '24863ec1bc8eca7d449a37a5bd3bd85e7ccbd2d77ee51c84e1b5b8ade8bada01', outIdx: 49, }, }, { value: 27153210, outputScript: '76a914fc64d1ceb75ef723b8bb81f53039f239f69de25d88ac', spentBy: { txid: '24863ec1bc8eca7d449a37a5bd3bd85e7ccbd2d77ee51c84e1b5b8ade8bada01', outIdx: 50, }, }, { value: 27888923, outputScript: '76a9140b395214ae8c35fd7e8bb6921fa478216fd9e41988ac', spentBy: { txid: '9086ca2908df5f06b61ca2ec2040fc3e7bd39843e35b934f23f89ea7196c7036', outIdx: 46, }, }, { value: 28283566, outputScript: '76a9145c9faf662be3667f760e03535c511085a2bc814488ac', spentBy: { txid: 'f7ffab99cca8005728105f2b93a0afd49444116bcd6131b2fcecd0fea40391ab', outIdx: 53, }, }, { value: 29688615, outputScript: '76a914f883cd4d8e8b6e1cba5d127e24c57b45c26b46a288ac', spentBy: { txid: 'de39274a222922abfdd373cd373b1f71fb0e58c0c569ac6bc813d01a1dc64f8e', outIdx: 1, }, }, { value: 32471718, outputScript: '76a9147fe1c85d201af0ab1322d5809aaa03bb7dac05fb88ac', spentBy: { txid: '1ed72329e29d9441dc9fb3ac828fc66d08e52c8e67a9a0b0268b5ce6bb7e0695', outIdx: 4, }, }, { value: 35209256, outputScript: '76a9141ab1428e336477a213d18207570b5008841d24ea88ac', spentBy: { txid: 'de39274a222922abfdd373cd373b1f71fb0e58c0c569ac6bc813d01a1dc64f8e', outIdx: 0, }, }, { value: 40404442, outputScript: '76a914219f01df857ef5faa2c1509b8dc958eb9425f5df88ac', spentBy: { txid: '697372648af8320cd2975e4ea52d9772f6f06d9610e5088f4d92ef3f69422c30', outIdx: 35, }, }, { value: 48107746, outputScript: '76a914a4c2e50019b19c9d152b6327733033253d61efe188ac', spentBy: { txid: 'de39274a222922abfdd373cd373b1f71fb0e58c0c569ac6bc813d01a1dc64f8e', outIdx: 16, }, }, { value: 54611567, outputScript: '76a91479be8c6a6fc20a9f4cd1e55d8e99fef936a5b4fb88ac', spentBy: { txid: 'd3942acaefee091f6bf0a9d34282988b31458bb6b10b7cfc3fcd3471be3c2ea7', outIdx: 55, }, }, { value: 54872231, outputScript: '76a914e8f011eded020ed1605848c7b5e6704eb689b33f88ac', spentBy: { txid: 'de39274a222922abfdd373cd373b1f71fb0e58c0c569ac6bc813d01a1dc64f8e', outIdx: 6, }, }, { value: 56164346, outputScript: '76a9146573038dc2d55422c20b91588f8264f9aa038d6088ac', spentBy: { txid: '9086ca2908df5f06b61ca2ec2040fc3e7bd39843e35b934f23f89ea7196c7036', outIdx: 47, }, }, { value: 58564003, outputScript: '76a9147077be58e7ead7443259fe5409309edbabef41d388ac', spentBy: { txid: '24863ec1bc8eca7d449a37a5bd3bd85e7ccbd2d77ee51c84e1b5b8ade8bada01', outIdx: 51, }, }, { value: 59817398, outputScript: '76a9149cf6eb2a055f3340d31d83bf5e29cfe0e9d919f288ac', spentBy: { txid: 'd3942acaefee091f6bf0a9d34282988b31458bb6b10b7cfc3fcd3471be3c2ea7', outIdx: 56, }, }, { value: 64104923, outputScript: '76a914d12908c4b7be22044226856207328e20e3e1f2c288ac', spentBy: { txid: 'de39274a222922abfdd373cd373b1f71fb0e58c0c569ac6bc813d01a1dc64f8e', outIdx: 15, }, }, { value: 87305777, outputScript: '76a91437a517f6174aed807cb1f9fb26ff25912c8ea4ee88ac', spentBy: { txid: '021b600e1425c69c1977daf2e72a13b83fe40414061641011573eef88834dec1', outIdx: 53, }, }, { value: 91558238, outputScript: '76a914b2094f7a6f5c39a66ddff6852bfef1f6dac495fb88ac', spentBy: { txid: '14483f95867cb556833f90ef73485fc883a04fa31404a650c11208dfc391183e', outIdx: 1, }, }, ], lockTime: 0, timeFirstSeen: 0, size: 11331, isCoinbase: false, tokenEntries: [], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NON_TOKEN', block: { height: 787920, hash: '000000000000000000ecda3dc336cd44ddf32eac28cebdee3c4a0abda75471e0', timestamp: 1681610827, }, }, { txid: 'd02d94a1a520877c60d1e3026c3e85f8995d48d7b90140f83e24ede592c30306', version: 2, inputs: [ { prevOut: { txid: '02cafc6812b1193207d9b336fd109c4044eb02a4ee7d39599d4411f0e797f08e', outIdx: 2, }, inputScript: '48304502210097bb101905c26f6198cd862b64a4174e3263fa3dda571cef058e3fb6576fe1da022002c086b779f8129d6f44697e403c7607c26589659134a9468d4471da65b116774121037f36f6573744fbf433eafc2579737e041a99b242eb0fd88dfe8570b5f6a829c7', value: 98396406, sequenceNo: 4294967295, outputScript: '76a9147d432e8ccc646fe6c52e36c285bce2b75f0f500b88ac', }, ], outputs: [ { value: 0, outputScript: '6a04007461620c49206c696b65206543617368', }, { value: 1000000, outputScript: '76a9144fb74b44c66ab529428a943131f236c80d99b82088ac', spentBy: { txid: 'b16ef7b4c184201dd858325c43cd67459bce352f041f7bf44f4b99972cba81bd', outIdx: 5, }, }, { value: 97395927, outputScript: '76a9147d432e8ccc646fe6c52e36c285bce2b75f0f500b88ac', spentBy: { txid: '2c644fba674af09cec58af30be0a93b6ebe4f48b976e0531aaacb0b3220a1556', outIdx: 0, }, }, ], lockTime: 0, timeFirstSeen: 0, size: 254, isCoinbase: false, tokenEntries: [], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NON_TOKEN', block: { height: 800715, hash: '00000000000000000e5fdea738d0f7e1c30aec453c380b03edf7788ccc154906', timestamp: 1689255059, }, }, { txid: '1083da7ead4779fbab5c5e8291bb7a37abaf4f97f5ff99ee654759b2eaee445b', version: 2, inputs: [ { prevOut: { txid: 'afda16412e7b9770dd15286295d02544cdf5eec2ebe827616c6357647c31b950', outIdx: 0, }, inputScript: '483045022100ad2200533b3fc6cf4d06b1450a0e10cbb493933fe5927e4a9d53e7005547eeea02207f1e406d9a6a33cab711461219cd085ae33c9b605f40c479b7d072659904f35d412102394542bf928bc707dcc156acf72e87c9d2fef77eaefc5f6b836d9ceeb0fc6a3e', value: 1000, sequenceNo: 4294967295, outputScript: '76a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac', }, { prevOut: { txid: 'aff8fdde86ccf5a1afb83fe9600b0b4d598a7317a9ee2a5f799e9f5fe724ab0c', outIdx: 1, }, inputScript: '483045022100a7e1c2bab17698871a4b5d88e15fbcb64578bf1ac525b38c73f21c8ea7e697d702201841b8459beb066fcf388b50a3b4db6a0fd8e0f525fb5353d2de40dbc314c565412102394542bf928bc707dcc156acf72e87c9d2fef77eaefc5f6b836d9ceeb0fc6a3e', value: 550, sequenceNo: 4294967295, outputScript: '76a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac', }, { prevOut: { txid: '691bcc03b07fa12d5e54628eab24497456af57c76a13d646393ad62105a37382', outIdx: 2, }, inputScript: '483045022100d1e6a7051b3a641c490f6f9129903138a9a5d1f11cbdc4727d4272e1b5fc4b170220429f7d0e7e38265e8f2ced7311a6cc39868e930f6d35457164c891e2ac3e7f79412102394542bf928bc707dcc156acf72e87c9d2fef77eaefc5f6b836d9ceeb0fc6a3e', value: 64959, sequenceNo: 4294967295, outputScript: '76a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac', }, ], outputs: [ { value: 0, outputScript: '6a04657461624c71036bdec11e461033145b5d96661e45ba2a40081aad01d34c4da4dac5e42b9961c990fc603ad5c6fed77fff016d57caa7ba8cbcebb33bd47e5eb0707628c0331e4d714054ab773ae4a555c9ea432af23a83104209e5299e86081f5fabe4a744e96eac6675149ce4e7680e342270498d0e68', }, { value: 2000, outputScript: '76a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac', spentBy: { txid: '00dbc347e48d2d422541057f2d3d1a6c32542ec2bea9e59664edbd8613774e7d', outIdx: 0, }, }, { value: 63232, outputScript: '76a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac', spentBy: { txid: 'f189376fd662b113e5da5904e318123aee5f573221e57a2545849ac556f31130', outIdx: 0, }, }, ], lockTime: 0, timeFirstSeen: 0, size: 652, isCoinbase: false, tokenEntries: [], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NON_TOKEN', block: { height: 798057, hash: '00000000000000001007743cd991fa1832e391e147bf75fa6648467ee4e98410', timestamp: 1687751968, }, }, { txid: 'ad44bf5e214ab71bb60a2eee165f368c139cd49c2380c3352f0a4fffc746b36a', version: 1, inputs: [ { prevOut: { txid: 'd5530ac59da5f357713f3294afdced768584232d2cc21e63d9e3d60f4aaeba62', outIdx: 2, }, inputScript: '41176c28f0b30bbd8b985a5960a6e29c3f8cfae093349d48bc609d0957b2eab91ecaf98b5de8b13bd5df66331ae6b9ae5dd73d8a9042bb8d54ae833e826d318cd74121037b28c10168dfb8007d25638ceb6bc13a168a6e4ddcd0aec28591d14387958796', value: 546, sequenceNo: 4294967295, outputScript: '76a914aed3f8a5add35a9ddaf0a07986c2b73a2202727d88ac', }, { prevOut: { txid: 'd5530ac59da5f357713f3294afdced768584232d2cc21e63d9e3d60f4aaeba62', outIdx: 3, }, inputScript: '41bf1a7934dd1c5997953a1cf500bfb2b928b66574782b1dae33546e758d278c58b9527d1ff3b2913d113924eaa3af18d059ae61d49274893af1bc7f233f7bff454121022c2394a4ed0a2d8fb9c30185ea7173a15d69adceda08b96ad408fce866bcd1f9', value: 43110, sequenceNo: 4294967295, outputScript: '76a914d50ec518d64850fda86e926764ce1bce1ba01a1988ac', }, ], outputs: [ { value: 0, outputScript: '6a04535750000101010120aebcae9afe88d61d8b8ed7b8c83c7c2a555583bf8f8591c94a2c9eb82f34816c0453454c4c0831353938383335340100203dd9beaedcbb3ad90eec2214fcf71381fa89b08b899813e182a7a393e1ab06190101010012333236322e39393939393939393939393935', }, { value: 43190, outputScript: '76a914aed3f8a5add35a9ddaf0a07986c2b73a2202727d88ac', spentBy: { txid: '09fe3d1b848dcc23006393604811cd1e79cfb12f79c44b0d76a8822c8910ca69', outIdx: 1, }, }, ], lockTime: 0, timeFirstSeen: 0, size: 450, isCoinbase: false, tokenEntries: [], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NON_TOKEN', block: { height: 800700, hash: '00000000000000000f9a3ee0a9455329c7a66830ff9fbcbc5bd7223263e4afc4', timestamp: 1689250180, }, }, { txid: 'a8c348539a1470b28b9f99693994b918b475634352994dddce80ad544e871b3a', version: 1, inputs: [ { prevOut: { txid: '0fceef4c1425e44171a26a9d089d77dc9bf68b8ab5c1020b411afa1ad253382c', outIdx: 2, }, inputScript: '483045022100fe0f77633de5e95397f2a0fba128e1de7c2467e6d70558c5ef632b5afd504c62022013b4704186e5934f46547212dbd2054a5c8ab08546db945b83477542c51b3b46412103a16e0df390d377ffda5195c4b06148d674a331144fab6ad08e9ec5e8a4e5a4b4', value: 21944557, sequenceNo: 4294967295, outputScript: '76a9148fa951904f6d0ebbc92dc29e761b9eb0a837545c88ac', }, ], outputs: [ { value: 0, outputScript: '6a026d0320eae5710aba50a0a22b266ddbb445e05b7348d15c88cbc2e012a91a09bec3861a4c9654776974746572206b65657073207475726e696e6720746865697220415049206f6e20616e64206f66662e20536f6d6574696d657320697420776f726b732c20736f6d6574696d657320697420646f65736e27742e204665617475726520746f20637265617465207477656574732066726f6d206d656d6f206d617920776f726b20616761696e20617420736f6d6520706f696e742e', }, { value: 21944167, outputScript: '76a9148fa951904f6d0ebbc92dc29e761b9eb0a837545c88ac', spentBy: { txid: 'ce4a58281bda100a572a1d365cb4a83b85d08874daff194179708b18e3f651cf', outIdx: 0, }, }, ], lockTime: 0, timeFirstSeen: 0, size: 390, isCoinbase: false, tokenEntries: [], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NON_TOKEN', block: { height: 800324, hash: '0000000000000000017ec312fd2d03fee5e52ab163733d5f07e380d36138e58e', timestamp: 1689021150, }, }, { txid: '7a0d6ae3384e293183478f681f51a77ef4c71f29957199364bb9ba4d8e1938be', version: 2, inputs: [ { prevOut: { txid: 'a3b227c732f71b8c66b5593923fb41ac19529b4f87d453082bb2289382dbb4c1', outIdx: 1, }, inputScript: '47304402204b55cb0e0a458f41be000f28ac41d23b0ce7ac6184c009a08e21cde04736602a02204e2e36d24c17d174c96e0d716a0d9d51d3009bde27a37d1961cac10fe483bec3412103f1f48963ab04429f0cacf2db96ec8189598b56afd2cde8a614440c78479ae037', value: 500000, sequenceNo: 4294967295, outputScript: '76a914f93029e7593327c5b864ea6896ecfda4fffb6ab888ac', }, { prevOut: { txid: 'ed234d8854c599d88304e807b04b9e41a04b6a9fdfd92b1970e7a27c89a68473', outIdx: 3, }, inputScript: '47304402202ddc33dd6e31885d3ae5c8f35adce3c6af89ba4c7e6bcec60e90f9ac059eb42c022037d2fefcb1ad7e13f066b4bf336cdebf54cb90d59a7375c09f64d5340cf6ceaf412103f1f48963ab04429f0cacf2db96ec8189598b56afd2cde8a614440c78479ae037', value: 28351, sequenceNo: 4294967295, outputScript: '76a914f93029e7593327c5b864ea6896ecfda4fffb6ab888ac', }, { prevOut: { txid: '0a4e28b70b6b1e26e02ea160261edfc7e6ffdb4ee9d7a53cf5e9c50686b4c5cb', outIdx: 1, }, inputScript: '4830450221009d35c5646c47be48040aaa781bce22cc287b2f54d71ea028d9c12fd11dc2115d022025cf6916dfe0c4ad695b8a948eadeafb2bcdc0304dcc701637e33cbf79a401a7412103f1f48963ab04429f0cacf2db96ec8189598b56afd2cde8a614440c78479ae037', value: 200000, sequenceNo: 4294967295, outputScript: '76a914f93029e7593327c5b864ea6896ecfda4fffb6ab888ac', }, { prevOut: { txid: '5909dd3d0cf37141651b629f9ada6b8c45901889e3153e27bc0673461f67fafd', outIdx: 1, }, inputScript: '483045022100cf624e1a8cbc2cb9fd61f55c443490ba9eb922296bdc698c0a7db6a23b9cd33b02206f57d5f503c70a68097f0866c9b46bc7a35b130673b0b9a72b8b71b82b2d004e412103f1f48963ab04429f0cacf2db96ec8189598b56afd2cde8a614440c78479ae037', value: 48995, sequenceNo: 4294967295, outputScript: '76a914f93029e7593327c5b864ea6896ecfda4fffb6ab888ac', }, { prevOut: { txid: '8b837074b3d1f36066c9f0d880e69e3476368e905039fbeebb4bebab083c21fa', outIdx: 0, }, inputScript: '47304402202b097ee4881fc57fe7a1551f8ee8bf9e0b40b6639a038d2c744e399850690e1d022063b5ff44314df2ec83b163683ff46c963a2f021f3e59caf3fa96914cc17342f1412103f1f48963ab04429f0cacf2db96ec8189598b56afd2cde8a614440c78479ae037', value: 4000000, sequenceNo: 4294967295, outputScript: '76a914f93029e7593327c5b864ea6896ecfda4fffb6ab888ac', }, ], outputs: [ { value: 0, outputScript: '6a0464726f7020b76878b29eff39c8c28aaed7d18a166c20057c43beeb90b630264470983c984a04007461624c525374617920776974682075732c2065436173682041667269636120697320746865206e6578742062696720636f6d6d756e69747920696e20746865204166726963616e2063727970746f7370686572652e20', }, { value: 264706, outputScript: '76a914957b59a2bfa17ea7fc234e532b263169b6d34aa988ac', spentBy: { txid: '33b640c3f87a5d7d148bdf8870a3cdfad4dbfdad7abfb39fa97787ca9744b912', outIdx: 1, }, }, { value: 529412, outputScript: '76a9148f882b02e1040f83c2f73007bb334716c38dbffc88ac', spentBy: { txid: '54d7c32d64ac3cb94c55ae4a9ca5c0519ea2dcf65962485e668ebb0bd7b0e990', outIdx: 1, }, }, { value: 529412, outputScript: '76a914f43ac7271cee240bee3796938203105fb54c045c88ac', }, { value: 264706, outputScript: '76a914d5a79acda6dbbe14a686a0c59466f52656330a9588ac', }, { value: 529412, outputScript: '76a91429207c3d229d6163521fbe87e52e64bbe584dbc988ac', spentBy: { txid: '80129555a3900a8d1084a6ecaf24f60dfb4fb25fa93ffed057536f0655d4df24', outIdx: 0, }, }, { value: 264706, outputScript: '76a914e735901add6ea366a0964ab54ad9d9158597f50c88ac', }, { value: 264706, outputScript: '76a91407acf15b7cc6a4c18d8d1c3a5611ea30718c2a0d88ac', }, { value: 529412, outputScript: '76a9146671b4690e282cb79707b1ee696d54a6072329fa88ac', }, { value: 264706, outputScript: '76a914d4f7e7b420eb1c5410abf698c72d790f0c4cc1b388ac', }, { value: 264706, outputScript: '76a914b19e12ae2aa186102486e8348f22b87ae426eafd88ac', }, { value: 529412, outputScript: '76a914e6309418b6e60b8119928ec45b8ba87de8e735f788ac', spentBy: { txid: 'e58c868250f4e2949d5b72a1975d3a90247d59ea019edf4aebd214ed0d4c62b3', outIdx: 0, }, }, { value: 264706, outputScript: '76a914cdba2655ee5abf18a5b6203da5b7d8cc28c36ca888ac', spentBy: { txid: 'f33e2516b93628e6f3608a2233e35af00426eb8ec3cba5836af827ba62cc0bf9', outIdx: 2, }, }, { value: 274783, outputScript: '76a914f93029e7593327c5b864ea6896ecfda4fffb6ab888ac', spentBy: { txid: 'a2d019cffeb34decfe11f6135ee6aec09aeed439f34369ea9216b78a5d2040a0', outIdx: 0, }, }, ], lockTime: 0, timeFirstSeen: 0, size: 1326, isCoinbase: false, tokenEntries: [], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NON_TOKEN', block: { height: 800392, hash: '00000000000000000ce15f89ed2d06604932e5ff53fceebc9a3e1dd6fdff8438', timestamp: 1689078229, }, }, { txid: '22135bb69435023a84c80b1b93b31fc8898c3507eaa70569ed038f32d59599a9', version: 2, inputs: [ { prevOut: { txid: 'd5626b4441deba5ffdb52f32352726e87db432f66c183c7f1c2dc23a81080915', outIdx: 0, }, inputScript: '4730440220718019bbe581cd2d505837df7c76be53f7ea3798cbe2c8e9985b32d95b4f3c9202200db9c710c07e3dc7b2e6153e6af3fc0c6d9a1e9b953a2c5812c3f6a12bcfb8c8412102394542bf928bc707dcc156acf72e87c9d2fef77eaefc5f6b836d9ceeb0fc6a3e', value: 1000, sequenceNo: 4294967295, outputScript: '76a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac', }, { prevOut: { txid: 'd5626b4441deba5ffdb52f32352726e87db432f66c183c7f1c2dc23a81080915', outIdx: 1, }, inputScript: '483045022100a309d82c4a00ea00b2a9f11bc0a1625b4272fbf9c627c723377ee81428c9cc5a0220523f45935bbc9bb8cfa2454cd3be1c9ee3322e5c849c5f4384bb72736958a628412102394542bf928bc707dcc156acf72e87c9d2fef77eaefc5f6b836d9ceeb0fc6a3e', value: 28091, sequenceNo: 4294967295, outputScript: '76a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac', }, ], outputs: [ { value: 0, outputScript: '6a042e7865630005646f67653215000b7d35fda03544a08e65464d54cfae4257eb6db7', }, { value: 554, outputScript: 'a914d37c4c809fe9840e7bfa77b86bd47163f6fb6c6087', spentBy: { txid: '9b9bc49c18a513f04c9d05dcff2d5e1164408b837d95ef695d24524520fb9358', outIdx: 196, }, }, { value: 27785, outputScript: '76a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac', spentBy: { txid: 'c3bae8350772a99d52086720d2712813f8e47313d20f5d914a728cb7a2bcd9ea', outIdx: 0, }, }, ], lockTime: 0, timeFirstSeen: 0, size: 415, isCoinbase: false, tokenEntries: [], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NON_TOKEN', block: { height: 800458, hash: '00000000000000000360b4edae6ac5cf508cf728d508bd6521d4fd9495a24ac6', timestamp: 1689130494, }, }, { txid: '45ec66bc2440d2f94fa2c645e20a44f6fab7c397053ce77a95484c6053104cdc', version: 1, inputs: [ { prevOut: { txid: '979888d682e351714e6a3d6a96dedb57a0f19471a8eb7f4edc20f033f9b359b3', outIdx: 3, }, inputScript: '41597c9b4eff3b012afb56e745c46d3f9cddfbd72fbec655d63d0bfd4b4a23225aa237a425a25685b54a8f767becb9109f1b0951c3ecfe7fa3d1a519a8f49ef6a9c1210363bbf8cf60612f89a8da03416a7ff0f398b315c7217b0c7a15ca52d5fcecb316', value: 546, sequenceNo: 4294967295, token: { tokenId: 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', tokenType: { protocol: 'ALP', type: 'ALP_TOKEN_TYPE_STANDARD', number: 0, }, amount: '39976', isMintBaton: false, entryIdx: 0, }, outputScript: '76a9147276ae7693883fa1165628e298899d8ee9248e7c88ac', }, { prevOut: { txid: '6f6da390386a97d5ce9a585422c11e60e1a4dd30ac6abecef5887ceb1632151c', outIdx: 1, }, inputScript: '410dc4415d0ef25301d74f5fa770bb0b8e4f3bc1b9c87f3d9c65efdbe246468c364ad4a696c9b6491a215643807db3e3ac209e38590bbecd0185d23d045cea8c07c1210363bbf8cf60612f89a8da03416a7ff0f398b315c7217b0c7a15ca52d5fcecb316', value: 546, sequenceNo: 4294967295, token: { tokenId: 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', tokenType: { protocol: 'ALP', type: 'ALP_TOKEN_TYPE_STANDARD', number: 0, }, amount: '228263', isMintBaton: false, entryIdx: 0, }, outputScript: '76a9147276ae7693883fa1165628e298899d8ee9248e7c88ac', }, { prevOut: { txid: '714b40aaf824007782c0af61b0e72ad4e825446111633fbe1b7c5b4a82a1b911', outIdx: 0, }, inputScript: '41d2966d687a9ad26c155c99fcf87b94b2a54b9ecbbf6caee7f6063b756aafa63113c5a16abc21495aaff02b5e558853043038de716c3adbaae2bd69356be4e3ce412102f49a7fd4e0c6cea6401aed57b76b2fb358e1ebbb65fc5782e3c2165c9e850b31', value: 1000, sequenceNo: 4294967295, outputScript: '76a9148b9b3ba9199d98e131b762081c0c31754fb904c288ac', }, { prevOut: { txid: '714b40aaf824007782c0af61b0e72ad4e825446111633fbe1b7c5b4a82a1b911', outIdx: 1, }, inputScript: '41839e901cd8f3f8d20d99c02ffbc6268baa5f6e5d410ec3207f5a582b4a2731b4baa472afec7c78f834bb16900bbc7fb16d2db446cb895a36a18bd520524ae60c412102f49a7fd4e0c6cea6401aed57b76b2fb358e1ebbb65fc5782e3c2165c9e850b31', value: 1000, sequenceNo: 4294967295, outputScript: '76a9148b9b3ba9199d98e131b762081c0c31754fb904c288ac', }, { prevOut: { txid: '714b40aaf824007782c0af61b0e72ad4e825446111633fbe1b7c5b4a82a1b911', outIdx: 2, }, inputScript: '413ba7dddc6fed541f666086c4db0420b03a279f3926c5acc7dbc0d8d51c95cc1f0ad28f030bc4b76d6c42b94caed9750d7f9d7274c017e04049ae64b194299c49412102f49a7fd4e0c6cea6401aed57b76b2fb358e1ebbb65fc5782e3c2165c9e850b31', value: 1000, sequenceNo: 4294967295, outputScript: '76a9148b9b3ba9199d98e131b762081c0c31754fb904c288ac', }, { prevOut: { txid: '714b40aaf824007782c0af61b0e72ad4e825446111633fbe1b7c5b4a82a1b911', outIdx: 3, }, inputScript: '41a14fe12995bd1fbd3e53b00767121e4504a5a97be0482f936cedf3cbc3c04449a3155293522afd213bf4c6e5655c6335179eae63b94afd70a979d63ae7621c04412102f49a7fd4e0c6cea6401aed57b76b2fb358e1ebbb65fc5782e3c2165c9e850b31', value: 1000, sequenceNo: 4294967295, outputScript: '76a9148b9b3ba9199d98e131b762081c0c31754fb904c288ac', }, { prevOut: { txid: '714b40aaf824007782c0af61b0e72ad4e825446111633fbe1b7c5b4a82a1b911', outIdx: 4, }, inputScript: '416c5a43be467820cb14fb8c82a389a80507816dae3649c28f06def267ca9de2c52089d8f7be41076bf1baea97f29538b3a8cbc6cca2e22d7227a58824a676e43f412102f49a7fd4e0c6cea6401aed57b76b2fb358e1ebbb65fc5782e3c2165c9e850b31', value: 1000, sequenceNo: 4294967295, outputScript: '76a9148b9b3ba9199d98e131b762081c0c31754fb904c288ac', }, ], outputs: [ { value: 0, outputScript: '6a503d534c5032000453454e4445e1f25de444e399b6d46fa66e3424c04549a85a14b12bc9a4ddc9cdcdcdcdcd03401f000000006067010000002f9102000000', }, { value: 546, outputScript: '76a914dee50f576362377dd2f031453c0bb09009acaf8188ac', token: { tokenId: 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', tokenType: { protocol: 'ALP', type: 'ALP_TOKEN_TYPE_STANDARD', number: 0, }, amount: '8000', isMintBaton: false, entryIdx: 0, }, }, { value: 2588, outputScript: 'a914f71cf8cb91804a2205901cc0972c3f4a088a1aae87', token: { tokenId: 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', tokenType: { protocol: 'ALP', type: 'ALP_TOKEN_TYPE_STANDARD', number: 0, }, amount: '92000', isMintBaton: false, entryIdx: 0, }, spentBy: { txid: '73ce0b742131ace72e8598a9585971220c4ddcd4d4d13a058bbcc52c355dca2c', outIdx: 0, }, }, { value: 546, outputScript: '76a9147276ae7693883fa1165628e298899d8ee9248e7c88ac', token: { tokenId: 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', tokenType: { protocol: 'ALP', type: 'ALP_TOKEN_TYPE_STANDARD', number: 0, }, amount: '168239', isMintBaton: false, entryIdx: 0, }, spentBy: { txid: '292057e2071f8245317ff4e504a0b57ce3c841f4a9505cfbe7ecc7521d0054ff', outIdx: 4, }, }, ], lockTime: 0, timeFirstSeen: 0, size: 1170, isCoinbase: false, tokenEntries: [ { tokenId: 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', tokenType: { protocol: 'ALP', type: 'ALP_TOKEN_TYPE_STANDARD', number: 0, }, txType: 'SEND', isInvalid: false, burnSummary: '', failedColorings: [], actualBurnAmount: '0', intentionalBurn: '0', burnsMintBatons: false, }, ], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NORMAL', block: { height: 800378, hash: '0000000000000000089a8326edc4a36fd34ecf37fcfcdb2b7ffe9f3dd10bad2a', timestamp: 1689065237, }, }, { txid: '413b57617d2c497b137d31c53151fee595415ec273ef7a111160da8093147ed8', version: 1, inputs: [ { prevOut: { txid: 'ff06c312bef229f6f27989326d9be7e0e142aaa84538967b104b262af69f7f00', outIdx: 41, }, inputScript: '40e9025766015ff3fb2bb1a0643540589de84a6bd74799f6a5130e5f4793bb04b6614eda7d11f0e3c6edba32e8f97952f87fd0065e08d7d5700d668b72880356184730450221009aa1063132f8fe88d11d69438be0320201ec79e74b43af57ec91f6697f9eedc10220294cc6f123ef35a2a92146b3af9f19a8b84745df28ff81b0f918547c0f316aa020912cacf95b220116236c81840549456ae3cc3a537d94e9a5a7691eb609b9e1bb2102ccd325f41c9e343dcfb2b767e9fd1be0b60636c2f08c403bb72d1dc5751a1f0d2c6fbb7b01417e78ad7e21022b7c4d310cd9aee3a0256b2f6399d2d737da47f14582667ea30a159ed879f003ba', value: 1000, sequenceNo: 4294967295, token: { tokenId: 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', tokenType: { protocol: 'ALP', type: 'ALP_TOKEN_TYPE_STANDARD', number: 0, }, amount: '0', isMintBaton: true, entryIdx: 0, }, outputScript: 'a914ea826cc1a3a981d048cd78b66711222bece8ebf287', }, ], outputs: [ { value: 0, outputScript: '6a5032534c503200044d494e5445e1f25de444e399b6d46fa66e3424c04549a85a14b12bc9a4ddc9cdcdcdcdcd0160ae0a00000000', }, { value: 546, outputScript: '76a91472a92e48c5ab72566959db1dbf1b8dce83afabb788ac', token: { tokenId: 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', tokenType: { protocol: 'ALP', type: 'ALP_TOKEN_TYPE_STANDARD', number: 0, }, amount: '700000', isMintBaton: false, entryIdx: 0, }, spentBy: { txid: '4307458f1952db756e959e68aacae82c73a4b86d6e996636d66bf79bca28cbbe', outIdx: 0, }, }, ], lockTime: 0, timeFirstSeen: 0, size: 396, isCoinbase: false, tokenEntries: [ { tokenId: 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', tokenType: { protocol: 'ALP', type: 'ALP_TOKEN_TYPE_STANDARD', number: 0, }, txType: 'MINT', isInvalid: false, burnSummary: '', failedColorings: [], actualBurnAmount: '0', intentionalBurn: '0', burnsMintBatons: false, }, ], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NORMAL', block: { height: 800218, hash: '0000000000000000024133f7d5dcce943020d32452704aab90d00e7bf8c8025e', timestamp: 1688956131, }, }, { txid: '9094e1aab7ac73c680bf66e78cc8311831b3d813e608bff1e07b1854855fc0f1', version: 1, inputs: [ { prevOut: { txid: 'c211f534b17dfe5356c7e83e16202a4621bb7720bb2efb96cfcae14a6bae49ca', outIdx: 1, }, inputScript: '473044022035f1526804dbc6164f905280e1b3ff09841b14471344bb676985ac5ded00bf3602206981faee987c646a169538a8bbef38c5d0e2f377b6d6beb7a56d01a961ca65b1412102063d93675f351cb3b95e671fb2b8b20fa2e0ff624079ab7d32c49cf462286c23', value: 8862751, sequenceNo: 0, outputScript: '76a914d95a60cea21479569e6b1ad39416c8fbc97323c588ac', }, ], outputs: [ { value: 8419613, outputScript: '76a914453c8c15aee05fe5a027d4bf5681cd0bc682c0b788ac', }, { value: 442686, outputScript: '76a914d95a60cea21479569e6b1ad39416c8fbc97323c588ac', }, { value: 0, outputScript: '6a403d3a4554482e4554483a3078613961614633304636353935354336396331364233333435423531443432364439423838426138373a3834313332313a74723a30', }, ], lockTime: 0, timeFirstSeen: 0, size: 300, isCoinbase: false, tokenEntries: [], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NON_TOKEN', block: { height: 800383, hash: '000000000000000014f30e80baf015eab61fb92f62813708fe8ee38ce27a46be', timestamp: 1689070486, }, }, { txid: 'b5782d3a3b55e5ee9e4330a969c2891042ae05fafab7dc05cd14da63e7242f8e', version: 1, inputs: [ { prevOut: { txid: 'f75274016ff26983635008c3c1500c5aa3412e37c55f82bdf2542873376cbec6', outIdx: 1, }, inputScript: '47304402207dd238f4aea9210eb5e311ec38a38c079290f89ba39a436cf1d7c4b3ee10a1b002202516389f5631a67d7eb89e03598dd90a20cae966049445cb1cf82059feb1cd49412103c918521e29ff4986c49c48750d18f1f586a34578ab37f2d206ed8d25abd95d39', value: 12966881, sequenceNo: 4294967295, outputScript: '76a9142cc3608fe629c4f402e511878982bc01bde3445d88ac', }, ], outputs: [ { value: 12966621, outputScript: 'a9145aafaadba9ff909067a640e5e2a46b756aeaf71387', spentBy: { txid: '56f1c0813d1b44697deccdd32b5182448e8c4c1bd6db771ceed779d4a5021ab9', outIdx: 0, }, }, { value: 0, outputScript: '6a14663ddd99990bcd969994ec2288a2a86dc532e1a8', }, ], lockTime: 1692219709, timeFirstSeen: 0, size: 220, isCoinbase: false, tokenEntries: [], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NON_TOKEN', block: { height: 805569, hash: '000000000000000013999983f42bc6270ca0cb0f0247872458546c381a270d01', timestamp: 1692219954, }, }, { txid: '4f33c81d95641eb0f80e793dc96c58a2438f9bb1f18750d8fb3b56c28cd25035', version: 1, inputs: [ { prevOut: { txid: '87e03ed9f2b1690a0c32484a9084d0a5a494a084616c8a2cda2d967bf58f4758', outIdx: 0, }, inputScript: '483045022100d48813ecde5c1878587111e2b6a931414acfcd133af250f3b6f15363afb554570220780fc409af09dfe10b6b0bcbea7c1e588456a8fb029571363e31f3e4174f652b412103562731a08eb23e6260b516c4564f746033e9080bc9f61ad2158a63927500b8b1', value: 584106362127, sequenceNo: 4294967295, outputScript: '76a914231f7087937684790d1049294f3aef9cfb7b05dd88ac', }, ], outputs: [ { value: 214748364700, outputScript: '76a914231f7087937684790d1049294f3aef9cfb7b05dd88ac', spentBy: { txid: '63edf584e527ec15d6d3ffa33db55cd6055a5d8af3dd778a8ffaad35a594a180', outIdx: 0, }, }, { value: 154609632467, outputScript: '76a914231f7087937684790d1049294f3aef9cfb7b05dd88ac', spentBy: { txid: '99eec6fec03c1c3d2043a07f38488aae9416e676f859a940e521b619f9308d6d', outIdx: 0, }, }, { value: 214748364700, outputScript: '76a914231f7087937684790d1049294f3aef9cfb7b05dd88ac', spentBy: { txid: '63edf584e527ec15d6d3ffa33db55cd6055a5d8af3dd778a8ffaad35a594a180', outIdx: 1, }, }, ], lockTime: 0, timeFirstSeen: 0, size: 260, isCoinbase: false, tokenEntries: [], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NON_TOKEN', block: { height: 800230, hash: '00000000000000000684570e0d5280b19e0bda62d3a620c0f90d1d7943b17fed', timestamp: 1688964211, }, }, { txid: 'f5d4c112cfd22701226ba050cacfacc3aff570964c6196f67e326fc3224300a2', version: 2, inputs: [ { prevOut: { txid: 'bafb00bf336f89b3e82af6c9692fcfa9c0c485fd296f6e2d11a9aeffdf0658c9', outIdx: 0, }, inputScript: '483045022100b868d4ead08172bcd2dd666173f108476d6ad83536b1d669063ad48281271fa002202a1ed3f7217cf0dbc6c6114d6087a06d9d2fdcd60e3a8301f9e311be53eb91e6412102bda582a4bf42f9b5520d5023f37e53744a8f048a404d477f3187ccf3fdb0a43e', value: 30426563453, sequenceNo: 4294967295, outputScript: '76a9147c09e7cf1b2c40d4e08057e47bda9cabcdfd208588ac', }, { prevOut: { txid: 'f67df2c2b0cbe6bbbf2d364305e5e5b57498bc688f5062a46ce6b273c160999b', outIdx: 0, }, inputScript: '483045022100d5d1c650af14f2fc58ede519385f184c81cc26ab7e49bcb81700bb4fd91a33cb02204dc0c91c915dadf5bff6bb2d54713bb338072e229a9053c557a11c8df7aedf92412102bda582a4bf42f9b5520d5023f37e53744a8f048a404d477f3187ccf3fdb0a43e', value: 30225350654, sequenceNo: 4294967295, outputScript: '76a9147c09e7cf1b2c40d4e08057e47bda9cabcdfd208588ac', }, { prevOut: { txid: '28b8e29e2450dbe72f23e759de05466aad90a6a21161617ec4eddbe2f16cf0dc', outIdx: 0, }, inputScript: '4730440220299867647c651e087b2362cb46cea041d1340cc9d292a747b79f60a39c5e6ed802200ce28ec8453004aed4698cc91572571c3fb6c7dcab27e5b81838ba11b7fcf47b41210347225b9b5059b0ecd55f4d3c611396893be1e4ddbc4f6fb359344d66d90603f1', value: 16816891900, sequenceNo: 4294967295, outputScript: '76a9149f27f41b828019c141516179ab00e538a3f65a1788ac', }, { prevOut: { txid: '7292db20773561ff63fdccbe0860333a2cfed8efac6077257219a9d9d0e6c7c2', outIdx: 2, }, inputScript: '473044022025398b595b52b1960da327514ecfa26c764d996336b983ef8a7824082e421c93022016691118624efe4eba4c70faca860f41e36b68ee39a8d7513d170ff43c984a84412102b56ea6bd49408c797271d772223ac85b92b10237d160e5ad4ff4c44c8ed00a50', value: 6908486500, sequenceNo: 4294967295, outputScript: '76a9142d9c4a5a292f5fef07b8cfe76b0c66d90d15a5d688ac', }, { prevOut: { txid: '7292db20773561ff63fdccbe0860333a2cfed8efac6077257219a9d9d0e6c7c2', outIdx: 0, }, inputScript: '473044022004f43cc3b9ceb7f913ee80e128b08ecfa4b610c70e1099fa406f275b3df23dfc022061fd494601553d29396bba36c60510e8d2b8c3383144c917ef20a7c6866d13a64121037b692abf35a795b78711ef5455f47579360375d96eca666e2c48c85260f2eb4b', value: 5101721000, sequenceNo: 4294967295, outputScript: '76a9149d1cd75ff25c555213ab5fef4bbd6a180a5e5e7d88ac', }, { prevOut: { txid: 'da7a6212606e606a58a933794140c6cd61766f9ba7d818d6a8c81132f2b4fa7e', outIdx: 0, }, inputScript: '47304402200ffaa112a2dd2dc15616ad307375c48ca838d0ad181e0ad832e288f664d821a40220734a342dd82e175c422831ef59ae0023c01ce38589b9f1748157176533d7164f412102b56ea6bd49408c797271d772223ac85b92b10237d160e5ad4ff4c44c8ed00a50', value: 4810263100, sequenceNo: 4294967295, outputScript: '76a9142d9c4a5a292f5fef07b8cfe76b0c66d90d15a5d688ac', }, { prevOut: { txid: '440e06a33ca6313a4fe046f1376940494f3eb1cd0691c72b8ee0969d76470a12', outIdx: 0, }, inputScript: '483045022100aaff5eb83c7bada3dd363f528ea7c8cf0e48d745d994d1d4bb2b906da832a24c02205430989a4dd0955690eb9a2e41b7c91bfefd3b79b82f27e0ac434f9cc55aca86412103dea874528abd36e4f0484357f510dd3875c8a2aec1baeac73dfaf5ea860bd510', value: 4000000000, sequenceNo: 4294967295, outputScript: '76a9143d1ee2681911f344e77097a8bd25576e1da1c51788ac', }, { prevOut: { txid: 'bb1f39fc82315bac41cfe967f435c11a928c3b2a1bb6562880c04f25b8eafd29', outIdx: 0, }, inputScript: '483045022100f0a2b357da363c552f8ddcc9ce6e2e7554a90ea7214a80af62c0195d1df1ad0b02202847981fc03cb4f751b7640b283f722a241896e5a1b6221cb9c31ead0b44e524412102b56ea6bd49408c797271d772223ac85b92b10237d160e5ad4ff4c44c8ed00a50', value: 3463133300, sequenceNo: 4294967295, outputScript: '76a9142d9c4a5a292f5fef07b8cfe76b0c66d90d15a5d688ac', }, { prevOut: { txid: 'e9c35dd14fbb770d497160a4119aefbd17d0e920ebaf91a12639d58e6f6a3fc4', outIdx: 0, }, inputScript: '483045022100a5df4cf238b2014f748f3ebe6c7e83cd1a32322bcea736d169b1576e0f1604c70220363170a21ae3c612dc4ef6bd4b6348ac528956e499f7c5d28a3cb40d439024d9412102b56ea6bd49408c797271d772223ac85b92b10237d160e5ad4ff4c44c8ed00a50', value: 3295559900, sequenceNo: 4294967295, outputScript: '76a9142d9c4a5a292f5fef07b8cfe76b0c66d90d15a5d688ac', }, { prevOut: { txid: '76c2cd906a42adb530b9cecd184c49230786f8ccdd6c498d743c17e197c53a1f', outIdx: 0, }, inputScript: '483045022100dd42a76f7d99703eb5f08adbdf0a55b76ca80ba84668a181a26a0a4586e894bc0220754cd7b68540db2cd51cd599880585c6005e43953ca7af282d3929b9234703a741210297dffbcf8f417222ddadba818df5110a4b83fccdce773479c2044f25d3cb0071', value: 1412324900, sequenceNo: 4294967295, outputScript: '76a91429981fd0910f0851fcb3422b37abb5de442de33188ac', }, { prevOut: { txid: '66d8b183d7fd40dc5e8d06e16a6189b8d80b07a7615601e008ec476cb847e6be', outIdx: 0, }, inputScript: '4730440220219e5b506570bc1d259d792a2ef33ed04a4a5ad01614bfab79d2f0f903b257d102205bd60750fca96273264c6f736397693198203f2047e457c3878b166d6145a46d412103020f6e4ca80517db5a8ce83bf33169e018f75aa55a399678a8a33dcf0506c3fc', value: 99900000, sequenceNo: 4294967295, outputScript: '76a914ef95330c2b65976c5c63b03d544207b5e55bb58b88ac', }, { prevOut: { txid: '5acbc53048b80ce6f0caa7fe402ec8719f6b5d0a679b9a7646a099bfe4453bd0', outIdx: 0, }, inputScript: '483045022100f74b49fa395d8cd8d92d23e9c076e090a654ab30d0fd28dcf862fe827bb35d3c02206cd85e601f7b183934553e829e2e97c20062ecc23316fd8b033a29781e7261bf412102609d0ef47f50c17d99aa5db555e267ab2f91521c8f9c6be0301ae3e69c031176', value: 8625006, sequenceNo: 4294967295, outputScript: '76a9144d746dbe5864a2635376326a3995f8ada2ec339d88ac', }, { prevOut: { txid: '434b32c3cb3583a5ce0d27dcfdf8a8c99266d5a909af28e914c62526e9cc0d60', outIdx: 1, }, inputScript: '47304402205260a686c8b3d2272a92930d76fda2dae5f9a16afee43c3b2ddb2c3d2742ab930220176b484e84b26a422481e2e6fb690484c2f6cab7d1b03af31f44a8b77aa4b58a412102609d0ef47f50c17d99aa5db555e267ab2f91521c8f9c6be0301ae3e69c031176', value: 8625000, sequenceNo: 4294967295, outputScript: '76a9144d746dbe5864a2635376326a3995f8ada2ec339d88ac', }, ], outputs: [ { value: 106577441836, outputScript: '76a914a520c86a08366941cd90d22e11ac1c7eefa2db3788ac', spentBy: { txid: '78bc65dce0917bf1d3c29f8a9cce02204cc85f90dfeda68356d7338e4d68be4f', outIdx: 1, }, }, ], lockTime: 0, timeFirstSeen: 0, size: 1962, isCoinbase: false, tokenEntries: [], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NON_TOKEN', block: { height: 800383, hash: '000000000000000014f30e80baf015eab61fb92f62813708fe8ee38ce27a46be', timestamp: 1689070486, }, }, { txid: 'b2c9c056339d41ec59341541dda8bd6e570730beba485e14eb54d0a073700c22', version: 1, inputs: [ { prevOut: { txid: '8918611f5a0a432269a4e1dc560e9189b9423089d2dbe94d972515b7c96b214a', outIdx: 1, }, inputScript: '419e644b0b2da83425baffd1d591632fb07216568e0e63c93f4794541aad34801779ce531c546bc8896086670cb59c27afe70972d7e9c058d9c6c43da43ecf6ba4c121020cd8434356c9c73fe2efb9cce867cd86e2649fb77fc28b9bd72f17cf9c4b221a', value: 546, sequenceNo: 4294967295, token: { tokenId: 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', tokenType: { protocol: 'ALP', type: 'ALP_TOKEN_TYPE_STANDARD', number: 0, }, amount: '2000', isMintBaton: false, entryIdx: 0, }, outputScript: '76a914378c3b416e77e01198c01ad215b8afd0bb72799488ac', }, { prevOut: { txid: '696ec00c645c715a146ad6a910295ece67d846e0e6e7f925519df07880a968e9', outIdx: 14, }, inputScript: '41c865813618b58d4e6f311d70392b4f1cc15d160ef6813ea95255246b0da2ee3dbfc057138998354b81c67df5e23f75ebd39c6d5b2071b0c7ae4d8521469beef7412102f49a7fd4e0c6cea6401aed57b76b2fb358e1ebbb65fc5782e3c2165c9e850b31', value: 1000, sequenceNo: 4294967295, outputScript: '76a9148b9b3ba9199d98e131b762081c0c31754fb904c288ac', }, ], outputs: [ { value: 0, outputScript: '6a5031534c5032000453454e4445e1f25de444e399b6d46fa66e3424c04549a85a14b12bc9a4ddc9cdcdcdcdcd01d00700000000', }, { value: 546, outputScript: '76a914acdbf937b086ddaa970072a610daa8d10f14549a88ac', token: { tokenId: 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', tokenType: { protocol: 'ALP', type: 'ALP_TOKEN_TYPE_STANDARD', number: 0, }, amount: '2000', isMintBaton: false, entryIdx: 0, }, }, ], lockTime: 0, timeFirstSeen: 1715869910, size: 387, isCoinbase: false, tokenEntries: [ { tokenId: 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', tokenType: { protocol: 'ALP', type: 'ALP_TOKEN_TYPE_STANDARD', number: 0, }, txType: 'SEND', isInvalid: false, burnSummary: '', failedColorings: [], actualBurnAmount: '0', intentionalBurn: '0', burnsMintBatons: false, }, ], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NORMAL', block: { height: 844893, hash: '00000000000000001e5a53b3ee70596a3c02f847a295a01116302af25529cc68', timestamp: 1715871026, }, }, { txid: 'd8fe456c89357c23ac6d240fe9319ce9ba393c9c3833631046a265ca7c8349e6', version: 2, inputs: [ { prevOut: { txid: '00e43308683a7b97dbfa97f8e8a83dc76c0a49b36c646ef0e31410c546b1bacd', outIdx: 1, }, inputScript: '415baa3f7a0f8294ec0555e1f825c43043eb229f4e2c0bf9a5a676838a881daca32346bbdee05b346f95c39cd69ef709990fd19bbf574f69bf898cd9a1cba001a341210353f81d61d41d6e22c73ab449476113dea124afe3972991cd237e654f15950b7c', value: 2531619, sequenceNo: 4294967295, outputScript: '76a914821407ac2993f8684227004f4086082f3f801da788ac', }, ], outputs: [ { value: 4200, outputScript: '76a91430f16ad77116a4b9e7f337743e35271323d63e0d88ac', spentBy: { txid: '16eb156e4b2688706f9e3da8bdcba69f363d1975ccb363b02274a7ff13d3220a', outIdx: 0, }, }, { value: 2527200, outputScript: '76a914821407ac2993f8684227004f4086082f3f801da788ac', spentBy: { txid: '6f77cfe3148f28de4b982c18d34e79b8d51a368a7c94354b7399133318daf129', outIdx: 1, }, }, ], lockTime: 0, timeFirstSeen: 1727906695, size: 219, isCoinbase: false, tokenEntries: [], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NON_TOKEN', block: { height: 864888, hash: '000000000000000004fd9bd9e95cdc1d6b6bf963c743fec7c808d407d4e52c46', timestamp: 1727906948, }, }, { txid: '083b7862bae48e78549ccf63833896f5f4f5bdef5c380a108fa99cdb64261fa3', version: 2, inputs: [ { prevOut: { txid: 'c7bde74f7a8ebfc57dc373036aac13277ae23f4f94593f0accf87a4317900fd4', outIdx: 3, }, inputScript: '41f9e93665c2d65431ce4142cc4fb93ec0818b50732c52be2de33df3fc4d056fb923351bdef771f46bb235371b5def9fec16590484c0525f27d1179e7e32ab30d641210353f81d61d41d6e22c73ab449476113dea124afe3972991cd237e654f15950b7c', value: 2524161, sequenceNo: 4294967295, outputScript: '76a914821407ac2993f8684227004f4086082f3f801da788ac', }, ], outputs: [ { value: 4200, outputScript: '76a914eeae0fea781c26c93879523ba5a47c244c768ece88ac', spentBy: { txid: 'afde4b9345a7bdbb20eaf7a0da3c730ca28334b8ceffce0a3e7b720b0c5d5a09', outIdx: 0, }, }, { value: 2519742, outputScript: '76a914821407ac2993f8684227004f4086082f3f801da788ac', spentBy: { txid: '68d48755e671dbd6946d811dff04532ede483cee2f3c284e08dddec3d7c351d9', outIdx: 0, }, }, ], lockTime: 0, timeFirstSeen: 1727907007, size: 219, isCoinbase: false, tokenEntries: [], tokenFailedParsings: [], tokenStatus: 'TOKEN_STATUS_NON_TOKEN', }, ], parsedBlock: { hash: '0000000000000000000000000000000000000000000000000000000000000000', height: 819346, miner: 'unknown, ...863u', staker: { staker: 'ecash:qrpkjsd0fjxd7m332mmlu9px6pwkzaufpcn2u7jcwt', reward: 62500000, }, numTxs: 27, parsedTxs: [ { txid: '4f33c81d95641eb0f80e793dc96c58a2438f9bb1f18750d8fb3b56c28cd25035', genesisInfo: false, opReturnInfo: false, txFee: 260, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a914231f7087937684790d1049294f3aef9cfb7b05dd88ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [], }, totalSatsSent: 584106361867, tokenSendInfo: false, tokenBurnInfo: false, }, { txid: 'f5d4c112cfd22701226ba050cacfacc3aff570964c6196f67e326fc3224300a2', genesisInfo: false, opReturnInfo: false, txFee: 2877, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a9147c09e7cf1b2c40d4e08057e47bda9cabcdfd208588ac', '76a9149f27f41b828019c141516179ab00e538a3f65a1788ac', '76a9142d9c4a5a292f5fef07b8cfe76b0c66d90d15a5d688ac', '76a9149d1cd75ff25c555213ab5fef4bbd6a180a5e5e7d88ac', '76a9143d1ee2681911f344e77097a8bd25576e1da1c51788ac', '76a91429981fd0910f0851fcb3422b37abb5de442de33188ac', '76a914ef95330c2b65976c5c63b03d544207b5e55bb58b88ac', '76a9144d746dbe5864a2635376326a3995f8ada2ec339d88ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '76a914a520c86a08366941cd90d22e11ac1c7eefa2db3788ac', 106577441836, ], ], }, totalSatsSent: 106577441836, tokenSendInfo: false, tokenBurnInfo: false, }, { txid: 'd5be7a4b483f9fdbbe3bf46cfafdd0100d5dbeee0b972f4dabc8ae9d9962fa55', genesisInfo: false, opReturnInfo: { app: 'CashFusion', msg: '', stackArray: [ '46555a00', 'ab3267b0b667ea2252d414b3714d6f08b5fbf16c0026ce454c903dc6ff002255', ], tokenId: false, }, txFee: 11430, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a91412934a7a99b69a60c3b99f991cd79d257104f5a688ac', '76a91415c0b62c9f73847ca9a686561216c20b487a0aad88ac', '76a914a4e299724b8e81474df916c25c7a816a43c8748888ac', '76a9147afa62562b93fecaff30190ee3a2836dcb95d42f88ac', '76a91468e15e8bfe2d969b7963181b976e6833e294661288ac', '76a914f941b2e03f973ce5b13766159eef190963e2393488ac', '76a9146e3430f87a128ac4509fb0547f07ba0e3e8cea7688ac', '76a914c72166790bc8c779163e17b11939a6bd6022a7e188ac', '76a91483c54d6ec805f4db16c935f5bb89da791f971ac888ac', '76a914336fb64b7e98221f82aced275440c29e7e1d11b388ac', '76a914b114a9d636ac7558c04e902c3a1f7c1fd9008bcd88ac', '76a91411667c453097adf3e71d08986df7766c26f3399088ac', '76a914a125966da9024acea37f867323778641ff0e891888ac', '76a914e03ba5757763a00aaa8aa9eda71da51610d5ef2788ac', '76a914b13b05d51174d91381b0ea6fb07a6345eea1abf788ac', '76a914349c3f91c2782b235ae0d1a2c3acf053d554170788ac', '76a9143afafec322ef1a4f70a6ca68dd9090182716181888ac', '76a914cb74cf87cd355cd01505645eaf165646a4eb1ce988ac', '76a914c42245ebeb7fea2996e5e0f65537b56fb58ea97d88ac', '76a91447d7bc2240955fd18d53c67c4b814e166b152ec388ac', '76a91410b45d95195a71957b43bb82762e6cb48e67888f88ac', '76a914894e84afe4b07413c99087067292aca67d286fbf88ac', '76a91473b804181c01f16cbf63fe262e9a0c8de929af1e88ac', '76a9147b1a9441467759f8693bdac3e356ab6110bebe1c88ac', '76a914443f7cf9987b921c10d888c3d617c54aba5e8fb088ac', '76a91490de1562e4aadc991dc13d28a9d112461fea9cb888ac', '76a914273808f74a845b9b77345d43cb679ca793c5e9e688ac', '76a91402a6a02a8bbdc6a9ebeb74bf5d8b9f7d20ad386688ac', '76a914fcc200903ed9167def3df599c599d0c98b2cea0588ac', '76a914692a38590fe1786dca47d2cbcc0ee30d969ca0c788ac', '76a91486b2a4458787245715865c9ea5e42f8d68e8828988ac', '76a914c472cd5ea7282180aa6a663498e98c2b781afa0488ac', '76a914457a8a10ca1b8ab373c7e5e9ea7d784e8ce2efd188ac', '76a91406cbe837f5a8b81ec8fddcf6e46c15b84b43965788ac', '76a9145ab8a85ea3f6bf3a69b15b9f7570aeb021df77b488ac', '76a9149704c9d13afb31a9b84ea5cb56140499e54743bd88ac', '76a91423dab92affaa336ae18cab2669d116fbfa55b0bf88ac', '76a914c6a520edfedb88ae478c1fdb309739d62d47dbd088ac', '76a914388d048805daa142def4833f5cb1e02db7013a6f88ac', '76a914cf55018839d8ab8b93de655551357d081f8120c788ac', '76a9147eb48844af0ceae69879fd66456a5afffed24cb788ac', '76a914e94c40d02b7860a76057a48b826ef847372eb74388ac', '76a9148fddf18aecb230772dec7d9fa6ec5c2eae1303bf88ac', '76a914687b26740360cae141c61c9e5dcb03b6100dc42b88ac', '76a914c9fd6f67f21b1970264ba239e82d4a3c40e2063188ac', '76a914cfbdaf0aaed19c7fc5e2a39e77cc780db5e333b588ac', '76a914a17017d5f758fcc1372746bce8509c3d23f218a788ac', '76a914d179b30a22db1d4aa04c163f7c1474fc1fbb5c5588ac', '76a914f3f590529240d25b82fe10c18efbb64a64f9625988ac', '76a9143856ed1d33df771934e14e0446518fa21c8ef6f188ac', '76a914d26a1fac6b5c02e98e839956f3a7547d0c1b5c0088ac', '76a9147cf1203b978724009018c3a5e6a605590f6e9fed88ac', '76a9146e56ad4a85fa5e2d03f3bc16b52cfcab65c5e74188ac', '76a914d17e89b26be59dfdbbd2582afdbf785cc11ad56388ac', '76a914888bdff661832d406351713b49c683776b90e7b088ac', '76a914e58661c82c66669cbaa2d1e813009d0b5d2fafb888ac', '76a91463b5940d2fd7d998b343a54986ca375ff8cd2bbd88ac', '76a91490b66329b172fd43feacbbb461c54183eed1bd5d88ac', '76a9142fd4bdafad85abcb999c4bab8a2901f92caf487988ac', '76a914979d66b56061bc4b7ac2118f54aecbf86ae5773888ac', '76a9144724b6e46690d083ece0390ced609aeb0488486988ac', '76a9142342542a4947b9bfcedffa803b369ec8c108b0b488ac', '76a9140f18bea6bafd89a55997c72703006ec7a83d6e6988ac', '76a914a7bf09e5099224ead64cb27cc9eb38283c3cde4288ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '6a0446555a0020ab3267b0b667ea2252d414b3714d6f08b5fbf16c0026ce454c903dc6ff002255', 0, ], [ '76a914d7fc1d156d8ec4384623bb8ceb135df93f2bd93188ac', 506531, ], [ '76a91447da14cfad47a7971dd345821ac7a81e194e474588ac', 1175076, ], [ '76a914d6b7baf14352dd9769a9a8bdb1f69cf700766aca88ac', 1557619, ], [ '76a914bc53fc8620ece064004a7bb72f0613a0045f6ae488ac', 1685802, ], [ '76a914ea34af00f2585bddc37607af492a7d5b35d431fe88ac', 1957993, ], [ '76a914dab80f23ec17efe39e3167ac47575f5b102855d288ac', 2280297, ], [ '76a914f10147fbbff24aa9f4f9a9f3726760a4abad6a9688ac', 2804591, ], [ '76a9140b8b9344a473853830f3657c7247e4834171d6fd88ac', 2810950, ], [ '76a914a0737c0938d04eff2d5074513ee5fd3fd41de38488ac', 2862208, ], [ '76a914b5d94938a3665b01fc0afee6b6179bb2b9e46b2e88ac', 2880530, ], [ '76a914dbb0e87717a034774a2435db6c9d4791f58bd43f88ac', 2894084, ], [ '76a9144e3bebebb3ac2785181534094eadccad4ea8dc4688ac', 3104218, ], [ '76a91458c2d76cd32e1d30d0e62b641d50bdd89200a7f188ac', 3122421, ], [ '76a9142980d02fa9a25306f3dd195ab9c82a2e2877f67e88ac', 3419974, ], [ '76a91451331eca38c944f17ee6354a3ee48193c7eb1b6b88ac', 3594078, ], [ '76a914755b984555fcd6305583c21d996a8dea7faa67d488ac', 3794311, ], [ '76a914e245bab4243bd6a8f3932c9dab9df496f003eae488ac', 4241488, ], [ '76a9147901f7c02a7fb7de87c373c143e15e87989f764b88ac', 5771042, ], [ '76a9149db2a709e1f26df987ecd5a5dcb8db0b36a449ef88ac', 5801672, ], [ '76a9141c5dd21c29a653e6922c2058852d9f56e483170188ac', 6529646, ], [ '76a9143510f0c92f8b26e26de575140a084773e95f439a88ac', 6536855, ], [ '76a914ee542bd41bb07264cf9f6e824e45d3446a26077c88ac', 7742026, ], [ '76a914c4131be628403d70a62e46dfc13b576af05aa5f088ac', 8072753, ], [ '76a914f5ffa38db9ffac77b5a1a6c35eebf2415fedf87c88ac', 8820534, ], [ '76a914b3e42f44a3dff21f72c90555d0ec62b273f0d4a588ac', 9000450, ], [ '76a91463a7fe1eff49be76e18538f3ed380b7386af1c8f88ac', 11771919, ], [ '76a91457f118d5f5eecebc88f711a80018bececbeb86e088ac', 13144002, ], [ '76a9148d2a8ce8e95b3047b918d8bd24db8c3e39d906cc88ac', 13393930, ], [ '76a914d6a0a87a3a5ea254ed4a2665ac328a7ef769747688ac', 13691033, ], [ '76a914810c66b72d769d1fefd2c5bb26d20024e25fd35088ac', 14490346, ], [ '76a914b3f036ee778de53049e0152a140bcba4952081f788ac', 15649462, ], [ '76a9144dbd06c9f304601d8fe89199ee7afa0afc3e5de688ac', 16885611, ], [ '76a91435cf783dd7fc1a919c5a92d73feedcab1d3e4dd588ac', 17311755, ], [ '76a914c570835edbc0de4a525a9ba9501eb0b123b8ab1c88ac', 19229444, ], [ '76a9142368a5b973c7d48fa8343b71cfb51b5a4ccfcb2488ac', 19612475, ], [ '76a9149163b5cb6618d7d67562270de630da0d62896c1e88ac', 20857697, ], [ '76a91464be00bf5c68a60ae520cfa81d051225457572a788ac', 21475345, ], [ '76a9148bc944201dec7391def49db52202a009c6a81f2088ac', 21879959, ], [ '76a914af6ae4c996d1ab51dd344b1f491c01163169053588ac', 21900743, ], [ '76a914c1f421d009c6b36b205721c064c2ae5ea3272a4688ac', 22276723, ], [ '76a9146454f4696e5bbb5eb4d368c162b35f6fcc861e6b88ac', 22828111, ], [ '76a9142a8af09882e0b5dd047b03e61eb3630e0678325e88ac', 22829710, ], [ '76a9147eec957f14c8c35b491f487a8d777cf3b427f47688ac', 23106927, ], [ '76a9148f41a4d08d01a574210a0d99784248d7b718a6b388ac', 25043923, ], [ '76a9149fbf277434a5a0582ffe774693c343e95c442a8188ac', 25946731, ], [ '76a914d35d6706484afdc79bbaab9ce1f84fed4939317f88ac', 26216189, ], [ '76a914fc64d1ceb75ef723b8bb81f53039f239f69de25d88ac', 27153210, ], [ '76a9140b395214ae8c35fd7e8bb6921fa478216fd9e41988ac', 27888923, ], [ '76a9145c9faf662be3667f760e03535c511085a2bc814488ac', 28283566, ], [ '76a914f883cd4d8e8b6e1cba5d127e24c57b45c26b46a288ac', 29688615, ], [ '76a9147fe1c85d201af0ab1322d5809aaa03bb7dac05fb88ac', 32471718, ], [ '76a9141ab1428e336477a213d18207570b5008841d24ea88ac', 35209256, ], [ '76a914219f01df857ef5faa2c1509b8dc958eb9425f5df88ac', 40404442, ], [ '76a914a4c2e50019b19c9d152b6327733033253d61efe188ac', 48107746, ], [ '76a91479be8c6a6fc20a9f4cd1e55d8e99fef936a5b4fb88ac', 54611567, ], [ '76a914e8f011eded020ed1605848c7b5e6704eb689b33f88ac', 54872231, ], [ '76a9146573038dc2d55422c20b91588f8264f9aa038d6088ac', 56164346, ], [ '76a9147077be58e7ead7443259fe5409309edbabef41d388ac', 58564003, ], [ '76a9149cf6eb2a055f3340d31d83bf5e29cfe0e9d919f288ac', 59817398, ], [ '76a914d12908c4b7be22044226856207328e20e3e1f2c288ac', 64104923, ], [ '76a91437a517f6174aed807cb1f9fb26ff25912c8ea4ee88ac', 87305777, ], [ '76a914b2094f7a6f5c39a66ddff6852bfef1f6dac495fb88ac', 91558238, ], ], }, totalSatsSent: 1308715143, tokenSendInfo: false, tokenBurnInfo: false, }, { txid: 'b5782d3a3b55e5ee9e4330a969c2891042ae05fafab7dc05cd14da63e7242f8e', genesisInfo: false, opReturnInfo: { app: 'unknown', msg: '0x663ddd99990bcd9699...', stackArray: ['663ddd99990bcd969994ec2288a2a86dc532e1a8'], tokenId: false, }, txFee: 260, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a9142cc3608fe629c4f402e511878982bc01bde3445d88ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ 'a9145aafaadba9ff909067a640e5e2a46b756aeaf71387', 12966621, ], ['6a14663ddd99990bcd969994ec2288a2a86dc532e1a8', 0], ], }, totalSatsSent: 12966621, tokenSendInfo: false, tokenBurnInfo: false, }, { txid: '9094e1aab7ac73c680bf66e78cc8311831b3d813e608bff1e07b1854855fc0f1', genesisInfo: false, opReturnInfo: { app: 'unknown', msg: '=:ETH.ETH:0xa9aaF30F65955C69c16B3345B51D426D9B88Ba87:841321:tr:0', stackArray: [ '3d3a4554482e4554483a3078613961614633304636353935354336396331364233333435423531443432364439423838426138373a3834313332313a74723a30', ], tokenId: false, }, txFee: 452, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a914d95a60cea21479569e6b1ad39416c8fbc97323c588ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '76a914453c8c15aee05fe5a027d4bf5681cd0bc682c0b788ac', 8419613, ], [ '6a403d3a4554482e4554483a3078613961614633304636353935354336396331364233333435423531443432364439423838426138373a3834313332313a74723a30', 0, ], ], }, totalSatsSent: 8419613, tokenSendInfo: false, tokenBurnInfo: false, }, { txid: '7a0d6ae3384e293183478f681f51a77ef4c71f29957199364bb9ba4d8e1938be', genesisInfo: false, opReturnInfo: { app: 'Airdrop', msg: '', stackArray: [ '64726f70', 'b76878b29eff39c8c28aaed7d18a166c20057c43beeb90b630264470983c984a', '00746162', '5374617920776974682075732c2065436173682041667269636120697320746865206e6578742062696720636f6d6d756e69747920696e20746865204166726963616e2063727970746f7370686572652e20', ], tokenId: 'b76878b29eff39c8c28aaed7d18a166c20057c43beeb90b630264470983c984a', }, txFee: 2561, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a914f93029e7593327c5b864ea6896ecfda4fffb6ab888ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '6a0464726f7020b76878b29eff39c8c28aaed7d18a166c20057c43beeb90b630264470983c984a04007461624c525374617920776974682075732c2065436173682041667269636120697320746865206e6578742062696720636f6d6d756e69747920696e20746865204166726963616e2063727970746f7370686572652e20', 0, ], [ '76a914957b59a2bfa17ea7fc234e532b263169b6d34aa988ac', 264706, ], [ '76a9148f882b02e1040f83c2f73007bb334716c38dbffc88ac', 529412, ], [ '76a914f43ac7271cee240bee3796938203105fb54c045c88ac', 529412, ], [ '76a914d5a79acda6dbbe14a686a0c59466f52656330a9588ac', 264706, ], [ '76a91429207c3d229d6163521fbe87e52e64bbe584dbc988ac', 529412, ], [ '76a914e735901add6ea366a0964ab54ad9d9158597f50c88ac', 264706, ], [ '76a91407acf15b7cc6a4c18d8d1c3a5611ea30718c2a0d88ac', 264706, ], [ '76a9146671b4690e282cb79707b1ee696d54a6072329fa88ac', 529412, ], [ '76a914d4f7e7b420eb1c5410abf698c72d790f0c4cc1b388ac', 264706, ], [ '76a914b19e12ae2aa186102486e8348f22b87ae426eafd88ac', 264706, ], [ '76a914e6309418b6e60b8119928ec45b8ba87de8e735f788ac', 529412, ], [ '76a914cdba2655ee5abf18a5b6203da5b7d8cc28c36ca888ac', 264706, ], ], }, totalSatsSent: 4500002, tokenSendInfo: false, tokenBurnInfo: false, }, { txid: 'd02d94a1a520877c60d1e3026c3e85f8995d48d7b90140f83e24ede592c30306', genesisInfo: false, opReturnInfo: { app: 'Cashtab Msg', msg: 'I like eCash', stackArray: ['00746162', '49206c696b65206543617368'], tokenId: false, }, txFee: 479, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a9147d432e8ccc646fe6c52e36c285bce2b75f0f500b88ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ ['6a04007461620c49206c696b65206543617368', 0], [ '76a9144fb74b44c66ab529428a943131f236c80d99b82088ac', 1000000, ], ], }, totalSatsSent: 1000000, tokenSendInfo: false, tokenBurnInfo: false, }, { txid: 'd8fe456c89357c23ac6d240fe9319ce9ba393c9c3833631046a265ca7c8349e6', genesisInfo: false, opReturnInfo: false, txFee: 219, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a914821407ac2993f8684227004f4086082f3f801da788ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '76a91430f16ad77116a4b9e7f337743e35271323d63e0d88ac', 4200, ], ], }, totalSatsSent: 4200, tokenSendInfo: false, tokenBurnInfo: false, }, { txid: '083b7862bae48e78549ccf63833896f5f4f5bdef5c380a108fa99cdb64261fa3', genesisInfo: false, opReturnInfo: false, txFee: 219, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a914821407ac2993f8684227004f4086082f3f801da788ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '76a914eeae0fea781c26c93879523ba5a47c244c768ece88ac', 4200, ], ], }, totalSatsSent: 4200, tokenSendInfo: false, tokenBurnInfo: false, }, { txid: '45ec66bc2440d2f94fa2c645e20a44f6fab7c397053ce77a95484c6053104cdc', genesisInfo: false, opReturnInfo: false, txFee: 2412, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a9147276ae7693883fa1165628e298899d8ee9248e7c88ac', '76a9148b9b3ba9199d98e131b762081c0c31754fb904c288ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '6a503d534c5032000453454e4445e1f25de444e399b6d46fa66e3424c04549a85a14b12bc9a4ddc9cdcdcdcdcd03401f000000006067010000002f9102000000', 0, ], [ '76a914dee50f576362377dd2f031453c0bb09009acaf8188ac', 546, ], [ 'a914f71cf8cb91804a2205901cc0972c3f4a088a1aae87', 2588, ], ], }, totalSatsSent: 3134, tokenSendInfo: { tokenId: 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', parsedTokenType: 'ALP', txType: 'SEND', tokenChangeOutputs: { dataType: 'Map', value: [ [ '76a9147276ae7693883fa1165628e298899d8ee9248e7c88ac', { dataType: 'BigNumberReplacer', value: '168239', }, ], ], }, tokenReceivingOutputs: { dataType: 'Map', value: [ [ '76a914dee50f576362377dd2f031453c0bb09009acaf8188ac', { dataType: 'BigNumberReplacer', value: '8000', }, ], [ 'a914f71cf8cb91804a2205901cc0972c3f4a088a1aae87', { dataType: 'BigNumberReplacer', value: '92000', }, ], ], }, tokenSendingOutputScripts: { dataType: 'Set', value: [ '76a9147276ae7693883fa1165628e298899d8ee9248e7c88ac', ], }, }, tokenBurnInfo: false, }, { txid: '1083da7ead4779fbab5c5e8291bb7a37abaf4f97f5ff99ee654759b2eaee445b', genesisInfo: false, opReturnInfo: { app: 'Cashtab Encrypted', msg: '', stackArray: [ '65746162', '036bdec11e461033145b5d96661e45ba2a40081aad01d34c4da4dac5e42b9961c990fc603ad5c6fed77fff016d57caa7ba8cbcebb33bd47e5eb0707628c0331e4d714054ab773ae4a555c9ea432af23a83104209e5299e86081f5fabe4a744e96eac6675149ce4e7680e342270498d0e68', ], tokenId: false, }, txFee: 1277, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '6a04657461624c71036bdec11e461033145b5d96661e45ba2a40081aad01d34c4da4dac5e42b9961c990fc603ad5c6fed77fff016d57caa7ba8cbcebb33bd47e5eb0707628c0331e4d714054ab773ae4a555c9ea432af23a83104209e5299e86081f5fabe4a744e96eac6675149ce4e7680e342270498d0e68', 0, ], [ '76a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac', 2000, ], ], }, totalSatsSent: 2000, tokenSendInfo: false, tokenBurnInfo: false, }, { txid: '22135bb69435023a84c80b1b93b31fc8898c3507eaa70569ed038f32d59599a9', genesisInfo: false, opReturnInfo: { app: 'Alias (beta)', msg: 'doge2', stackArray: [ '2e786563', '00', '646f676532', '000b7d35fda03544a08e65464d54cfae4257eb6db7', ], tokenId: false, }, txFee: 752, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '6a042e7865630005646f67653215000b7d35fda03544a08e65464d54cfae4257eb6db7', 0, ], ['a914d37c4c809fe9840e7bfa77b86bd47163f6fb6c6087', 554], ], }, totalSatsSent: 554, tokenSendInfo: false, tokenBurnInfo: false, }, { txid: '004e018dd98520aa722ee76c608771dd578a044f38103a8298f25e6ffbc7c3ba', genesisInfo: false, opReturnInfo: false, txFee: 481, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a914821407ac2993f8684227004f4086082f3f801da788ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '6a04534c500001010453454e4420aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb108000000000000271008000000000589ffd0', 0, ], [ '76a914dcc535261a43835ca12352d0926ba06cf07cbe8388ac', 546, ], ], }, totalSatsSent: 546, tokenSendInfo: { tokenId: 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', parsedTokenType: 'SLP', txType: 'SEND', tokenChangeOutputs: { dataType: 'Map', value: [ [ '76a914821407ac2993f8684227004f4086082f3f801da788ac', { dataType: 'BigNumberReplacer', value: '92930000', }, ], ], }, tokenReceivingOutputs: { dataType: 'Map', value: [ [ '76a914dcc535261a43835ca12352d0926ba06cf07cbe8388ac', { dataType: 'BigNumberReplacer', value: '10000', }, ], ], }, tokenSendingOutputScripts: { dataType: 'Set', value: [ '76a914821407ac2993f8684227004f4086082f3f801da788ac', ], }, }, tokenBurnInfo: false, }, { txid: '0110cd886ecd2d9570e98b7501cd039f4e5352d69659a46f1a49cc19c1869701', genesisInfo: false, opReturnInfo: false, txFee: 481, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a914821407ac2993f8684227004f4086082f3f801da788ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '6a04534c500001010453454e4420aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1080000000000002710080000000005856bf0', 0, ], [ '76a91469724b96df46096cc95b1a6d408a4240ea80d85588ac', 546, ], ], }, totalSatsSent: 546, tokenSendInfo: { tokenId: 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', parsedTokenType: 'SLP', txType: 'SEND', tokenChangeOutputs: { dataType: 'Map', value: [ [ '76a914821407ac2993f8684227004f4086082f3f801da788ac', { dataType: 'BigNumberReplacer', value: '92630000', }, ], ], }, tokenReceivingOutputs: { dataType: 'Map', value: [ [ '76a91469724b96df46096cc95b1a6d408a4240ea80d85588ac', { dataType: 'BigNumberReplacer', value: '10000', }, ], ], }, tokenSendingOutputScripts: { dataType: 'Set', value: [ '76a914821407ac2993f8684227004f4086082f3f801da788ac', ], }, }, tokenBurnInfo: false, }, { txid: '327101f6f3b740280a6e9fbd8edc41f4f0500633672975a5974a4147c94016a5', genesisInfo: false, opReturnInfo: false, txFee: 481, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a914821407ac2993f8684227004f4086082f3f801da788ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '6a04534c500001010453454e4420aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1080000000000002710080000000005843370', 0, ], [ '76a91458cddba2449285814dae43d4ed4a1c9998f3693e88ac', 546, ], ], }, totalSatsSent: 546, tokenSendInfo: { tokenId: 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', parsedTokenType: 'SLP', txType: 'SEND', tokenChangeOutputs: { dataType: 'Map', value: [ [ '76a914821407ac2993f8684227004f4086082f3f801da788ac', { dataType: 'BigNumberReplacer', value: '92550000', }, ], ], }, tokenReceivingOutputs: { dataType: 'Map', value: [ [ '76a91458cddba2449285814dae43d4ed4a1c9998f3693e88ac', { dataType: 'BigNumberReplacer', value: '10000', }, ], ], }, tokenSendingOutputScripts: { dataType: 'Set', value: [ '76a914821407ac2993f8684227004f4086082f3f801da788ac', ], }, }, tokenBurnInfo: false, }, { txid: 'aa13c6f214ff58f36ed5e108a7f36d8f98729c50186b27a53b989c7f36fbf517', genesisInfo: false, opReturnInfo: false, txFee: 481, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '6a04534c500001010453454e4420aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1080000000000002710080000000000986f70', 0, ], [ '76a914821407ac2993f8684227004f4086082f3f801da788ac', 546, ], ], }, totalSatsSent: 546, tokenSendInfo: { tokenId: 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', parsedTokenType: 'SLP', txType: 'SEND', tokenChangeOutputs: { dataType: 'Map', value: [ [ '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', { dataType: 'BigNumberReplacer', value: '9990000', }, ], ], }, tokenReceivingOutputs: { dataType: 'Map', value: [ [ '76a914821407ac2993f8684227004f4086082f3f801da788ac', { dataType: 'BigNumberReplacer', value: '10000', }, ], ], }, tokenSendingOutputScripts: { dataType: 'Set', value: [ '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', ], }, }, tokenBurnInfo: false, }, { txid: '6ffcc83e76226bd32821cc6862ce9b363b22594247a4e73ccf3701b0023592b2', genesisInfo: false, opReturnInfo: false, txFee: 1137, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '6a04534c500001010453454e442098183238638ecb4ddc365056e22de0e8a05448c1e6084bae247fae5a74ad4f4808000000000000003708000000003b9a72a4', 0, ], [ '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', 546, ], ], }, totalSatsSent: 546, tokenSendInfo: { tokenId: '98183238638ecb4ddc365056e22de0e8a05448c1e6084bae247fae5a74ad4f48', parsedTokenType: 'SLP', txType: 'SEND', tokenChangeOutputs: { dataType: 'Map', value: [ [ '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', { dataType: 'BigNumberReplacer', value: '999977636', }, ], ], }, tokenReceivingOutputs: { dataType: 'Map', value: [ [ '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', { dataType: 'BigNumberReplacer', value: '55', }, ], ], }, tokenSendingOutputScripts: { dataType: 'Set', value: [ '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', ], }, }, tokenBurnInfo: false, }, { txid: 'fb70df00c07749082756054522d3f08691fd9caccd0e0abf736df23d22845a6e', genesisInfo: false, opReturnInfo: false, txFee: 1137, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '6a04534c500001010453454e44207443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d0800000000068c953f08001299507b7b143a', 0, ], [ '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', 546, ], ], }, totalSatsSent: 546, tokenSendInfo: { tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', parsedTokenType: 'SLP', txType: 'SEND', tokenChangeOutputs: { dataType: 'Map', value: [ [ '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', { dataType: 'BigNumberReplacer', value: '5235120528888890', }, ], ], }, tokenReceivingOutputs: { dataType: 'Map', value: [ [ '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', { dataType: 'BigNumberReplacer', value: '109876543', }, ], ], }, tokenSendingOutputScripts: { dataType: 'Set', value: [ '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', ], }, }, tokenBurnInfo: false, }, { txid: '25345b0bf921a2a9080c647768ba440bbe84499f4c7773fba8a1b03e88ae7fe7', genesisInfo: false, opReturnInfo: false, txFee: 1137, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a9141c13ddb8dd422bbe02dc2ae8798b4549a67a3c1d88ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '6a04534c500001010453454e4420fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa08000000001dcd65000800000007e7339728', 0, ], [ '76a914dadf34cde9c774fdd6340cd2916a9b9c5d57cf4388ac', 546, ], ], }, totalSatsSent: 546, tokenSendInfo: { tokenId: 'fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa', parsedTokenType: 'SLP', txType: 'SEND', tokenChangeOutputs: { dataType: 'Map', value: [ [ '76a9141c13ddb8dd422bbe02dc2ae8798b4549a67a3c1d88ac', { dataType: 'BigNumberReplacer', value: '33943689000', }, ], ], }, tokenReceivingOutputs: { dataType: 'Map', value: [ [ '76a914dadf34cde9c774fdd6340cd2916a9b9c5d57cf4388ac', { dataType: 'BigNumberReplacer', value: '500000000', }, ], ], }, tokenSendingOutputScripts: { dataType: 'Set', value: [ '76a9141c13ddb8dd422bbe02dc2ae8798b4549a67a3c1d88ac', ], }, }, tokenBurnInfo: false, }, { txid: '413b57617d2c497b137d31c53151fee595415ec273ef7a111160da8093147ed8', genesisInfo: false, opReturnInfo: false, txFee: 454, xecSendingOutputScripts: { dataType: 'Set', value: ['a914ea826cc1a3a981d048cd78b66711222bece8ebf287'], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '6a5032534c503200044d494e5445e1f25de444e399b6d46fa66e3424c04549a85a14b12bc9a4ddc9cdcdcdcdcd0160ae0a00000000', 0, ], [ '76a91472a92e48c5ab72566959db1dbf1b8dce83afabb788ac', 546, ], ], }, totalSatsSent: 546, tokenSendInfo: false, tokenBurnInfo: false, }, { txid: 'b2c9c056339d41ec59341541dda8bd6e570730beba485e14eb54d0a073700c22', genesisInfo: false, opReturnInfo: false, txFee: 1000, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a914378c3b416e77e01198c01ad215b8afd0bb72799488ac', '76a9148b9b3ba9199d98e131b762081c0c31754fb904c288ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '6a5031534c5032000453454e4445e1f25de444e399b6d46fa66e3424c04549a85a14b12bc9a4ddc9cdcdcdcdcd01d00700000000', 0, ], [ '76a914acdbf937b086ddaa970072a610daa8d10f14549a88ac', 546, ], ], }, totalSatsSent: 546, tokenSendInfo: { tokenId: 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', parsedTokenType: 'ALP', txType: 'SEND', tokenChangeOutputs: { dataType: 'Map', value: [], }, tokenReceivingOutputs: { dataType: 'Map', value: [ [ '76a914acdbf937b086ddaa970072a610daa8d10f14549a88ac', { dataType: 'BigNumberReplacer', value: '2000', }, ], ], }, tokenSendingOutputScripts: { dataType: 'Set', value: [ '76a914378c3b416e77e01198c01ad215b8afd0bb72799488ac', ], }, }, tokenBurnInfo: false, }, { txid: '010114b9bbe776def1a512ad1e96a4a06ec4c34fc79bcb5d908845f5102f6b0f', genesisInfo: { tokenId: '010114b9bbe776def1a512ad1e96a4a06ec4c34fc79bcb5d908845f5102f6b0f', }, opReturnInfo: false, txFee: 455, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a914104e67d912a7aab2a159bba141477e5867c04bfd88ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '6a04534c500001010747454e45534953054c6f6c6c79054c4f4c4c591468747470733a2f2f636173687461622e636f6d2f4c0001084c00080162ea854d0fc000', 0, ], ], }, totalSatsSent: 0, tokenSendInfo: false, tokenBurnInfo: false, }, { txid: '0167e881fcb359cdfc82af5fc6c0821daf55f40767694eea2f23c0d42a9b1c17', genesisInfo: false, opReturnInfo: false, txFee: 1638, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a9146d69b5cbe7c85d87628473c43620c0daa9a8102988ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '6a04534c500001010453454e44207e7dacd72dcdb14e00a03dd3aff47f019ed51a6f1f4e4f532ae50692f62bc4e50800000000002737100800000000000f3636', 0, ], ], }, totalSatsSent: 0, tokenSendInfo: { tokenId: '7e7dacd72dcdb14e00a03dd3aff47f019ed51a6f1f4e4f532ae50692f62bc4e5', parsedTokenType: 'SLP', txType: 'SEND', tokenChangeOutputs: { dataType: 'Map', value: [ [ '76a9146d69b5cbe7c85d87628473c43620c0daa9a8102988ac', { dataType: 'BigNumberReplacer', value: '3566918', }, ], ], }, tokenReceivingOutputs: { dataType: 'Map', value: [], }, tokenSendingOutputScripts: { dataType: 'Set', value: [ '76a9146d69b5cbe7c85d87628473c43620c0daa9a8102988ac', ], }, }, tokenBurnInfo: false, }, { txid: '6b139007a0649f99a1a099c7c924716ee1920f74ea83111f6426854d4c3c3c79', genesisInfo: false, opReturnInfo: false, txFee: 1137, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a9144bb6f659b8dafd99527e0c0a3289f121b0a0209f88ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '6a04534c500001010453454e4420fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa08000000000c380cdc', 0, ], ], }, totalSatsSent: 0, tokenSendInfo: false, tokenBurnInfo: { tokenId: 'fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa', undecimalizedTokenBurnAmount: '100', }, }, { txid: 'ad44bf5e214ab71bb60a2eee165f368c139cd49c2380c3352f0a4fffc746b36a', genesisInfo: false, opReturnInfo: { app: 'SWaP', msg: '', stackArray: [ '53575000', '01', '01', 'aebcae9afe88d61d8b8ed7b8c83c7c2a555583bf8f8591c94a2c9eb82f34816c', '53454c4c', '3135393838333534', '00', '3dd9beaedcbb3ad90eec2214fcf71381fa89b08b899813e182a7a393e1ab0619', '01', '00', '333236322e39393939393939393939393935', ], tokenId: 'aebcae9afe88d61d8b8ed7b8c83c7c2a555583bf8f8591c94a2c9eb82f34816c', }, txFee: 466, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a914aed3f8a5add35a9ddaf0a07986c2b73a2202727d88ac', '76a914d50ec518d64850fda86e926764ce1bce1ba01a1988ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '6a04535750000101010120aebcae9afe88d61d8b8ed7b8c83c7c2a555583bf8f8591c94a2c9eb82f34816c0453454c4c0831353938383335340100203dd9beaedcbb3ad90eec2214fcf71381fa89b08b899813e182a7a393e1ab06190101010012333236322e39393939393939393939393935', 0, ], ], }, totalSatsSent: 0, tokenSendInfo: false, tokenBurnInfo: false, }, { txid: 'a8c348539a1470b28b9f99693994b918b475634352994dddce80ad544e871b3a', genesisInfo: false, opReturnInfo: { app: 'memo', msg: 'Reply to memo|<a href="https://explorer.e.cash/tx/eae5710aba50a0a22b266ddbb445e05b7348d15c88cbc2e012a91a09bec3861a">memo</a>|Twitter keeps turning their API on and off. Sometimes it works, sometimes it doesn\'t. Feature to create tweets from memo may work again at some point.', }, txFee: 390, xecSendingOutputScripts: { dataType: 'Set', value: [ '76a9148fa951904f6d0ebbc92dc29e761b9eb0a837545c88ac', ], }, xecReceivingOutputs: { dataType: 'Map', value: [ [ '6a026d0320eae5710aba50a0a22b266ddbb445e05b7348d15c88cbc2e012a91a09bec3861a4c9654776974746572206b65657073207475726e696e6720746865697220415049206f6e20616e64206f66662e20536f6d6574696d657320697420776f726b732c20736f6d6574696d657320697420646f65736e27742e204665617475726520746f20637265617465207477656574732066726f6d206d656d6f206d617920776f726b20616761696e20617420736f6d6520706f696e742e', 0, ], ], }, totalSatsSent: 0, tokenSendInfo: false, tokenBurnInfo: false, }, ], tokenIds: { dataType: 'Set', value: [ 'b76878b29eff39c8c28aaed7d18a166c20057c43beeb90b630264470983c984a', 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', '98183238638ecb4ddc365056e22de0e8a05448c1e6084bae247fae5a74ad4f48', '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', 'fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa', '010114b9bbe776def1a512ad1e96a4a06ec4c34fc79bcb5d908845f5102f6b0f', '7e7dacd72dcdb14e00a03dd3aff47f019ed51a6f1f4e4f532ae50692f62bc4e5', 'aebcae9afe88d61d8b8ed7b8c83c7c2a555583bf8f8591c94a2c9eb82f34816c', ], }, outputScripts: { dataType: 'Set', value: [ '76a914231f7087937684790d1049294f3aef9cfb7b05dd88ac', '76a9147c09e7cf1b2c40d4e08057e47bda9cabcdfd208588ac', '76a914a520c86a08366941cd90d22e11ac1c7eefa2db3788ac', '76a91412934a7a99b69a60c3b99f991cd79d257104f5a688ac', '76a914d7fc1d156d8ec4384623bb8ceb135df93f2bd93188ac', '76a9142cc3608fe629c4f402e511878982bc01bde3445d88ac', 'a9145aafaadba9ff909067a640e5e2a46b756aeaf71387', '76a914d95a60cea21479569e6b1ad39416c8fbc97323c588ac', '76a914453c8c15aee05fe5a027d4bf5681cd0bc682c0b788ac', '76a914f93029e7593327c5b864ea6896ecfda4fffb6ab888ac', '76a914957b59a2bfa17ea7fc234e532b263169b6d34aa988ac', '76a9147d432e8ccc646fe6c52e36c285bce2b75f0f500b88ac', '76a9144fb74b44c66ab529428a943131f236c80d99b82088ac', '76a914821407ac2993f8684227004f4086082f3f801da788ac', '76a91430f16ad77116a4b9e7f337743e35271323d63e0d88ac', '76a914eeae0fea781c26c93879523ba5a47c244c768ece88ac', '76a9147276ae7693883fa1165628e298899d8ee9248e7c88ac', '76a914dee50f576362377dd2f031453c0bb09009acaf8188ac', '76a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac', '76a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac', 'a914d37c4c809fe9840e7bfa77b86bd47163f6fb6c6087', '76a914dcc535261a43835ca12352d0926ba06cf07cbe8388ac', '76a91469724b96df46096cc95b1a6d408a4240ea80d85588ac', '76a91458cddba2449285814dae43d4ed4a1c9998f3693e88ac', '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', '76a9141c13ddb8dd422bbe02dc2ae8798b4549a67a3c1d88ac', '76a914dadf34cde9c774fdd6340cd2916a9b9c5d57cf4388ac', 'a914ea826cc1a3a981d048cd78b66711222bece8ebf287', '76a91472a92e48c5ab72566959db1dbf1b8dce83afabb788ac', '76a914378c3b416e77e01198c01ad215b8afd0bb72799488ac', '76a914acdbf937b086ddaa970072a610daa8d10f14549a88ac', '76a914104e67d912a7aab2a159bba141477e5867c04bfd88ac', '76a9146d69b5cbe7c85d87628473c43620c0daa9a8102988ac', '76a9144bb6f659b8dafd99527e0c0a3289f121b0a0209f88ac', '76a914aed3f8a5add35a9ddaf0a07986c2b73a2202727d88ac', '76a9148fa951904f6d0ebbc92dc29e761b9eb0a837545c88ac', ], }, }, coingeckoResponse: { bitcoin: { usd: 30000, }, ecash: { usd: 0.0001, }, ethereum: { usd: 2000, }, }, coingeckoPrices: [ { fiat: 'usd', price: 0.0001, ticker: 'XEC', }, { fiat: 'usd', price: 30000, ticker: 'BTC', }, { fiat: 'usd', price: 2000, ticker: 'ETH', }, ], tokenInfoMap: { dataType: 'Map', value: [ [ 'b76878b29eff39c8c28aaed7d18a166c20057c43beeb90b630264470983c984a', { tokenTicker: 'eAfrica', tokenName: 'eAfrica Token', url: 'https://chat.whatsapp.com/BJDUldMxnNm23KAFRE4diq', decimals: 2, hash: '', }, ], [ 'cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145', { tokenTicker: 'CRD', tokenName: 'Credo In Unum Deo', url: 'https://crd.network/token', decimals: 4, data: {}, authPubkey: '0334b744e6338ad438c92900c0ed1869c3fd2c0f35a4a9b97a88447b6e2b145f10', }, ], [ 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', { tokenTicker: 'CACHET', tokenName: 'Cachet', url: 'https://cashtab.com/', decimals: 2, hash: '', }, ], [ '98183238638ecb4ddc365056e22de0e8a05448c1e6084bae247fae5a74ad4f48', { tokenTicker: 'DVV', tokenName: 'Delta Variant Variants', url: 'https://cashtabapp.com/', decimals: 0, hash: '', }, ], [ '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', { tokenTicker: 'WDT', tokenName: 'Test Token With Exceptionally Long Name For CSS And Style Revisions', url: 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', decimals: 7, hash: '85b591c15c9f49531e39fcfeb2a5a26b2bd0f7c018fb9cd71b5d92dfb732d5cc', }, ], [ 'fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa', { tokenTicker: 'GRP', tokenName: 'GRUMPY', url: 'https://bit.ly/GrumpyDoc', decimals: 2, hash: '', }, ], [ '010114b9bbe776def1a512ad1e96a4a06ec4c34fc79bcb5d908845f5102f6b0f', { tokenTicker: 'Lolly', tokenName: 'LOLLY', url: 'https://cashtab.com/', decimals: 8, hash: '', }, ], [ '7e7dacd72dcdb14e00a03dd3aff47f019ed51a6f1f4e4f532ae50692f62bc4e5', { tokenTicker: 'BUX', tokenName: 'Badger Universal Token', url: 'https://bux.digital', decimals: 4, hash: '', }, ], [ 'aebcae9afe88d61d8b8ed7b8c83c7c2a555583bf8f8591c94a2c9eb82f34816c', { tokenTicker: 'GORB', tokenName: 'Gorbeious', url: 'gorbeious.cash', decimals: 0, hash: '', }, ], ], }, outputScriptInfoMap: { dataType: 'Map', value: [ [ '76a914231f7087937684790d1049294f3aef9cfb7b05dd88ac', { emoji: 'π³', balanceSats: '01000000000000', utxos: [ { value: '1000000000000', }, ], }, ], [ '76a9147c09e7cf1b2c40d4e08057e47bda9cabcdfd208588ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a914a520c86a08366941cd90d22e11ac1c7eefa2db3788ac', { emoji: 'π¦', balanceSats: '010000000000', utxos: [ { value: '10000000000', }, ], }, ], [ '76a91412934a7a99b69a60c3b99f991cd79d257104f5a688ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a914d7fc1d156d8ec4384623bb8ceb135df93f2bd93188ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a9142cc3608fe629c4f402e511878982bc01bde3445d88ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ 'a9145aafaadba9ff909067a640e5e2a46b756aeaf71387', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a914d95a60cea21479569e6b1ad39416c8fbc97323c588ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a914453c8c15aee05fe5a027d4bf5681cd0bc682c0b788ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a914f93029e7593327c5b864ea6896ecfda4fffb6ab888ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a914957b59a2bfa17ea7fc234e532b263169b6d34aa988ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a9147d432e8ccc646fe6c52e36c285bce2b75f0f500b88ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a9144fb74b44c66ab529428a943131f236c80d99b82088ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a914821407ac2993f8684227004f4086082f3f801da788ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a91430f16ad77116a4b9e7f337743e35271323d63e0d88ac', { emoji: '', balanceSats: 10000, utxos: [ { value: 10000, }, ], }, ], [ '76a914eeae0fea781c26c93879523ba5a47c244c768ece88ac', { emoji: '', balanceSats: 10000, utxos: [ { value: 10000, }, ], }, ], [ '76a9147276ae7693883fa1165628e298899d8ee9248e7c88ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a914dee50f576362377dd2f031453c0bb09009acaf8188ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ 'a914d37c4c809fe9840e7bfa77b86bd47163f6fb6c6087', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a914dcc535261a43835ca12352d0926ba06cf07cbe8388ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a91469724b96df46096cc95b1a6d408a4240ea80d85588ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a91458cddba2449285814dae43d4ed4a1c9998f3693e88ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a9141c13ddb8dd422bbe02dc2ae8798b4549a67a3c1d88ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a914dadf34cde9c774fdd6340cd2916a9b9c5d57cf4388ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ 'a914ea826cc1a3a981d048cd78b66711222bece8ebf287', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a91472a92e48c5ab72566959db1dbf1b8dce83afabb788ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a914378c3b416e77e01198c01ad215b8afd0bb72799488ac', { emoji: '', balanceSats: 10000, utxos: [ { value: 10000, }, ], }, ], [ '76a914acdbf937b086ddaa970072a610daa8d10f14549a88ac', { emoji: '', balanceSats: 10000, utxos: [ { value: 10000, }, ], }, ], [ '76a914104e67d912a7aab2a159bba141477e5867c04bfd88ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a9146d69b5cbe7c85d87628473c43620c0daa9a8102988ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a9144bb6f659b8dafd99527e0c0a3289f121b0a0209f88ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a914aed3f8a5add35a9ddaf0a07986c2b73a2202727d88ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], [ '76a9148fa951904f6d0ebbc92dc29e761b9eb0a837545c88ac', { emoji: '', balanceSats: '010000', utxos: [ { value: '10000', }, ], }, ], ], }, blockSummaryTgMsgs: [ 'π¦<a href="https://explorer.e.cash/block/0000000000000000000000000000000000000000000000000000000000000000">819346</a> | 27 txs | unknown, ...863u\nβ° 20,654 blocks until eCash halving\nπ°$63 to <a href="https://explorer.e.cash/address/ecash:qrpkjsd0fjxd7m332mmlu9px6pwkzaufpcn2u7jcwt">qrp...cwt</a>\n1 XEC = $0.0001\n1 BTC = $30,000\n1 ETH = $2,000\n\n<b>1 new eToken created</b>\nπ§ͺ<a href="https://explorer.e.cash/tx/010114b9bbe776def1a512ad1e96a4a06ec4c34fc79bcb5d908845f5102f6b0f">LOLLY</a> (Lolly) <a href="https://cashtab.com/">[doc]</a>\n\n<a href="https://cashtab.com/">Cashtab</a>\n<b>3</b> <a href="https://explorer.e.cash/tx/aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1">CACHET</a> rewards\n<b>2</b> new users received <b>84 XEC</b>\n\n2 txs sent 10.2000 <a href="https://explorer.e.cash/tx/cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145">Credo In Unum Deo (CRD)</a>\n1 tx sent 100.00 <a href="https://explorer.e.cash/tx/aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1">Cachet (CACHET)</a>\n1 tx sent 55 <a href="https://explorer.e.cash/tx/98183238638ecb4ddc365056e22de0e8a05448c1e6084bae247fae5a74ad4f48">Delta Variant Variants (DVV)</a>\n1 tx sent 10.9876543 <a href="https://explorer.e.cash/tx/7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d">Test Token With Exceptionally Long Name For CSS And Style Revisions (WDT)</a>\n1 tx sent 5,000,000.00 <a href="https://explorer.e.cash/tx/fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa">GRUMPY (GRP)</a>\n1 tx sent 356.6918 <a href="https://explorer.e.cash/tx/7e7dacd72dcdb14e00a03dd3aff47f019ed51a6f1f4e4f532ae50692f62bc4e5">Badger Universal Token (BUX)</a>\n\n<b>1 eToken burn tx</b>\nπ₯qp9...et0 <a href="https://explorer.e.cash/tx/6b139007a0649f99a1a099c7c924716ee1920f74ea83111f6426854d4c3c3c79">burned</a> 1.00 <a href="https://explorer.e.cash/tx/fb4233e8a568993976ed38a81c2671587c5ad09552dedefa78760deed6ff87aa">GRP</a> \n\n<b>9 app txs</b>\nβοΈ<a href="https://explorer.e.cash/tx/d5be7a4b483f9fdbbe3bf46cfafdd0100d5dbeee0b972f4dabc8ae9d9962fa55">CashFusion:</a> Fused $1k from 64 inputs into 63 outputs\nβ<a href="https://explorer.e.cash/tx/b5782d3a3b55e5ee9e4330a969c2891042ae05fafab7dc05cd14da63e7242f8e">unknown:</a> 0x663ddd99990bcd9699...\nβ<a href="https://explorer.e.cash/tx/9094e1aab7ac73c680bf66e78cc8311831b3d813e608bff1e07b1854855fc0f1">unknown:</a> =:ETH.ETH:0xa9aaF30F65955C69c16B3345B51D426D9B88Ba87:841321:tr:0\nπͺ<a href="https://explorer.e.cash/tx/7a0d6ae3384e293183478f681f51a77ef4c71f29957199364bb9ba4d8e1938be">Airdrop:</a> qru...jys airdropped $5 to 13 holders of <a href="https://explorer.e.cash/tx/b76878b29eff39c8c28aaed7d18a166c20057c43beeb90b630264470983c984a">eAfrica</a>|Stay with us, eCash Africa is the next big community in the African cryptosphere. \nπ<a href="https://explorer.e.cash/tx/d02d94a1a520877c60d1e3026c3e85f8995d48d7b90140f83e24ede592c30306">Cashtab Msg, $1 for $0.0005:</a> I like eCash\nπ<a href="https://explorer.e.cash/tx/1083da7ead4779fbab5c5e8291bb7a37abaf4f97f5ff99ee654759b2eaee445b">Cashtab Encrypted:</a> qq9...fgx sent an encrypted message and $0.002 to qzv...fed\nπΎ<a href="https://explorer.e.cash/tx/22135bb69435023a84c80b1b93b31fc8898c3507eaa70569ed038f32d59599a9">Alias (beta):</a> doge2\nπ€³<a href="https://explorer.e.cash/tx/ad44bf5e214ab71bb60a2eee165f368c139cd49c2380c3352f0a4fffc746b36a">SWaP:</a> Signal|SLP Atomic Swap|<a href="https://explorer.e.cash/tx/aebcae9afe88d61d8b8ed7b8c83c7c2a555583bf8f8591c94a2c9eb82f34816c">GORB</a>|SELL for 159,883.54 XEC|Min trade: 0 XEC\nπ<a href="https://explorer.e.cash/tx/a8c348539a1470b28b9f99693994b918b475634352994dddce80ad544e871b3a">memo:</a> Reply to memo|<a href="https://explorer.e.cash/tx/eae5710aba50a0a22b266ddbb445e05b7348d15c88cbc2e012a91a09bec3861a">memo</a>|Twitter keeps turning their API on and off. Sometimes it works, sometimes it doesn\'t. Feature to create tweets from memo may work again at some point.\n\n<b>3 eCash txs</b>', 'πΈ<a href="https://explorer.e.cash/tx/4f33c81d95641eb0f80e793dc96c58a2438f9bb1f18750d8fb3b56c28cd25035">$584k for $0.0003</a> π³ Binance β‘οΈ itself\nπΈ<a href="https://explorer.e.cash/tx/f5d4c112cfd22701226ba050cacfacc3aff570964c6196f67e326fc3224300a2">$107k for $0.003</a> qp7...sr4 β‘οΈ π¦qzj...ksg\nπΈ<a href="https://explorer.e.cash/tx/413b57617d2c497b137d31c53151fee595415ec273ef7a111160da8093147ed8">$0.0005 for $0.0005</a>', ], blockSummaryTgMsgsApiFailure: [ 'π¦<a href="https://explorer.e.cash/block/0000000000000000000000000000000000000000000000000000000000000000">819346</a> | 27 txs | unknown, ...863u\nβ° 20,654 blocks until eCash halving\nπ°625k XEC to <a href="https://explorer.e.cash/address/ecash:qrpkjsd0fjxd7m332mmlu9px6pwkzaufpcn2u7jcwt">qrp...cwt</a>\n\n<b>9 app txs</b>\nβοΈ<a href="https://explorer.e.cash/tx/d5be7a4b483f9fdbbe3bf46cfafdd0100d5dbeee0b972f4dabc8ae9d9962fa55">CashFusion:</a> Fused 13M XEC from 64 inputs into 63 outputs\nβ<a href="https://explorer.e.cash/tx/b5782d3a3b55e5ee9e4330a969c2891042ae05fafab7dc05cd14da63e7242f8e">unknown:</a> 0x663ddd99990bcd9699...\nβ<a href="https://explorer.e.cash/tx/9094e1aab7ac73c680bf66e78cc8311831b3d813e608bff1e07b1854855fc0f1">unknown:</a> =:ETH.ETH:0xa9aaF30F65955C69c16B3345B51D426D9B88Ba87:841321:tr:0\nπͺ<a href="https://explorer.e.cash/tx/7a0d6ae3384e293183478f681f51a77ef4c71f29957199364bb9ba4d8e1938be">Airdrop:</a> qru...jys airdropped 45k XEC to 13 holders of <a href="https://explorer.e.cash/tx/b76878b29eff39c8c28aaed7d18a166c20057c43beeb90b630264470983c984a">b76...84a</a>|Stay with us, eCash Africa is the next big community in the African cryptosphere. \nπ<a href="https://explorer.e.cash/tx/d02d94a1a520877c60d1e3026c3e85f8995d48d7b90140f83e24ede592c30306">Cashtab Msg, 10k XEC for 4.79 XEC:</a> I like eCash\nπ<a href="https://explorer.e.cash/tx/1083da7ead4779fbab5c5e8291bb7a37abaf4f97f5ff99ee654759b2eaee445b">Cashtab Encrypted:</a> qq9...fgx sent an encrypted message and 20 XEC to qzv...fed\nπΎ<a href="https://explorer.e.cash/tx/22135bb69435023a84c80b1b93b31fc8898c3507eaa70569ed038f32d59599a9">Alias (beta):</a> doge2\nπ€³<a href="https://explorer.e.cash/tx/ad44bf5e214ab71bb60a2eee165f368c139cd49c2380c3352f0a4fffc746b36a">SWaP:</a> Signal|SLP Atomic Swap|<a href="https://explorer.e.cash/tx/aebcae9afe88d61d8b8ed7b8c83c7c2a555583bf8f8591c94a2c9eb82f34816c">Unknown Token</a>|SELL for 159,883.54 XEC|Min trade: 0 XEC\nπ<a href="https://explorer.e.cash/tx/a8c348539a1470b28b9f99693994b918b475634352994dddce80ad544e871b3a">memo:</a> Reply to memo|<a href="https://explorer.e.cash/tx/eae5710aba50a0a22b266ddbb445e05b7348d15c88cbc2e012a91a09bec3861a">memo</a>|Twitter keeps turning their API on and off. Sometimes it works, sometimes it doesn\'t. Feature to create tweets from memo may work again at some point.\n\n<b>17 eCash txs</b>\nπΈ<a href="https://explorer.e.cash/tx/4f33c81d95641eb0f80e793dc96c58a2438f9bb1f18750d8fb3b56c28cd25035">6B XEC for 2.6 XEC</a>\nπΈ<a href="https://explorer.e.cash/tx/f5d4c112cfd22701226ba050cacfacc3aff570964c6196f67e326fc3224300a2">1B XEC for 29 XEC</a>\nπΈ<a href="https://explorer.e.cash/tx/d8fe456c89357c23ac6d240fe9319ce9ba393c9c3833631046a265ca7c8349e6">42 XEC for 2.19 XEC</a>\nπΈ<a href="https://explorer.e.cash/tx/083b7862bae48e78549ccf63833896f5f4f5bdef5c380a108fa99cdb64261fa3">42 XEC for 2.19 XEC</a>\nπΈ<a href="https://explorer.e.cash/tx/45ec66bc2440d2f94fa2c645e20a44f6fab7c397053ce77a95484c6053104cdc">31 XEC for 24 XEC</a>\nπΈ<a href="https://explorer.e.cash/tx/004e018dd98520aa722ee76c608771dd578a044f38103a8298f25e6ffbc7c3ba">5.46 XEC for 4.81 XEC</a>\nπΈ<a href="https://explorer.e.cash/tx/0110cd886ecd2d9570e98b7501cd039f4e5352d69659a46f1a49cc19c1869701">5.46 XEC for 4.81 XEC</a>\nπΈ<a href="https://explorer.e.cash/tx/327101f6f3b740280a6e9fbd8edc41f4f0500633672975a5974a4147c94016a5">5.46 XEC for 4.81 XEC</a>\nπΈ<a href="https://explorer.e.cash/tx/aa13c6f214ff58f36ed5e108a7f36d8f98729c50186b27a53b989c7f36fbf517">5.46 XEC for 4.81 XEC</a>\nπΈ<a href="https://explorer.e.cash/tx/6ffcc83e76226bd32821cc6862ce9b363b22594247a4e73ccf3701b0023592b2">5.46 XEC for 11 XEC</a>\nπΈ<a href="https://explorer.e.cash/tx/fb70df00c07749082756054522d3f08691fd9caccd0e0abf736df23d22845a6e">5.46 XEC for 11 XEC</a>\nπΈ<a href="https://explorer.e.cash/tx/25345b0bf921a2a9080c647768ba440bbe84499f4c7773fba8a1b03e88ae7fe7">5.46 XEC for 11 XEC</a>\n...and <a href="https://explorer.e.cash/block/0000000000000000000000000000000000000000000000000000000000000000">5 more</a>', ], }; export default mockedBlock; diff --git a/apps/ecash-herald/test/mocks/telegramBotMock.ts b/apps/ecash-herald/test/mocks/telegramBotMock.ts index 07c0fc831..65e793966 100644 --- a/apps/ecash-herald/test/mocks/telegramBotMock.ts +++ b/apps/ecash-herald/test/mocks/telegramBotMock.ts @@ -1,47 +1,47 @@ // Copyright (c) 2023 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. /* Mock node-telegram-bot-api TelegramBot instance * Supports sendMessage function */ import { SendMessageOptions } from 'node-telegram-bot-api'; export const mockChannelId = '-1001999999999'; interface SendMessageResponse { success: boolean; channelId: string; msg: string; options: SendMessageOptions; } -export interface MockTelegramBot { +export interface MockTelegramBotInterface { messageSent: boolean; errors: { [key: string]: string | undefined }; sendMessage: ( channelId: string, msg: string, options: SendMessageOptions, ) => SendMessageResponse; setExpectedError: (method: string, error: string) => void; } -export class MockTelegramBot implements MockTelegramBot { - constructor() { - // Use self since it is not a reserved term in js - // Can access self from inside a method and still get the class - const self = this; - self.messageSent = false; - self.errors = {}; - self.sendMessage = function (channelId, msg, options) { - if (!self.errors.sendMessage) { - self.messageSent = true; - return { success: true, channelId, msg, options }; - } - throw new Error(self.errors.sendMessage); - }; - self.setExpectedError = function (method, error) { - self.errors[method] = error; - }; +export class MockTelegramBot implements MockTelegramBotInterface { + public messageSent = false; + public errors: { [key: string]: string | undefined } = {}; + public sendMessage( + channelId: string, + msg: string, + options: SendMessageOptions, + ): SendMessageResponse { + if (!this.errors.sendMessage) { + this.messageSent = true; + return { success: true, channelId, msg, options }; + } + throw new Error(this.errors.sendMessage || 'Unknown error'); + } + + public setExpectedError(method: string, error: string): void { + this.errors[method] = error; } } diff --git a/apps/ecash-herald/test/mocks/templates.ts b/apps/ecash-herald/test/mocks/templates.ts index 42c8fc315..8ea0f6874 100644 --- a/apps/ecash-herald/test/mocks/templates.ts +++ b/apps/ecash-herald/test/mocks/templates.ts @@ -1,101 +1,101 @@ // 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. import { FiatCode } from '../../config'; import { CoinGeckoPrice } from '../../src/utils'; interface TgStringFixture { dangerous: string; safe: string; noChangeExpected: string; } interface Templates { telegramHtmlStrings: TgStringFixture; addressPreviews: { address: string; preview: string; sliceSize: number }[]; mockCoingeckoPrices: CoinGeckoPrice[]; } -const templates = { +const templates: Templates = { telegramHtmlStrings: { dangerous: '<b>Try to hack the format</b> ${true && <i>yes</i>}', safe: '<b>Try to hack the format</b> ${true && <i>yes</i>}', noChangeExpected: 'Just a normal sentence with punctuation and things, but none of the forbidden characters.', }, addressPreviews: [ { address: 'ecash:qqf76scx4s8yayz80n6r3wcvuqdnypw5dvt38lr8up', preview: 'qqf...8up', sliceSize: 3, }, { address: 'ecash:qpp66yg3dsp0fx3w8gl9zw6nwkwf587pqcduy5jp3z', preview: 'qpp...p3z', sliceSize: 3, }, { address: 'ecash:qq7uq470gu0afsffkveesckges366wrcrssvngy7gu', preview: 'qq7...7gu', sliceSize: 3, }, { address: 'ecash:qqf76scx4s8yayz80n6r3wcvuqdnypw5dvt38lr8up', preview: 'qqf76s...8lr8up', sliceSize: 6, }, { address: 'ecash:qpp66yg3dsp0fx3w8gl9zw6nwkwf587pqcduy5jp3z', preview: 'qpp66y...y5jp3z', sliceSize: 6, }, { address: 'ecash:qq7uq470gu0afsffkveesckges366wrcrssvngy7gu', preview: 'qq7uq4...ngy7gu', sliceSize: 6, }, { address: 'ecash:qq7uq470gu0afsffkveesckges366wrcrssvngy7gu', preview: 'qq...gu', sliceSize: 2, }, { address: 'ecash:qqf76scx4s8yayz80n6r3wcvuqdnypw5dvt38lr8up', preview: 'qqf76...lr8up', sliceSize: 5, }, { address: 'ecash:qq337uy8jdmgg7gdzpyjjne6a7w0k7c9m5m5gnpx4u', preview: 'Binance', sliceSize: 3, }, { address: 'ecash:qq580luw0dkypdlrply9ulk3rht6nrqfugvgm9le8a', preview: 'Coinex 1', sliceSize: 3, }, { address: 'ecash:qqv2vqz6he83x9pczvt552fuxnvhevlt6ugrqqa7w5', preview: 'Coinex 2', sliceSize: 3, }, ], mockCoingeckoPrices: [ { fiat: 'usd' as FiatCode, price: 0.00003, ticker: 'XEC', }, { fiat: 'usd' as FiatCode, price: 28044.64857505, ticker: 'BTC', }, { fiat: 'usd' as FiatCode, price: 1900.73166438, ticker: 'ETH', }, ], }; export default templates; diff --git a/apps/ecash-herald/test/parse.test.ts b/apps/ecash-herald/test/parse.test.ts index 5f5b4cec7..c768f7bd3 100644 --- a/apps/ecash-herald/test/parse.test.ts +++ b/apps/ecash-herald/test/parse.test.ts @@ -1,588 +1,588 @@ // 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. import assert from 'assert'; import opReturn from '../constants/op_return'; import unrevivedBlock from './mocks/block'; import minersJson, { KnownMiners } from '../constants/miners'; import minerTestFixtures from './fixtures/miners'; import stakerTestFixtures from './fixtures/stakers'; import invalidatedBlocksTestFixtures from './fixtures/invalidatedBlocks'; import { jsonReviver } from '../src/utils'; import memoFixtures from './mocks/memo'; import { consumeNextPush } from 'ecash-script'; import { MockChronikClient } from '../../../modules/mock-chronik-client'; import { TxOutput } from 'chronik-client'; import { caching } from 'cache-manager'; import { StoredMock } from '../src/events'; import { parseBlockTxs, getStakerFromCoinbaseTx, getMinerFromCoinbaseTx, parseMemoOutputScript, getBlockTgMessage, parseOpReturn, getSwapTgMsg, getAirdropTgMsg, getEncryptedCashtabMsg, parseMultipushStack, parseSlpTwo, guessRejectReason, summarizeTxHistory, } from '../src/parse'; import appTxSamples from './mocks/appTxSamples'; import { dailyTxs, tokenInfoMap } from './mocks/dailyTxs'; const { swaps, airdrops, encryptedCashtabMsgs, slp2PushVectors, slp2TxVectors, aliasRegistrations, cashtabMsgs, payButtonTxs, paywallTxs, authenticationTxs, } = appTxSamples; const block: StoredMock = JSON.parse( JSON.stringify(unrevivedBlock), jsonReviver, ); const miners: KnownMiners = JSON.parse(JSON.stringify(minersJson), jsonReviver); 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 () { memoFixtures.map(memoTestObj => { const { outputScript, msg } = memoTestObj; // Get array of pushes - let stack = { remainingHex: outputScript.slice(2) }; - let stackArray = []; + const stack = { remainingHex: outputScript.slice(2) }; + const 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 as TxOutput[], ), 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 }, ] as TxOutput[]; 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, tokenInfoMap, { usd: 0.00003487, usd_market_cap: 689047177.8128564, usd_24h_vol: 5957332.9687223025, usd_24h_change: -0.3973642442197056, }, ), [ '<b>15 Oct 2024</b>\n' + 'π¦57,430 blocks\n' + 'β‘οΈ30 txs\n' + '\n' + 'π<b>1 XEC = $0.00003487</b> <i>(-0.40%)</i>\n' + 'Trading volume: $5,957,333\n' + 'Market cap: $689,047,178\n' + '\n' + '<b><i>βοΈ3 miners found blocks</i></b>\n' + '<u>Top 3</u>\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 $33</i></b>\n' + '<u>Top 3</u>\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><i>3 Agora token txs from 3 tokens</i></b>\n' + 'π°Buy, π·List, βCancel\n' + '<a href="https://explorer.e.cash/tx/aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1">Cachet</a> (CACHET): π°\n' + '<a href="https://explorer.e.cash/tx/20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8">Vespene Gas</a> (VSP): β\n' + '<a href="https://explorer.e.cash/tx/01d63c4f4cb496829a6743f7b1805d086ea3877a1dd34b3f92ffba2c9c99f896">Bull</a> (BULL): π·\n' + '\nππΌ <b><i>3 Agora NFT txs from 2 NFTs in 2 collections</i></b>\n' + 'π°Buy, π·List, βCancel\n' + '<a href="https://explorer.e.cash/tx/78efa5177e99bf05b48948ac7e23e6cc2255764e52ccf7092afb979a766dee2c">xolosArmyPOP</a> (RMZPOP): π°\n' + '<a href="https://explorer.e.cash/tx/0fb781a98fffb980b1c9c609f62b29783c348e74aa7ea3908dcf7f46388ab316">Flags</a> (FLAGS): π·β\n' + '\n' + 'πͺ <b><i>8 token txs from 2 tokens</i></b>\n' + '<a href="https://explorer.e.cash/tx/04009a8be347f21a1122964c3226b99c36a9bd755c5a450a53848471a2466103">Perpetua</a> (PRP): π§ͺβ‘οΈπ₯π¨\n' + '<a href="https://explorer.e.cash/tx/20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8">Vespene Gas</a> (VSP): β‘οΈ\n' + '\n' + 'πΌ <b><i>2 NFT txs from 2 NFTs in 2 collections</i></b>\n' + '<a href="https://explorer.e.cash/tx/8fd3f14abd2b176a1d4bd5136542cd2a7ba3df0e11947dd19326c9d1cd81ae09">Xoloitzcuintli NFT Cigar Collection.</a> (RMZsmoke): π§ͺ\n' + '<a href="https://explorer.e.cash/tx/78efa5177e99bf05b48948ac7e23e6cc2255764e52ccf7092afb979a766dee2c">xolosArmyPOP</a> (RMZPOP): β‘οΈ\n' + '\n' + 'πΌ <b><i>1 NFT mint</i></b>\n' + 'π <b><i>1 new fixed-supply token</i></b>\n' + 'π¨ <b><i>1 new variable-supply token</i></b>\n' + 'π» <b><i>1 ALP tx</i></b>\n' + '𧩠<b><i>1 Mint Vault tx</i></b>\n' + 'β <b><i>1 invalid token tx</i></b>\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 including fiat prices with no token cache info', function () { const mockUtcNewDayTimestampSeconds = 1728950400; assert.deepEqual( summarizeTxHistory( mockUtcNewDayTimestampSeconds, dailyTxs, // we can't get any token cache info new Map(), { usd: 0.00003487, usd_market_cap: 689047177.8128564, usd_24h_vol: 5957332.9687223025, usd_24h_change: -0.3973642442197056, }, ), [ '<b>15 Oct 2024</b>\n' + 'π¦57,430 blocks\n' + 'β‘οΈ30 txs\n' + '\n' + 'π<b>1 XEC = $0.00003487</b> <i>(-0.40%)</i>\n' + 'Trading volume: $5,957,333\n' + 'Market cap: $689,047,178\n' + '\n' + '<b><i>βοΈ3 miners found blocks</i></b>\n' + '<u>Top 3</u>\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 $33</i></b>\n' + '<u>Top 3</u>\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><i>3 Agora token txs from 3 tokens</i></b>\n' + 'π°Buy, π·List, βCancel\n' + '<a href="https://explorer.e.cash/tx/aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1">aed...cb1</a>: π°\n' + '<a href="https://explorer.e.cash/tx/20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8">20a...6f8</a>: β\n' + '<a href="https://explorer.e.cash/tx/01d63c4f4cb496829a6743f7b1805d086ea3877a1dd34b3f92ffba2c9c99f896">01d...896</a>: π·\n' + '\nππΌ <b><i>3 Agora NFT txs from 2 NFTs in 2 collections</i></b>\n' + 'π°Buy, π·List, βCancel\n' + '<a href="https://explorer.e.cash/tx/78efa5177e99bf05b48948ac7e23e6cc2255764e52ccf7092afb979a766dee2c">78e...e2c</a>: π°\n' + '<a href="https://explorer.e.cash/tx/0fb781a98fffb980b1c9c609f62b29783c348e74aa7ea3908dcf7f46388ab316">0fb...316</a>: π·β\n' + '\n' + 'πͺ <b><i>8 token txs from 2 tokens</i></b>\n' + '<a href="https://explorer.e.cash/tx/04009a8be347f21a1122964c3226b99c36a9bd755c5a450a53848471a2466103">040...103</a>: π§ͺβ‘οΈπ₯π¨\n' + '<a href="https://explorer.e.cash/tx/20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8">20a...6f8</a>: β‘οΈ\n' + '\n' + 'πΌ <b><i>2 NFT txs from 2 NFTs in 2 collections</i></b>\n' + '<a href="https://explorer.e.cash/tx/8fd3f14abd2b176a1d4bd5136542cd2a7ba3df0e11947dd19326c9d1cd81ae09">8fd...e09</a>: π§ͺ\n' + '<a href="https://explorer.e.cash/tx/78efa5177e99bf05b48948ac7e23e6cc2255764e52ccf7092afb979a766dee2c">78e...e2c</a>: β‘οΈ\n' + '\n' + 'πΌ <b><i>1 NFT mint</i></b>\n' + 'π <b><i>1 new fixed-supply token</i></b>\n' + 'π¨ <b><i>1 new variable-supply token</i></b>\n' + 'π» <b><i>1 ALP tx</i></b>\n' + '𧩠<b><i>1 Mint Vault tx</i></b>\n' + 'β <b><i>1 invalid token tx</i></b>\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, tokenInfoMap, ), [ '<b>15 Oct 2024</b>\n' + 'π¦57,430 blocks\n' + 'β‘οΈ30 txs\n' + '\n' + '<b><i>βοΈ3 miners found blocks</i></b>\n' + '<u>Top 3</u>\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>(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><i>3 Agora token txs from 3 tokens</i></b>\n' + 'π°Buy, π·List, βCancel\n' + '<a href="https://explorer.e.cash/tx/aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1">Cachet</a> (CACHET): π°\n' + '<a href="https://explorer.e.cash/tx/20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8">Vespene Gas</a> (VSP): β\n' + '<a href="https://explorer.e.cash/tx/01d63c4f4cb496829a6743f7b1805d086ea3877a1dd34b3f92ffba2c9c99f896">Bull</a> (BULL): π·\n' + '\nππΌ <b><i>3 Agora NFT txs from 2 NFTs in 2 collections</i></b>\n' + 'π°Buy, π·List, βCancel\n' + '<a href="https://explorer.e.cash/tx/78efa5177e99bf05b48948ac7e23e6cc2255764e52ccf7092afb979a766dee2c">xolosArmyPOP</a> (RMZPOP): π°\n' + '<a href="https://explorer.e.cash/tx/0fb781a98fffb980b1c9c609f62b29783c348e74aa7ea3908dcf7f46388ab316">Flags</a> (FLAGS): π·β\n' + '\n' + 'πͺ <b><i>8 token txs from 2 tokens</i></b>\n' + '<a href="https://explorer.e.cash/tx/04009a8be347f21a1122964c3226b99c36a9bd755c5a450a53848471a2466103">Perpetua</a> (PRP): π§ͺβ‘οΈπ₯π¨\n' + '<a href="https://explorer.e.cash/tx/20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8">Vespene Gas</a> (VSP): β‘οΈ\n' + '\n' + 'πΌ <b><i>2 NFT txs from 2 NFTs in 2 collections</i></b>\n' + '<a href="https://explorer.e.cash/tx/8fd3f14abd2b176a1d4bd5136542cd2a7ba3df0e11947dd19326c9d1cd81ae09">Xoloitzcuintli NFT Cigar Collection.</a> (RMZsmoke): π§ͺ\n' + '<a href="https://explorer.e.cash/tx/78efa5177e99bf05b48948ac7e23e6cc2255764e52ccf7092afb979a766dee2c">xolosArmyPOP</a> (RMZPOP): β‘οΈ\n' + '\n' + 'πΌ <b><i>1 NFT mint</i></b>\n' + 'π <b><i>1 new fixed-supply token</i></b>\n' + 'π¨ <b><i>1 new variable-supply token</i></b>\n' + 'π» <b><i>1 ALP tx</i></b>\n' + '𧩠<b><i>1 Mint Vault tx</i></b>\n' + 'β <b><i>1 invalid token tx</i></b>\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', ], ); }); }); diff --git a/apps/ecash-herald/test/utils.test.ts b/apps/ecash-herald/test/utils.test.ts index 138cbb145..8b9a514d0 100644 --- a/apps/ecash-herald/test/utils.test.ts +++ b/apps/ecash-herald/test/utils.test.ts @@ -1,442 +1,442 @@ // 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. import assert from 'assert'; import BigNumber from 'bignumber.js'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import config, { FiatCode, HeraldPriceApi } from '../config'; import { returnAddressPreview, getCoingeckoPrices, formatPrice, jsonReplacer, jsonReviver, mapToKeyValueArray, formatXecAmount, satsToFormattedValue, getEmojiFromBalanceSats, bigNumberAmountToLocaleString, containsOnlyPrintableAscii, } from '../src/utils'; import templates from './mocks/templates'; const { addressPreviews, mockCoingeckoPrices } = templates; describe('ecash-telegram-bot utils.js functions', function () { it('returnAddressPreview converts a valid ecash: address into an abbreviated preview at various slice sizes', function () { for (let i = 0; i < addressPreviews.length; i += 1) { const { address, preview, sliceSize } = addressPreviews[i]; assert.strictEqual( returnAddressPreview(address, sliceSize), preview, ); } }); it('getCoingeckoPrices returns object of expected shape for config API call', async function () { // onNoMatch: 'throwException' helps to debug if mock is not being used const mock = new MockAdapter(axios, { onNoMatch: 'throwException' }); const mockResult = { bitcoin: { usd: 28044.64857505 }, ecash: { usd: 0.00003113 }, ethereum: { usd: 1900.73166438 }, }; // Mock a successful API request mock.onGet().reply(200, mockResult); // Expected value will include ticker information const expectedCoingeckoPrices = [ { fiat: 'usd', price: 0.00003113, ticker: 'XEC', }, { fiat: 'usd', price: 28044.64857505, ticker: 'BTC', }, { fiat: 'usd', price: 1900.73166438, ticker: 'ETH', }, ]; assert.deepEqual(await getCoingeckoPrices(config.priceApi), { coingeckoResponse: mockResult, coingeckoPrices: expectedCoingeckoPrices, }); }); it('getCoingeckoPrices returns object of expected shape for API call of custom config', async function () { const apiConfig: HeraldPriceApi = { apiBase: 'https://api.coingecko.com/api/v3/simple/price', cryptos: [ { coingeckoSlug: 'ecash', ticker: 'XEC' }, { coingeckoSlug: 'monero', ticker: 'XMR' }, { coingeckoSlug: 'solana', ticker: 'SOL' }, ], fiat: 'eur', precision: 8, }; // onNoMatch: 'throwException' helps to debug if mock is not being used const mock = new MockAdapter(axios, { onNoMatch: 'throwException' }); const mockResult = { ecash: { eur: 0.00003113 }, monero: { eur: 107.64857505 }, solana: { eur: 22.73166438 }, }; // Mock a successful API request mock.onGet().reply(200, mockResult); // Expected value will include ticker information const expectedCoingeckoPrices = [ { fiat: 'eur' as FiatCode, price: 0.00003113, ticker: 'XEC', }, { fiat: 'eur' as FiatCode, price: 107.64857505, ticker: 'XMR', }, { fiat: 'eur' as FiatCode, price: 22.73166438, ticker: 'SOL', }, ]; assert.deepEqual(await getCoingeckoPrices(apiConfig), { coingeckoResponse: mockResult, coingeckoPrices: expectedCoingeckoPrices, }); }); it('getCoingeckoPrices returns false if API returns error response', async function () { // onNoMatch: 'throwException' helps to debug if mock is not being used const mock = new MockAdapter(axios, { onNoMatch: 'throwException' }); const mockResult = {}; // Mock an API error mock.onGet().reply(500, mockResult); assert.deepEqual(await getCoingeckoPrices(config.priceApi), false); }); it('getCoingeckoPrices returns false if API returns object of unexpected shape', async function () { // onNoMatch: 'throwException' helps to debug if mock is not being used const mock = new MockAdapter(axios, { onNoMatch: 'throwException' }); const mockResult = { bitcoin: { usd: 28044.64857505 }, ecash: { usd: 0.00003113 }, monero: { usd: 153.21055216 }, }; // Mock a successful API request that returns data of unexpected shape mock.onGet().reply(200, mockResult); assert.deepEqual(await getCoingeckoPrices(config.priceApi), false); }); it('getCoingeckoPrices returns false if API response is not of type object', async function () { // onNoMatch: 'throwException' helps to debug if mock is not being used const mock = new MockAdapter(axios, { onNoMatch: 'throwException' }); const mockResult = 'a string for some reason'; // Mock a successful API request that returns data of unexpected shape mock.onGet().reply(200, mockResult); assert.deepEqual(await getCoingeckoPrices(config.priceApi), false); }); it('formatPrice correctly formats a USD price greater than $10 and less than $100', function () { assert.strictEqual(formatPrice(10.55303, 'usd'), `$10.55`); }); it('formatPrice correctly formats a USD price greater than $1', function () { assert.strictEqual(formatPrice(1.52303, 'usd'), `$1.52`); }); it('formatPrice correctly formats a USD price less than $1', function () { assert.strictEqual(formatPrice(0.000035123, 'usd'), `$0.00003512`); }); it('formatPrice correctly formats a EUR price less than β¬1', function () { assert.strictEqual(formatPrice(0.000035123, 'eur'), `β¬0.00003512`); }); it('formatPrice correctly formats a GBP price greater than 100', function () { assert.strictEqual(formatPrice(1523.134239, 'gbp'), `Β£1,523`); }); it('formatPrice correctly formats a JPY price greater than Β₯100', function () { assert.strictEqual(formatPrice(100000.999923422, 'jpy'), `Β₯100,001`); }); it('formatPrice omits a currency symbol if it cannot find it', function () { assert.strictEqual( formatPrice(100000.999923422, 'cad' as FiatCode), `100,001`, ); }); it('formatXecAmount returns a string with 2 decimal places if XEC amount < 10', function () { assert.strictEqual(formatXecAmount(9.99), `9.99 XEC`); }); it('formatXecAmount returns a string with no decimal places if XEC amount < 10 and round number', function () { assert.strictEqual(formatXecAmount(9), `9 XEC`); }); it('formatXecAmount returns a string with no decimal places if 10 <= XEC amount < 100', function () { assert.strictEqual(formatXecAmount(12.51), `13 XEC`); }); it('formatXecAmount returns a string with no decimal places if 100 < XEC amount < 1000', function () { assert.strictEqual(formatXecAmount(125), `125 XEC`); }); it('formatXecAmount returns a thousands string with no decimal places if 1000 < XEC amount < 1 million', function () { assert.strictEqual(formatXecAmount(1000), `1k XEC`); }); it('formatXecAmount returns a rounded thousands string with no decimal places if 1000 < XEC amount < 1 million', function () { assert.strictEqual(formatXecAmount(555555.55), `556k XEC`); }); it('formatXecAmount returns a string with no decimal places if XEC amount is 1 trillion', function () { assert.strictEqual(formatXecAmount(1000000000000), `1T XEC`); }); it('formatXecAmount returns a rounded thousands string with no decimal places if 1000 < XEC amount < 1 million', function () { assert.strictEqual(formatXecAmount(555555.55), `556k XEC`); }); it('formatXecAmount returns a rounded millions string with no decimal places if 1M < XEC amount < 1B', function () { assert.strictEqual(formatXecAmount(555555555.55), `556M XEC`); }); it('formatXecAmount returns a rounded billions string with no decimal places if 1B < XEC amount < 1T', function () { assert.strictEqual(formatXecAmount(555555555555.55), `556B XEC`); }); it('formatXecAmount returns a rounded trillions string with no decimal places if XEC amount > 1T', function () { assert.strictEqual(formatXecAmount(55555555555555.55), `56T XEC`); }); it('formatXecAmount returns a rounded trillions string with no decimal places if XEC amount > 1T', function () { assert.strictEqual(formatXecAmount(19999999999999.99), `20T XEC`); }); it('formatXecAmount returns a trillions string with no decimal places for max possible XEC amount', function () { assert.strictEqual(formatXecAmount(21000000000000), `21T XEC`); }); it('satsToFormattedValue returns a 6-decimal formatted fiat amount if total fiat value is less than $0.00001', function () { assert.strictEqual( satsToFormattedValue(10, mockCoingeckoPrices), `$0.000003`, ); }); it('satsToFormattedValue returns a 5-decimal formatted fiat amount if total fiat value is less than $0.0001', function () { assert.strictEqual( satsToFormattedValue(100, mockCoingeckoPrices), `$0.00003`, ); }); it('satsToFormattedValue returns a 4-decimal formatted fiat amount if total fiat value is less than $0.001', function () { assert.strictEqual( satsToFormattedValue(1000, mockCoingeckoPrices), `$0.0003`, ); }); it('satsToFormattedValue returns a 3-decimal formatted fiat amount if total fiat value is less than $0.01', function () { assert.strictEqual( satsToFormattedValue(10000, mockCoingeckoPrices), `$0.003`, ); }); it('satsToFormattedValue returns a 2-decimal formatted fiat amount if total fiat value is less than $1', function () { assert.strictEqual( satsToFormattedValue(1000000, mockCoingeckoPrices), `$0.30`, ); }); it('satsToFormattedValue returns a formatted fiat amount if total fiat value is less than $10', function () { assert.strictEqual( satsToFormattedValue(10000000, mockCoingeckoPrices), '$3', ); }); it('satsToFormattedValue returns a formatted fiat amount if $100 < total fiat value < $1k', function () { assert.strictEqual( satsToFormattedValue(1234567890, mockCoingeckoPrices), '$370', ); }); it('satsToFormattedValue returns a formatted fiat amount if $1k < total fiat value < $1M', function () { assert.strictEqual( satsToFormattedValue(55555555555, mockCoingeckoPrices), '$17k', ); }); it('satsToFormattedValue returns a formatted fiat amount of $1M if $1M < total fiat value < $1B', function () { assert.strictEqual( satsToFormattedValue(3367973856209, mockCoingeckoPrices), '$1M', ); }); it('satsToFormattedValue returns a formatted fiat amount if $1M < total fiat value < $1B', function () { assert.strictEqual( satsToFormattedValue(55555555555555, mockCoingeckoPrices), '$17M', ); }); it('satsToFormattedValue returns a formatted fiat amount if total fiat value > $1B', function () { assert.strictEqual( satsToFormattedValue(21000000000000000, mockCoingeckoPrices), '$6B', ); }); it('satsToFormattedValue returns a formatted fiat amount if Β£1M < total fiat value < Β£1B', function () { const gbpPrices = [ { fiat: 'gbp' as FiatCode, price: 0.00003, ticker: 'XEC', }, ]; assert.strictEqual( satsToFormattedValue(55555555555555, gbpPrices), 'Β£17M', ); }); it('satsToFormattedValue returns a formatted XEC amount if coingeckoPrices is false', function () { assert.strictEqual( satsToFormattedValue(55555555555555, false), '556B XEC', ); }); it('satsToFormattedValue returns a USD amount with 7 decimal places if fiat qty is less than 0.000001', function () { assert.strictEqual( satsToFormattedValue(1, mockCoingeckoPrices), '$0.0000003', ); }); it('jsonReplacer and jsonReviver can encode and decode a Map to and from JSON', function () { const map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'], ]); const jsonText = JSON.stringify(map, jsonReplacer); const roundTrip = JSON.parse(jsonText, jsonReviver); assert.deepEqual(map, roundTrip); }); it('jsonReplacer and jsonReviver can encode and decode Map containing a BigNumber', function () { const bigNumberMap = new Map([ [ '76a9144c1efd024f560e4e1aaf4b62416cd1e82fbed24f88ac', new BigNumber(36), ], [ '76a9144c1efd024f560e4e1aaf4b62416cd1e82fbed24f88ac', new BigNumber(72), ], ]); const jsonText = JSON.stringify(bigNumberMap, jsonReplacer); const roundTrip = JSON.parse(jsonText, jsonReviver); assert.deepEqual(bigNumberMap, roundTrip); }); it('jsonReplacer and jsonReviver can encode and decode a Set to and from JSON', function () { const set = new Set(['one', 'two', 'three']); const jsonText = JSON.stringify(set, jsonReplacer); const roundTrip = JSON.parse(jsonText, jsonReviver); assert.deepEqual(set, roundTrip); }); it('jsonReplacer and jsonReviver can encode and decode an object including a Set and a Map to and from JSON', async function () { const map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'], ]); const set = new Set(['one', 'two', 'three']); const jsonText = JSON.stringify({ map, set }, jsonReplacer); const roundTrip = JSON.parse(jsonText, jsonReviver); assert.deepEqual({ map, set }, roundTrip); }); it('mapToKeyValueArray converts a map to a key value array and back to the same map', function () { const map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'], ]); const kvArray = mapToKeyValueArray(map); const roundTrip = new Map(kvArray); assert.deepEqual(map, roundTrip); }); it('getEmojiFromBalanceSats provides all correct threshold emojis', function () { const { whaleSats, emojis } = config; const names = Object.keys(whaleSats); for (let i = 0; i < names.length; i += 1) { // Really bad types in the config as app was written without ts // TODO fixme - // @ts-ignore + // @ts-expect-error config object developed before typescript implementation const balanceSats = whaleSats[names[i]]; assert.strictEqual( getEmojiFromBalanceSats(balanceSats), - // @ts-ignore + // @ts-expect-error config object developed before typescript implementation emojis[names[i]], ); } }); it('bigNumberAmountToLocaleString correctly formats a token send amount with no decimal places', async function () { const tokenSendAmountString = '1000000'; const decimals = 0; assert.strictEqual( bigNumberAmountToLocaleString(tokenSendAmountString, decimals), '1,000,000', ); }); it('bigNumberAmountToLocaleString correctly formats a token send amount with 1 decimal place', async function () { const tokenSendAmountString = '10000000000000001'; const decimals = 1; assert.strictEqual( bigNumberAmountToLocaleString(tokenSendAmountString, decimals), '1,000,000,000,000,000.1', ); }); it('bigNumberAmountToLocaleString correctly formats a token send amount with 9 decimal places', async function () { const tokenSendAmountString = '123456789123456789'; const decimals = 9; assert.strictEqual( bigNumberAmountToLocaleString(tokenSendAmountString, decimals), '123,456,789.123456789', ); }); it('containsOnlyPrintableAscii correctly identifies a hex string containing only ascii ranged values as probably ascii', async function () { const hexString = '3d3a4554482e4554483a3078613961614633304636353935354336396331364233333435423531443432364439423838426138373a3834313332313a74723a30'; assert.strictEqual(containsOnlyPrintableAscii(hexString), true); }); it('containsOnlyPrintableAscii recognizes a string of odd length is probably not ascii', async function () { const hexString = '3d3a4554482e4554483a3078613961614633304636353935354336396331364233333435423531443432364439423838426138373a3834313332313a74723a3'; assert.strictEqual(containsOnlyPrintableAscii(hexString), false); }); it('containsOnlyPrintableAscii recognizes a string containing characters out of ascii range is probably not ascii', async function () { const hexString = '663ddd99990bcd969994ec2288a2a86dc532e1a8'; assert.strictEqual(containsOnlyPrintableAscii(hexString), false); }); it('containsOnlyPrintableAscii returns false for a string that contains all valid ascii characters but also a control character < 32', async function () { const hexString = '1f663ddd99990bcd969994ec2288a2a86dc532e1a8'; assert.strictEqual(containsOnlyPrintableAscii(hexString), false); }); it('containsOnlyPrintableAscii returns false for a string that contains all valid ascii characters but also a control character > 126', async function () { const hexString = '7f663ddd99990bcd969994ec2288a2a86dc532e1a8'; assert.strictEqual(containsOnlyPrintableAscii(hexString), false); }); });