diff --git a/apps/ecash-herald/config.js b/apps/ecash-herald/config.js --- a/apps/ecash-herald/config.js +++ b/apps/ecash-herald/config.js @@ -52,6 +52,10 @@ shrimp: 0, }, emojis: { + agora: 'π', + agoraBuy: 'π°', + agoraList: 'π·', + agoraCancel: 'β', alias: 'πΎ', alp: 'π»', invalid: 'β', diff --git a/apps/ecash-herald/package-lock.json b/apps/ecash-herald/package-lock.json --- a/apps/ecash-herald/package-lock.json +++ b/apps/ecash-herald/package-lock.json @@ -15,6 +15,8 @@ "cache-manager": "^5.5.2", "chronik-client": "file:../../modules/chronik-client", "cron": "^3.1.7", + "ecash-agora": "file:../../modules/ecash-agora", + "ecash-lib": "file:../../modules/ecash-lib", "ecash-script": "file:../../modules/ecash-script", "ecashaddrjs": "file:../../modules/ecashaddrjs", "node-telegram-bot-api": "^0.66.0" @@ -62,6 +64,56 @@ "typescript": "^4.5.2" } }, + "../../modules/ecash-agora": { + "version": "0.1.1", + "license": "MIT", + "dependencies": { + "chronik-client": "file:../chronik-client", + "ecash-lib": "file:../ecash-lib" + }, + "devDependencies": { + "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@types/chai": "^4.3.14", + "@types/chai-as-promised": "^7.1.8", + "@types/mocha": "^10.0.6", + "@types/node": "^20.12.7", + "chai": "^4.4.1", + "chai-as-promised": "^7.1.1", + "eslint-plugin-header": "^3.1.1", + "mocha": "^10.4.0", + "mocha-junit-reporter": "^2.2.1", + "nyc": "^15.1.0", + "source-map-support": "^0.5.21", + "ts-node": "^10.9.2", + "tsx": "^4.7.2", + "typescript": "^5.4.3", + "typescript-eslint": "^7.6.0" + } + }, + "../../modules/ecash-lib": { + "version": "0.2.1", + "license": "MIT", + "dependencies": { + "ecashaddrjs": "file:../ecashaddrjs" + }, + "devDependencies": { + "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@types/chai": "^4.3.14", + "@types/mocha": "^10.0.6", + "@types/node": "^20.12.7", + "chai": "^5.1.0", + "chronik-client": "file:../chronik-client", + "eslint-plugin-header": "^3.1.1", + "mocha": "^10.4.0", + "mocha-junit-reporter": "^2.2.1", + "nyc": "^15.1.0", + "source-map-support": "^0.5.21", + "ts-node": "^10.9.2", + "tsx": "^4.7.2", + "typescript": "^5.4.3", + "typescript-eslint": "^7.6.0" + } + }, "../../modules/ecash-script": { "version": "2.1.3", "license": "MIT", @@ -1779,6 +1831,14 @@ "node": ">=6.0.0" } }, + "node_modules/ecash-agora": { + "resolved": "../../modules/ecash-agora", + "link": true + }, + "node_modules/ecash-lib": { + "resolved": "../../modules/ecash-lib", + "link": true + }, "node_modules/ecash-script": { "resolved": "../../modules/ecash-script", "link": true @@ -6483,6 +6543,50 @@ "esutils": "^2.0.2" } }, + "ecash-agora": { + "version": "file:../../modules/ecash-agora", + "requires": { + "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@types/chai": "^4.3.14", + "@types/chai-as-promised": "^7.1.8", + "@types/mocha": "^10.0.6", + "@types/node": "^20.12.7", + "chai": "^4.4.1", + "chai-as-promised": "^7.1.1", + "chronik-client": "file:../chronik-client", + "ecash-lib": "file:../ecash-lib", + "eslint-plugin-header": "^3.1.1", + "mocha": "^10.4.0", + "mocha-junit-reporter": "^2.2.1", + "nyc": "^15.1.0", + "source-map-support": "^0.5.21", + "ts-node": "^10.9.2", + "tsx": "^4.7.2", + "typescript": "^5.4.3", + "typescript-eslint": "^7.6.0" + } + }, + "ecash-lib": { + "version": "file:../../modules/ecash-lib", + "requires": { + "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@types/chai": "^4.3.14", + "@types/mocha": "^10.0.6", + "@types/node": "^20.12.7", + "chai": "^5.1.0", + "chronik-client": "file:../chronik-client", + "ecashaddrjs": "file:../ecashaddrjs", + "eslint-plugin-header": "^3.1.1", + "mocha": "^10.4.0", + "mocha-junit-reporter": "^2.2.1", + "nyc": "^15.1.0", + "source-map-support": "^0.5.21", + "ts-node": "^10.9.2", + "tsx": "^4.7.2", + "typescript": "^5.4.3", + "typescript-eslint": "^7.6.0" + } + }, "ecash-script": { "version": "file:../../modules/ecash-script", "requires": { diff --git a/apps/ecash-herald/package.json b/apps/ecash-herald/package.json --- a/apps/ecash-herald/package.json +++ b/apps/ecash-herald/package.json @@ -28,6 +28,8 @@ "cache-manager": "^5.5.2", "chronik-client": "file:../../modules/chronik-client", "cron": "^3.1.7", + "ecash-agora": "file:../../modules/ecash-agora", + "ecash-lib": "file:../../modules/ecash-lib", "ecash-script": "file:../../modules/ecash-script", "ecashaddrjs": "file:../../modules/ecashaddrjs", "node-telegram-bot-api": "^0.66.0" diff --git a/apps/ecash-herald/src/parse.js b/apps/ecash-herald/src/parse.js --- a/apps/ecash-herald/src/parse.js +++ b/apps/ecash-herald/src/parse.js @@ -27,6 +27,8 @@ containsOnlyPrintableAscii, } = require('./utils'); const lokadMap = require('../constants/lokad'); +const { scriptOps } = require('ecash-agora'); +const { Script, fromHex, OP_0 } = require('ecash-lib'); // Constants for SLP 1 token types as returned by chronik-client const SLP_1_PROTOCOL_NUMBER = 1; @@ -2192,13 +2194,17 @@ let appTxs = 0; let unknownLokadTxs = 0; - // tokenId => {info, listings, adPreps, sends, burns, mints, genesis: {genesisQty: <>, hasBaton: <>}} + // tokenId => {info, list, cancel, buy, adPrep, send, burn, mint, genesis: {genesisQty: <>, hasBaton: <>}} const tokenActions = new Map(); let invalidTokenEntries = 0; let nftTokenEntries = 0; let mintVaultTokenEntries = 0; let alpTokenEntries = 0; + // Agora vars + let agoraTxs = 0; + const agoraActions = new Map(); + for (const tx of txs) { const { inputs, outputs, block, tokenEntries, isCoinbase } = tx; @@ -2409,33 +2415,262 @@ } case 'SEND': { // SEND may be Agora or Burn - const existingActions = + 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 + // Such txs will have + // 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 + agoraActions.set( + tokenId, + typeof existingAgoraActions === + 'undefined' + ? { + list: { + count: 1, + }, + actionCount: 1, + } + : { + ...existingAgoraActions, + list: { + count: + 'list' in + existingAgoraActions + ? existingAgoraActions + .list + .count + + 1 + : 1, + }, + actionCount: + existingAgoraActions.actionCount + + 1, + }, + ); + 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 + agoraActions.set( + tokenId, + typeof existingAgoraActions === + 'undefined' + ? { + cancel: { + count: 1, + }, + actionCount: 1, + } + : { + ...existingAgoraActions, + cancel: { + count: + 'cancel' in + existingAgoraActions + ? existingAgoraActions + .cancel + .count + + 1 + : 1, + }, + actionCount: + existingAgoraActions.actionCount + + 1, + }, + ); + isAgoraBuySellList = true; + // Stop processing inputs for this tx + break; + } else { + // Agora purchase + agoraActions.set( + tokenId, + typeof existingAgoraActions === + 'undefined' + ? { + buy: { + count: 1, + }, + actionCount: 1, + } + : { + ...existingAgoraActions, + buy: { + count: + 'buy' in + existingAgoraActions + ? existingAgoraActions + .buy + .count + + 1 + : 1, + }, + actionCount: + existingAgoraActions.actionCount + + 1, + }, + ); + isAgoraBuySellList = true; + // Stop processing inputs for this tx + break; + } + } + } + } catch (err) { + 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; + } - // TODO parse agora + // 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 + agoraActions.set( + tokenId, + typeof existingAgoraActions === + 'undefined' + ? { + adPrep: { + count: 1, + }, + actionCount: 1, + } + : { + ...existingAgoraActions, + adPrep: { + count: + 'adPrep' in + existingAgoraActions + ? existingAgoraActions + .adPrep + .count + + 1 + : 1, + }, + actionCount: + existingAgoraActions.actionCount + + 1, + }, + ); + break; + // Stop iterating over outputs + } + } catch (err) { + 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') { tokenActions.set( tokenId, - typeof existingActions === 'undefined' + typeof existingTokenActions === + 'undefined' ? { burn: { count: 1 }, actionCount: 1, } : { - ...existingActions, + ...existingTokenActions, burn: { count: 'burn' in - existingActions - ? existingActions + existingTokenActions + ? existingTokenActions .burn .count + 1 : 1, }, actionCount: - existingActions.actionCount + + existingTokenActions.actionCount + 1, }, ); @@ -2446,19 +2681,20 @@ // Parse as send tokenActions.set( tokenId, - typeof existingActions === 'undefined' + typeof existingTokenActions === 'undefined' ? { send: { count: 1 }, actionCount: 1 } : { - ...existingActions, + ...existingTokenActions, send: { count: - 'send' in existingActions - ? existingActions.send - .count + 1 + 'send' in + existingTokenActions + ? existingTokenActions + .send.count + 1 : 1, }, actionCount: - existingActions.actionCount + + existingTokenActions.actionCount + 1, }, ); @@ -2732,6 +2968,111 @@ tgMsg.push(''); } + // Agora summary + 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} <b><i>${agoraTxs.toLocaleString( + 'en-US', + )} Agora 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.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(''); + } + // Token summary if (tokenTxs > 0) { // Sort tokenActions map by number of token actions @@ -2742,12 +3083,39 @@ keyValueArrayA[1].actionCount, ), ); + + // Note we may have some agora tokens here, which is fine + // If agora tokens are also the top for other actions, want to demonstrate that + const nonAgoraTokens = Array.from(sortedTokenActions.keys()); + + const nonAgoraTokenCount = nonAgoraTokens.length; tgMsg.push( `${config.emojis.token} <b><i>${tokenTxs.toLocaleString( 'en-US', - )} token tx${tokenTxs > 1 ? 's' : ''}</i></b>`, + )} token tx${ + tokenTxs > 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, ); - sortedTokenActions.forEach((tokenActionInfo, tokenId) => { + + 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.get(tokenId); const { send, genesis, burn, mint } = tokenActionInfo; @@ -2787,7 +3155,7 @@ : '' }`, ); - }); + } if (alpTokenEntries > 0) { tgMsg.push( `${config.emojis.alp} <b>${alpTokenEntries.toLocaleString( diff --git a/apps/ecash-herald/test/mocks/dailyTxs.js b/apps/ecash-herald/test/mocks/dailyTxs.js --- a/apps/ecash-herald/test/mocks/dailyTxs.js +++ b/apps/ecash-herald/test/mocks/dailyTxs.js @@ -326,6 +326,313 @@ timestamp: 1728988376, }, }, + // Agora txs + // SLP1 partial list + // 20469a4316506e0fea99ad0673d6663f2f546c0aad84b741e08c4d0f9248b18c + { + txid: '20469a4316506e0fea99ad0673d6663f2f546c0aad84b741e08c4d0f9248b18c', + version: 2, + inputs: [ + { + prevOut: { + txid: 'd84e49d7396553f8931e7ecebf1717ced0ba962d3f4be7ce672d418a9f3a107d', + outIdx: 1, + }, + inputScript: + '0441475230075041525449414c4180f1bfdb06735c27ffe75627fa1fecd46844334f6686dd5e64b01d4e68de28d98b6ed87ac5bf86ce27af78b06944beae25e2f0084433d563d6bd19c6616a5a62414c8c4c766a04534c500001010453454e442001d63c4f4cb496829a6743f7b1805d086ea3877a1dd34b3f92ffba2c9c99f89608000000000000000000013b62100000000000298f0000000000006de4ff1700000000f3282c4e03771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba601557f77ad075041525449414c88044147523087', + value: 914, + sequenceNo: 4294967295, + token: { + tokenId: + '01d63c4f4cb496829a6743f7b1805d086ea3877a1dd34b3f92ffba2c9c99f896', + tokenType: { + protocol: 'SLP', + type: 'SLP_TOKEN_TYPE_FUNGIBLE', + number: 1, + }, + amount: '2000', + isMintBaton: false, + entryIdx: 0, + }, + outputScript: 'a9142ee1060eafaafbad92d2d5420120d40c0394a84e87', + }, + ], + outputs: [ + { + value: 0, + outputScript: + '6a04534c500001010453454e442001d63c4f4cb496829a6743f7b1805d086ea3877a1dd34b3f92ffba2c9c99f8960800000000000007d0', + }, + { + value: 546, + outputScript: 'a914563178ea073228709397a2c98baf10677e683e6687', + token: { + tokenId: + '01d63c4f4cb496829a6743f7b1805d086ea3877a1dd34b3f92ffba2c9c99f896', + tokenType: { + protocol: 'SLP', + type: 'SLP_TOKEN_TYPE_FUNGIBLE', + number: 1, + }, + amount: '2000', + isMintBaton: false, + entryIdx: 0, + }, + }, + ], + lockTime: 0, + timeFirstSeen: 1729929435, + size: 368, + isCoinbase: false, + tokenEntries: [ + { + tokenId: + '01d63c4f4cb496829a6743f7b1805d086ea3877a1dd34b3f92ffba2c9c99f896', + 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: 868211, + hash: '0000000000000000297b2e66c61c87a7b77c9f49c1033e0db146272ea150d2cc', + timestamp: 1729931891, + }, + }, + // SLP1 partial buy + // f4b2bd7fc77975103223f41f588751eedf16cfb4ee8dd44ebcb44191fd0d2eff + { + txid: 'f4b2bd7fc77975103223f41f588751eedf16cfb4ee8dd44ebcb44191fd0d2eff', + version: 2, + inputs: [ + { + prevOut: { + txid: '4822a0ccc510ddb0ce3ba423e3de49c12993a607c4c108345be9f6eef84767f5', + outIdx: 2, + }, + inputScript: + '0441475230075041525449414c21023c72addb4fdf09af94f0c94d7fe92a386a7e70cf8a1d85916386bb2535c7b1b1407996448a8c0b89e341453ba9726eb40a2e8c07401808b82dc3623a2ab2c353c9115cbdbcd738b01d01a718c9c10336823231f7f16cdcc3ac43001c4c0c11e3764422020000000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac89680000000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac4d2c01f56747f8eef6e95b3408c1c407a69329c149dee323a43bceb0dd10c5cca0224802000000d37b63817b6ea2697604d0aa4701a2697602e2539700887d94527901377f75789263587e7802e253965880bc007e7e68587e527902e253965880bc007e7e825980bc7c7e007e7b02e1539302e2539658807e041976a914707501557f77a97e0288ac7e7e6b7d02220258800317a9147e024c7672587d807e7e7e01ab7e537901257f7702d3007f5c7f7701207f547f750440aef137886b7ea97e01877e7c92647500687b8292697e6c6c7b7eaa88520144807c7ea86f7bbb7501c17e7c677501557f7768ad075041525449414c880441475230872202000000000000ffffffffdc591bfcfc4bbdd22709d6be93a5c9f25c9be52771a079ed51a1bd8767c4fa5d40aef137c100000004d0aa4701514d55014c766a04534c500001010453454e4420aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb10800000000000000000000e253000000000000e253000000000000d0aa47010000000040aef13703771805b54969a9bea4e3eb14a82851c67592156ddb5e52d3d53677d14a40fba60860b8507800000000ab7b63817b6ea2697604d0aa4701a2697602e2539700887d94527901377f75789263587e7802e253965880bc007e7e68587e527902e253965880bc007e7e825980bc7c7e007e7b02e1539302e2539658807e041976a914707501557f77a97e0288ac7e7e6b7d02220258800317a9147e024c7672587d807e7e7e01ab7e537901257f7702d3007f5c7f7701207f547f750440aef137886b7ea97e01877e7c92647500687b8292697e6c6c7b7eaa88520144807c7ea86f7bbb7501c17e7c677501557f7768ad075041525449414c88044147523087', + value: 546, + sequenceNo: 4294967295, + token: { + tokenId: + 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', + tokenType: { + protocol: 'SLP', + type: 'SLP_TOKEN_TYPE_FUNGIBLE', + number: 1, + }, + amount: '94000', + isMintBaton: false, + entryIdx: 0, + }, + outputScript: 'a914bdda81ba1d3bf0598d24d77b461bafbed0ba7af987', + }, + { + prevOut: { + txid: '4822a0ccc510ddb0ce3ba423e3de49c12993a607c4c108345be9f6eef84767f5', + outIdx: 4, + }, + inputScript: + '4171aa357cf2a1e41d819440432b05557bd24da15319e714e6084330496e63f4cce9d8b3e77c86b9ae4a1ebd078a6ed8cbef34347b2d5a99643443c44d350e2ff0412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', + value: 30808, + sequenceNo: 4294967295, + outputScript: + '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', + }, + ], + outputs: [ + { + value: 0, + outputScript: + '6a04534c500001010453454e4420aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1080000000000000000080000000000016b480800000000000003e8', + }, + { + value: 1000, + outputScript: + '76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', + }, + { + value: 546, + outputScript: 'a914366be7e1eee2040519012d19fbfc3002456aede487', + token: { + tokenId: + 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', + tokenType: { + protocol: 'SLP', + type: 'SLP_TOKEN_TYPE_FUNGIBLE', + number: 1, + }, + amount: '93000', + isMintBaton: false, + entryIdx: 0, + }, + }, + { + value: 546, + outputScript: + '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', + token: { + tokenId: + 'aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1', + tokenType: { + protocol: 'SLP', + type: 'SLP_TOKEN_TYPE_FUNGIBLE', + number: 1, + }, + amount: '1000', + isMintBaton: false, + entryIdx: 0, + }, + }, + { + value: 26761, + outputScript: + '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', + }, + ], + lockTime: 938585664, + timeFirstSeen: 1729985270, + size: 1244, + 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: 868307, + hash: '000000000000000010c9f9bdb9446aac244e19ad0ae3936d73507964ed544e36', + timestamp: 1729986430, + }, + }, + // SLP1 partial cancel + // e9d594e054bf9a7cead11cdc31953f0e45782c97c6298513f41b70eb408aa1a8 + { + txid: 'e9d594e054bf9a7cead11cdc31953f0e45782c97c6298513f41b70eb408aa1a8', + version: 2, + inputs: [ + { + prevOut: { + txid: '58ec58688cef1d0abe2ee30c15f84af51833e61e998841fac3ecbcadafc31233', + outIdx: 2, + }, + inputScript: + '41fd18138ab17386e9599e54d9d5f1994d1c4add3af860b1ece44b71d04bc7e7cd799e1234e2959236cd38558713d7fdb797a894c527906b0235a38519ad63fbea4121024f624d04900c2e3b7ea6014cb257f525b6d229db274bceeadbb1f06c07776e82', + value: 975251, + sequenceNo: 4294967295, + outputScript: + '76a9147847fe7070bec8567b3e810f543f2f80cc3e03be88ac', + }, + { + prevOut: { + txid: '0c580a7dbfb7f160f0e4623faa24eb0475b2220704c8c46f279a479a477433f8', + outIdx: 1, + }, + inputScript: + '0441475230075041525449414c4113bb98283dc7a2f69957940bb3a45f4ec6050b61bcc1b1134d786727e379c8793107bf0d0b0e051665ab3eed2cca34901646cf564a1ab52cb32668da229eef0b41004d5f014c766a04534c500001010453454e442020a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8080000000000000000030276a4000000000000e815000000000000a24a2600000000004b4a343a024f624d04900c2e3b7ea6014cb257f525b6d229db274bceeadbb1f06c07776e8208948eff7f00000000ab7b63817b6ea2697603a24a26a269760376a4009700887d94527901377f75789263587e780376a400965580bc030000007e7e68587e52790376a400965580bc030000007e7e825980bc7c7e0200007e7b02e7159302e8159656807e041976a914707501557f77a97e0288ac7e7e6b7d02220258800317a9147e024c7672587d807e7e7e01ab7e537901257f7702dd007f5c7f7701207f547f75044b4a343a886b7ea97e01877e7c92647500687b8292697e6c6c7b7eaa88520144807c7ea86f7bbb7501c17e7c677501557f7768ad075041525449414c88044147523087', + value: 546, + sequenceNo: 4294967295, + token: { + tokenId: + '20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8', + tokenType: { + protocol: 'SLP', + type: 'SLP_TOKEN_TYPE_FUNGIBLE', + number: 1, + }, + amount: '855738679296', + isMintBaton: false, + entryIdx: 0, + }, + outputScript: 'a914cb61d733f8e99b1b40d40a53a59aca8a08368a6f87', + }, + ], + outputs: [ + { + value: 0, + outputScript: + '6a04534c500001010453454e442020a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f808000000c73e000000', + }, + { + value: 546, + outputScript: + '76a9147847fe7070bec8567b3e810f543f2f80cc3e03be88ac', + token: { + tokenId: + '20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8', + tokenType: { + protocol: 'SLP', + type: 'SLP_TOKEN_TYPE_FUNGIBLE', + number: 1, + }, + amount: '855738679296', + isMintBaton: false, + entryIdx: 0, + }, + }, + { + value: 973723, + outputScript: + '76a9147847fe7070bec8567b3e810f543f2f80cc3e03be88ac', + }, + ], + lockTime: 0, + timeFirstSeen: 1729789538, + size: 760, + isCoinbase: false, + tokenEntries: [ + { + tokenId: + '20a0b9337a78603c6681ed2bc541593375535dcd9979196620ce71f233f2f6f8', + tokenType: { + protocol: 'SLP', + type: 'SLP_TOKEN_TYPE_FUNGIBLE', + number: 1, + }, + txType: 'SEND', + isInvalid: false, + burnSummary: '', + failedColorings: [], + actualBurnAmount: '0', + intentionalBurn: '0', + burnsMintBatons: false, + }, + ], + tokenFailedParsings: [], + tokenStatus: 'TOKEN_STATUS_NORMAL', + block: { + height: 867971, + hash: '000000000000000013f3d459ae121dc1494e7e9fe57c2e60cf393184d7ab6dc9', + timestamp: 1729793460, + }, + }, // Token txs // SLP1 fungible txs // Genesis tx @@ -3090,6 +3397,16 @@ hash: '', }, ], + [ + '01d63c4f4cb496829a6743f7b1805d086ea3877a1dd34b3f92ffba2c9c99f896', + { + tokenTicker: 'BULL', + tokenName: 'Bull', + url: 'https://cashtab.com/', + decimals: 0, + hash: '', + }, + ], ]); module.exports = { dailyTxs, tokenInfoMap }; diff --git a/apps/ecash-herald/test/parse.test.js b/apps/ecash-herald/test/parse.test.js --- a/apps/ecash-herald/test/parse.test.js +++ b/apps/ecash-herald/test/parse.test.js @@ -365,8 +365,8 @@ ), [ '<b>15 Oct 2024</b>\n' + - 'π¦56,311 blocks\n' + - 'β‘οΈ23 txs\n' + + 'π¦56,900 blocks\n' + + 'β‘οΈ26 txs\n' + '\n' + 'π<b>1 XEC = $0.00003487</b> <i>(-0.40%)</i>\n' + 'Trading volume: $5,957,333\n' + @@ -388,7 +388,13 @@ 'π <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>9 token txs</i></b>\n' + + 'π <b><i>3 Agora 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>12 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' + 'π» <b>1</b> ALP tx\n' + @@ -428,8 +434,8 @@ ), [ '<b>15 Oct 2024</b>\n' + - 'π¦56,311 blocks\n' + - 'β‘οΈ23 txs\n' + + 'π¦56,900 blocks\n' + + 'β‘οΈ26 txs\n' + '\n' + 'π<b>1 XEC = $0.00003487</b> <i>(-0.40%)</i>\n' + 'Trading volume: $5,957,333\n' + @@ -451,7 +457,13 @@ 'π <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>9 token txs</i></b>\n' + + 'π <b><i>3 Agora 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>12 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' + 'π» <b>1</b> ALP tx\n' + @@ -484,8 +496,8 @@ ), [ '<b>15 Oct 2024</b>\n' + - 'π¦56,311 blocks\n' + - 'β‘οΈ23 txs\n' + + 'π¦56,900 blocks\n' + + 'β‘οΈ26 txs\n' + '\n' + '<b><i>βοΈ3 miners found blocks</i></b>\n' + '<u>Top 3</u>\n' + @@ -503,7 +515,13 @@ 'π <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>9 token txs</i></b>\n' + + 'π <b><i>3 Agora 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>12 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' + 'π» <b>1</b> ALP tx\n' + diff --git a/contrib/teamcity/build-configurations.yml b/contrib/teamcity/build-configurations.yml --- a/contrib/teamcity/build-configurations.yml +++ b/contrib/teamcity/build-configurations.yml @@ -60,6 +60,8 @@ # to be built, otherwise unset # - DEPENDS_ECASH_LIB: "true" if these tests require ecash-lib to be # built, otherwise unset + # - DEPENDS_ECASH_AGORA: "true" if these tests require ecash-agora to be built, + # otherwise unset # - DEPENDS_CHRONIK_CLIENT: "true" if these tests require chronik-client # to be build, otherwise unset js-mocha: @@ -121,6 +123,15 @@ npm run build fi + if [ -z "${DEPENDS_ECASH_AGORA+x}" ] ; then + echo "Test does not depend on ecash-agora" + else + echo "Test depends on ecash-agora. Building TypeScript..." + pushd "${TOPLEVEL}/modules/ecash-agora" + npm ci + npm run build + fi + # Install ecash-script dependencies if this test uses them if [ -z "${DEPENDS_ECASH_SCRIPT+x}" ] ; then echo "Test does not depend on ecash-script, skipping ecash-script dependencies..." @@ -836,6 +847,9 @@ DEPENDS_CHRONIK_CLIENT: "true" DEPENDS_ECASH_SCRIPT: "true" DEPENDS_ECASHADDRJS: "true" + DEPENDS_ECASH_LIB_WASM: "true" + DEPENDS_ECASH_LIB: "true" + DEPENDS_ECASH_AGORA: "true" templates: - js-mocha diff --git a/ecash-herald.Dockerfile b/ecash-herald.Dockerfile --- a/ecash-herald.Dockerfile +++ b/ecash-herald.Dockerfile @@ -2,10 +2,52 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. +# Multi-stage +# 1) rust image for ecash-lib +# 2) Node image for prod deployment of ecash-lib + +# 1) rust image for ecash-lib +FROM rust:1.76.0 AS wasmbuilder + +RUN apt-get update \ + && apt-get install clang binaryen -y \ + && rustup target add wasm32-unknown-unknown \ + && cargo install -f wasm-bindgen-cli@0.2.92 + +# Copy Cargo.toml +WORKDIR /app/ +COPY Cargo.toml . + +# Copy chronik to same directory structure as monorepo +# This needs to be in place to run ./build-wasm +WORKDIR /app/chronik/ +COPY chronik/ . + +# Copy secp256k1 to same directory structure as monorepo +WORKDIR /app/src/secp256k1 +COPY src/secp256k1/ . + +# Copy ecash-secp256k1, ecash-lib and ecash-lib-wasm files to same directory structure as monorepo +WORKDIR /app/modules/ecash-secp256k1 +COPY modules/ecash-secp256k1 . +WORKDIR /app/modules/ecash-lib +COPY modules/ecash-lib . +WORKDIR /app/modules/ecash-lib-wasm +COPY modules/ecash-lib-wasm . + +# Build web assembly for ecash-lib +RUN ./build-wasm.sh + +# 2) Node image for prod deployment of token-server + # Node image for prod deployment of ecash-herald FROM node:20-bookworm-slim +# Copy static assets from WasmBuilder stage (ecash-lib-wasm and ecash-lib, with wasm built in place) +WORKDIR /app/modules +COPY --from=WasmBuilder /app/modules . + # Build all local ecash-herald dependencies # ecashaddrjs @@ -25,6 +67,17 @@ COPY modules/ecash-script/ . RUN npm ci +# ecash-lib +WORKDIR /app/modules/ecash-lib +RUN npm ci +RUN npm run build + +# ecash-agora +WORKDIR /app/modules/ecash-agora +COPY modules/ecash-agora/ . +RUN npm ci +RUN npm run build + # Now that local dependencies are ready, build ecash-herald WORKDIR /app/apps/ecash-herald diff --git a/modules/ecash-agora/README.md b/modules/ecash-agora/README.md --- a/modules/ecash-agora/README.md +++ b/modules/ecash-agora/README.md @@ -159,3 +159,4 @@ - Add validation to acceptTx method of AgoraPartial to prevent creation of unspendable offers [D16944](https://reviews.bitcoinabc.org/D16944) - Export `scriptOps` helper function [D16972](https://reviews.bitcoinabc.org/D16972) - Improve approximation for USD-esque tokens [D16995](https://reviews.bitcoinabc.org/D16995) +- Update tsconfig to support use in nodejs [D17019](https://reviews.bitcoinabc.org/D17019) diff --git a/modules/ecash-agora/package.json b/modules/ecash-agora/package.json --- a/modules/ecash-agora/package.json +++ b/modules/ecash-agora/package.json @@ -3,7 +3,6 @@ "version": "0.1.1", "description": "Library for interacting with the eCash Agora protocol", "main": "./dist/index.js", - "type": "module", "scripts": { "build": "tsc && tsc -p ./tsconfig.build.json", "test": "mocha --import=tsx ./src/*.test.ts ./src/**/*.test.ts", diff --git a/modules/ecash-agora/tsconfig.json b/modules/ecash-agora/tsconfig.json --- a/modules/ecash-agora/tsconfig.json +++ b/modules/ecash-agora/tsconfig.json @@ -5,8 +5,7 @@ /* ES2020 for bigint, DOM for WebAssemby */ "lib": ["ES2020", "DOM"], /* Modules */ - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "commonjs", /* Emit */ "noEmit": true, /* Interop Constraints */