Changeset View
Changeset View
Standalone View
Standalone View
apps/ecash-herald/src/parse.js
// Copyright (c) 2023 The Bitcoin developers | // Copyright (c) 2023 The Bitcoin developers | ||||
// Distributed under the MIT software license, see the accompanying | // Distributed under the MIT software license, see the accompanying | ||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php. | // file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
'use strict'; | 'use strict'; | ||||
const config = require('../config'); | const config = require('../config'); | ||||
const opReturn = require('../constants/op_return'); | |||||
const knownMiners = require('../constants/miners'); | |||||
const cashaddr = require('ecashaddrjs'); | const cashaddr = require('ecashaddrjs'); | ||||
const BigNumber = require('bignumber.js'); | const BigNumber = require('bignumber.js'); | ||||
const { | const { | ||||
prepareStringForTelegramHTML, | prepareStringForTelegramHTML, | ||||
splitOverflowTgMsg, | splitOverflowTgMsg, | ||||
} = require('./telegram'); | } = require('./telegram'); | ||||
const { formatPrice, returnAddressPreview } = require('./utils'); | const { formatPrice, returnAddressPreview } = require('./utils'); | ||||
module.exports = { | module.exports = { | ||||
Show All 19 Lines | parseBlock: function (chronikBlockResponse) { | ||||
if (thisParsedTx.tokenSendInfo) { | if (thisParsedTx.tokenSendInfo) { | ||||
tokenIds.add(thisParsedTx.tokenSendInfo.tokenId); | tokenIds.add(thisParsedTx.tokenSendInfo.tokenId); | ||||
} | } | ||||
} | } | ||||
return { hash, height, miner, numTxs, parsedTxs, tokenIds }; | return { hash, height, miner, numTxs, parsedTxs, tokenIds }; | ||||
}, | }, | ||||
getMinerFromCoinbase: function (coinbaseHexString) { | getMinerFromCoinbase: function (coinbaseHexString) { | ||||
const knownMiners = config.knownMiners; | |||||
let miner = 'unknown'; | let miner = 'unknown'; | ||||
// Iterate over known miners to find a match | // Iterate over known miners to find a match | ||||
for (let i = 0; i < knownMiners.length; i += 1) { | for (let i = 0; i < knownMiners.length; i += 1) { | ||||
const testedMiner = knownMiners[i]; | const testedMiner = knownMiners[i]; | ||||
const { coinbaseScript } = testedMiner; | const { coinbaseScript } = testedMiner; | ||||
if (coinbaseHexString.includes(coinbaseScript)) { | if (coinbaseHexString.includes(coinbaseScript)) { | ||||
miner = testedMiner.miner; | miner = testedMiner.miner; | ||||
} | } | ||||
▲ Show 20 Lines • Show All 103 Lines • ▼ Show 20 Lines | parseTx: function (tx) { | ||||
xecReceivingOutputs.set( | xecReceivingOutputs.set( | ||||
thisOutput.outputScript, | thisOutput.outputScript, | ||||
(xecReceivingOutputs.get(thisOutput.outputScript) ?? 0) + | (xecReceivingOutputs.get(thisOutput.outputScript) ?? 0) + | ||||
value, | value, | ||||
); | ); | ||||
} | } | ||||
// Don't parse OP_RETURN values of etoken txs, this info is available from chronik | // Don't parse OP_RETURN values of etoken txs, this info is available from chronik | ||||
if ( | if ( | ||||
thisOutput.outputScript.startsWith( | thisOutput.outputScript.startsWith(opReturn.opReturnPrefix) && | ||||
config.opReturn.opReturnPrefix, | |||||
) && | |||||
!isTokenTx | !isTokenTx | ||||
) { | ) { | ||||
opReturnInfo = module.exports.parseOpReturn( | opReturnInfo = module.exports.parseOpReturn( | ||||
thisOutput.outputScript, | thisOutput.outputScript, | ||||
); | ); | ||||
} | } | ||||
// For etoken send txs, parse outputs for tokenSendInfo object | // For etoken send txs, parse outputs for tokenSendInfo object | ||||
if (typeof thisOutput.slpToken !== 'undefined') { | if (typeof thisOutput.slpToken !== 'undefined') { | ||||
▲ Show 20 Lines • Show All 50 Lines • ▼ Show 20 Lines | parseTx: function (tx) { | ||||
}; | }; | ||||
}, | }, | ||||
parseOpReturn: function (outputScript) { | parseOpReturn: function (outputScript) { | ||||
// Initialize required vars | // Initialize required vars | ||||
let app; | let app; | ||||
let msg; | let msg; | ||||
// Determine if this is an OP_RETURN field | // Determine if this is an OP_RETURN field | ||||
const isOpReturn = | const isOpReturn = outputScript.slice(0, 2) === opReturn.opReturnPrefix; | ||||
outputScript.slice(0, 2) === config.opReturn.opReturnPrefix; | |||||
if (!isOpReturn) { | if (!isOpReturn) { | ||||
return false; | return false; | ||||
} | } | ||||
// Determine if this is a memo tx | // Determine if this is a memo tx | ||||
// Memo txs have a shorter prefix and require special processing | // Memo txs have a shorter prefix and require special processing | ||||
const isMemoTx = | const isMemoTx = outputScript.slice(2, 6) === opReturn.memo.prefix; | ||||
outputScript.slice(2, 6) === config.opReturn.memo.prefix; | |||||
if (isMemoTx) { | if (isMemoTx) { | ||||
// memo txs require special processing | // memo txs require special processing | ||||
// Send the unprocessed remainder of the string to a specialized function | // Send the unprocessed remainder of the string to a specialized function | ||||
return module.exports.parseMemoOutputScript(outputScript); | return module.exports.parseMemoOutputScript(outputScript); | ||||
} | } | ||||
// Parse for app prefix | // Parse for app prefix | ||||
// See https://github.com/Bitcoin-ABC/bitcoin-abc/blob/master/web/standards/op_return-prefix-guideline.md | // See https://github.com/Bitcoin-ABC/bitcoin-abc/blob/master/web/standards/op_return-prefix-guideline.md | ||||
const hasAppPrefix = | const hasAppPrefix = | ||||
outputScript.slice(2, 4) === | outputScript.slice(2, 4) === opReturn.opReturnAppPrefixLength; | ||||
config.opReturn.opReturnAppPrefixLength; | |||||
if (hasAppPrefix) { | if (hasAppPrefix) { | ||||
const appPrefix = outputScript.slice(4, 12); | const appPrefix = outputScript.slice(4, 12); | ||||
if (Object.keys(config.opReturn.appPrefixes).includes(appPrefix)) { | if (Object.keys(opReturn.appPrefixes).includes(appPrefix)) { | ||||
app = config.opReturn.appPrefixes[appPrefix]; | app = opReturn.appPrefixes[appPrefix]; | ||||
} else { | } else { | ||||
app = 'unknown app'; | app = 'unknown app'; | ||||
} | } | ||||
switch (app) { | switch (app) { | ||||
case 'Cashtab Msg': | case 'Cashtab Msg': | ||||
// For a Cashtab msg, the rest of the string will be parsed as an OP_RETURN msg | // For a Cashtab msg, the rest of the string will be parsed as an OP_RETURN msg | ||||
msg = module.exports.hexOpReturnToUtf8( | msg = module.exports.hexOpReturnToUtf8( | ||||
outputScript.slice(12), | outputScript.slice(12), | ||||
Show All 30 Lines | hexOpReturnToUtf8: function (hexStr) { | ||||
* String will have the form of 4c+bytelength+msg (? + 4c + bytelength + msg) | * String will have the form of 4c+bytelength+msg (? + 4c + bytelength + msg) | ||||
*/ | */ | ||||
let hexStrLength = hexStr.length; | let hexStrLength = hexStr.length; | ||||
let opReturnMsgArray = []; | let opReturnMsgArray = []; | ||||
for (let i = 0; hexStrLength !== 0; i++) { | for (let i = 0; hexStrLength !== 0; i++) { | ||||
// Check first byte for the message length or 4c + message length | // Check first byte for the message length or 4c + message length | ||||
let byteValue = hexStr.slice(0, 2); | let byteValue = hexStr.slice(0, 2); | ||||
let msgByteSize = 0; | let msgByteSize = 0; | ||||
if (byteValue === config.opReturn.opPushDataOne) { | if (byteValue === opReturn.opPushDataOne) { | ||||
// If this byte is 4c, then the next byte is the message byte size. | // If this byte is 4c, then the next byte is the message byte size. | ||||
// Retrieve the message byte size and convert from hex to decimal | // Retrieve the message byte size and convert from hex to decimal | ||||
msgByteSize = parseInt(hexStr.substring(2, 4), 16); | msgByteSize = parseInt(hexStr.substring(2, 4), 16); | ||||
// Remove 4c + message byte size info from the beginning of hexStr | // Remove 4c + message byte size info from the beginning of hexStr | ||||
hexStr = hexStr.slice(4); | hexStr = hexStr.slice(4); | ||||
} else { | } else { | ||||
// This byte is the length of an upcoming msg | // This byte is the length of an upcoming msg | ||||
msgByteSize = parseInt(hexStr.substring(0, 2), 16); | msgByteSize = parseInt(hexStr.substring(0, 2), 16); | ||||
Show All 16 Lines | hexOpReturnToUtf8: function (hexStr) { | ||||
} | } | ||||
// If there are multiple messages, for example an unknown prefix, signify this with the | character | // If there are multiple messages, for example an unknown prefix, signify this with the | character | ||||
return opReturnMsgArray.join('|'); | return opReturnMsgArray.join('|'); | ||||
}, | }, | ||||
parseMemoOutputScript: function (memoHexStr) { | parseMemoOutputScript: function (memoHexStr) { | ||||
// Remove the memo prefix, already processed | // Remove the memo prefix, already processed | ||||
memoHexStr = memoHexStr.slice(6); | memoHexStr = memoHexStr.slice(6); | ||||
// At the beginning of this function, we have already popped off '0d6d' | // At the beginning of this function, we have already popped off '0d6d' | ||||
let app = config.opReturn.memo.app; | let app = opReturn.memo.app; | ||||
let msg = ''; | let msg = ''; | ||||
for (let i = 0; memoHexStr !== 0; i++) { | for (let i = 0; memoHexStr !== 0; i++) { | ||||
// Get the memo action code | // Get the memo action code | ||||
// See https://memo.cash/protocol | // See https://memo.cash/protocol | ||||
const actionCode = memoHexStr.slice(0, 2); | const actionCode = memoHexStr.slice(0, 2); | ||||
// Remove actionCode from memoHexStr | // Remove actionCode from memoHexStr | ||||
memoHexStr = memoHexStr.slice(2); | memoHexStr = memoHexStr.slice(2); | ||||
switch (actionCode) { | switch (actionCode) { | ||||
case '01' || '02' || '05' || '0a' || '0c' || '0d' || '0e': | case '01' || '02' || '05' || '0a' || '0c' || '0d' || '0e': | ||||
// Action codes where the entire string may be parsed to utf8 | // Action codes where the entire string may be parsed to utf8 | ||||
msg += `${ | msg += `${ | ||||
config.opReturn.memo[actionCode] | opReturn.memo[actionCode] | ||||
}|${module.exports.hexOpReturnToUtf8(memoHexStr)}`; | }|${module.exports.hexOpReturnToUtf8(memoHexStr)}`; | ||||
break; | break; | ||||
default: | default: | ||||
// parse the rest of the string like a normal op_return utf8 string | // parse the rest of the string like a normal op_return utf8 string | ||||
msg += `${ | msg += `${ | ||||
Object.keys(config.opReturn.memo).includes(actionCode) | Object.keys(opReturn.memo).includes(actionCode) | ||||
? config.opReturn.memo[actionCode] | ? opReturn.memo[actionCode] | ||||
: '' | : '' | ||||
}|${module.exports.hexOpReturnToUtf8(memoHexStr)}`; | }|${module.exports.hexOpReturnToUtf8(memoHexStr)}`; | ||||
} | } | ||||
return { app, msg }; | return { app, msg }; | ||||
} | } | ||||
}, | }, | ||||
getBlockTgMessage: function (parsedBlock, coingeckoPrices, tokenInfoMap) { | getBlockTgMessage: function (parsedBlock, coingeckoPrices, tokenInfoMap) { | ||||
const { hash, height, miner, numTxs, parsedTxs } = parsedBlock; | const { hash, height, miner, numTxs, parsedTxs } = parsedBlock; | ||||
▲ Show 20 Lines • Show All 156 Lines • ▼ Show 20 Lines | getBlockTgMessage: function (parsedBlock, coingeckoPrices, tokenInfoMap) { | ||||
// Convert sats to XEC. Round as decimals will not be rendered in msgs. | // Convert sats to XEC. Round as decimals will not be rendered in msgs. | ||||
const totalXecSent = parseFloat((totalSatsSent / 100).toFixed(0)); | const totalXecSent = parseFloat((totalSatsSent / 100).toFixed(0)); | ||||
// Clone xecReceivingOutputs so that you don't modify unit test mocks | // Clone xecReceivingOutputs so that you don't modify unit test mocks | ||||
let xecReceivingAddressOutputs = new Map(xecReceivingOutputs); | let xecReceivingAddressOutputs = new Map(xecReceivingOutputs); | ||||
// Throw out OP_RETURN outputs for txs parsed as XEC send txs | // Throw out OP_RETURN outputs for txs parsed as XEC send txs | ||||
xecReceivingAddressOutputs.forEach((value, key, map) => { | xecReceivingAddressOutputs.forEach((value, key, map) => { | ||||
if (key.startsWith(config.opReturn.opReturnPrefix)) { | if (key.startsWith(opReturn.opReturnPrefix)) { | ||||
map.delete(key); | map.delete(key); | ||||
} | } | ||||
}); | }); | ||||
let xecSendMsg; | let xecSendMsg; | ||||
if (xecReceivingAddressOutputs.size === 0) { | if (xecReceivingAddressOutputs.size === 0) { | ||||
// self send tx | // self send tx | ||||
let changeAmountSats = 0; | let changeAmountSats = 0; | ||||
▲ Show 20 Lines • Show All 135 Lines • Show Last 20 Lines |