Changeset View
Changeset View
Standalone View
Standalone View
web/cashtab/src/utils/chronik.js
// Chronik methods | // Chronik methods | ||||
import BigNumber from 'bignumber.js'; | import BigNumber from 'bignumber.js'; | ||||
import { currency } from 'components/Common/Ticker'; | import { currency } from 'components/Common/Ticker'; | ||||
import { parseOpReturn } from 'utils/cashMethods'; | import { | ||||
parseOpReturn, | |||||
convertToEncryptStruct, | |||||
getHashArrayFromWallet, | |||||
getUtxoWif, | |||||
} from 'utils/cashMethods'; | |||||
import ecies from 'ecies-lite'; | |||||
import wif from 'wif'; | |||||
// Return false if do not get a valid response | // Return false if do not get a valid response | ||||
export const getTokenStats = async (chronik, tokenId) => { | export const getTokenStats = async (chronik, tokenId) => { | ||||
try { | try { | ||||
// token attributes available via chronik's token() method | // token attributes available via chronik's token() method | ||||
let tokenResponseObj = await chronik.token(tokenId); | let tokenResponseObj = await chronik.token(tokenId); | ||||
const tokenDecimals = tokenResponseObj.slpTxData.genesisInfo.decimals; | const tokenDecimals = tokenResponseObj.slpTxData.genesisInfo.decimals; | ||||
▲ Show 20 Lines • Show All 398 Lines • ▼ Show 20 Lines | return new Promise((resolve, reject) => { | ||||
}, | }, | ||||
err => { | err => { | ||||
reject(err); | reject(err); | ||||
}, | }, | ||||
); | ); | ||||
}); | }); | ||||
}; | }; | ||||
export const parseChronikTx = (tx, walletHash160s) => { | export const parseChronikTx = (tx, wallet) => { | ||||
const walletHash160s = getHashArrayFromWallet(wallet); | |||||
const { inputs, outputs } = tx; | const { inputs, outputs } = tx; | ||||
// Assign defaults | // Assign defaults | ||||
let incoming = true; | let incoming = true; | ||||
let xecAmount = new BigNumber(0); | let xecAmount = new BigNumber(0); | ||||
let originatingHash160 = ''; | let originatingHash160 = ''; | ||||
let etokenAmount = new BigNumber(0); | let etokenAmount = new BigNumber(0); | ||||
const isEtokenTx = 'slpTxData' in tx && typeof tx.slpTxData !== 'undefined'; | const isEtokenTx = 'slpTxData' in tx && typeof tx.slpTxData !== 'undefined'; | ||||
// Defining variables used in lines legacy parseTxData function from useBCH.js | |||||
let substring = ''; | |||||
let airdropFlag = false; | |||||
let airdropTokenId = ''; | |||||
let opReturnMessage = ''; | |||||
let isCashtabMessage = false; | |||||
let isEncryptedMessage = false; | |||||
let decryptionSuccess = false; | |||||
// Iterate over inputs to see if this is an incoming tx (incoming === true) | // Iterate over inputs to see if this is an incoming tx (incoming === true) | ||||
for (let i = 0; i < inputs.length; i += 1) { | for (let i = 0; i < inputs.length; i += 1) { | ||||
const thisInput = inputs[i]; | const thisInput = inputs[i]; | ||||
const thisInputSendingHash160 = thisInput.outputScript; | const thisInputSendingHash160 = thisInput.outputScript; | ||||
/* | /* | ||||
Assume the first input is the originating address | Assume the first input is the originating address | ||||
Show All 32 Lines | for (let i = 0; i < outputs.length; i += 1) { | ||||
const thisOutputReceivedAtHash160 = thisOutput.outputScript; | const thisOutputReceivedAtHash160 = thisOutput.outputScript; | ||||
// Check for OP_RETURN msg | // Check for OP_RETURN msg | ||||
if ( | if ( | ||||
thisOutput.value === '0' && | thisOutput.value === '0' && | ||||
typeof thisOutput.slpToken === 'undefined' | typeof thisOutput.slpToken === 'undefined' | ||||
) { | ) { | ||||
let hex = thisOutputReceivedAtHash160; | let hex = thisOutputReceivedAtHash160; | ||||
let parsedOpReturnArray = parseOpReturn(hex); | let parsedOpReturnArray = parseOpReturn(hex); | ||||
console.log(`parsedOpReturnArray`, parsedOpReturnArray); | |||||
// Exactly copying lines 177-293 of useBCH.js | |||||
// Differences | |||||
// 1 - patched ecies not async error | |||||
// 2 - Removed if loop for tx being token, as this is handled elsewhere here | |||||
if (!parsedOpReturnArray) { | |||||
console.log( | |||||
'useBCH.parsedTxData() error: parsed array is empty', | |||||
); | |||||
break; | |||||
} | |||||
let message = ''; | |||||
let txType = parsedOpReturnArray[0]; | |||||
if (txType === currency.opReturn.appPrefixesHex.airdrop) { | |||||
// this is to facilitate special Cashtab-specific cases of airdrop txs, both with and without msgs | |||||
// The UI via Tx.js can check this airdropFlag attribute in the parsedTx object to conditionally render airdrop-specific formatting if it's true | |||||
airdropFlag = true; | |||||
// index 0 is drop prefix, 1 is the token Id, 2 is msg prefix, 3 is msg | |||||
airdropTokenId = parsedOpReturnArray[1]; | |||||
txType = parsedOpReturnArray[2]; | |||||
// remove the first two elements of airdrop prefix and token id from array so the array parsing logic below can remain unchanged | |||||
parsedOpReturnArray.splice(0, 2); | |||||
// index 0 now becomes msg prefix, 1 becomes the msg | |||||
} | |||||
if (txType === currency.opReturn.appPrefixesHex.cashtab) { | |||||
// this is a Cashtab message | |||||
try { | |||||
opReturnMessage = Buffer.from( | |||||
parsedOpReturnArray[1], | |||||
'hex', | |||||
); | |||||
isCashtabMessage = true; | |||||
} catch (err) { | |||||
// soft error if an unexpected or invalid cashtab hex is encountered | |||||
opReturnMessage = ''; | |||||
console.log( | |||||
'useBCH.parsedTxData() error: invalid cashtab msg hex: ' + | |||||
parsedOpReturnArray[1], | |||||
); | |||||
} | |||||
} else if ( | |||||
txType === currency.opReturn.appPrefixesHex.cashtabEncrypted | |||||
) { | |||||
// this is an encrypted Cashtab message | |||||
let msgString = parsedOpReturnArray[1]; | |||||
let fundingWif, privateKeyObj, privateKeyBuff; | |||||
if ( | |||||
wallet && | |||||
wallet.state && | |||||
wallet.state.slpBalancesAndUtxos && | |||||
wallet.state.slpBalancesAndUtxos.nonSlpUtxos[0] | |||||
) { | |||||
fundingWif = getUtxoWif( | |||||
wallet.state.slpBalancesAndUtxos.nonSlpUtxos[0], | |||||
wallet, | |||||
); | |||||
privateKeyObj = wif.decode(fundingWif); | |||||
privateKeyBuff = privateKeyObj.privateKey; | |||||
if (!privateKeyBuff) { | |||||
throw new Error('Private key extraction error'); | |||||
} | |||||
} else { | |||||
break; | |||||
} | |||||
let structData; | |||||
let decryptedMessage; | |||||
try { | |||||
// Convert the hex encoded message to a buffer | |||||
const msgBuf = Buffer.from(msgString, 'hex'); | |||||
// Convert the bufer into a structured object. | |||||
structData = convertToEncryptStruct(msgBuf); | |||||
decryptedMessage = ecies.decrypt( | |||||
privateKeyBuff, | |||||
structData, | |||||
); | |||||
decryptionSuccess = true; | |||||
} catch (err) { | |||||
console.log( | |||||
'useBCH.parsedTxData() decryption error: ' + err, | |||||
); | |||||
decryptedMessage = | |||||
'Only the message recipient can view this'; | |||||
} | |||||
isCashtabMessage = true; | |||||
isEncryptedMessage = true; | |||||
opReturnMessage = decryptedMessage; | |||||
} else { | |||||
// this is an externally generated message | |||||
message = txType; // index 0 is the message content in this instance | |||||
// if there are more than one part to the external message | |||||
const arrayLength = parsedOpReturnArray.length; | |||||
for (let i = 1; i < arrayLength; i++) { | |||||
message = message + parsedOpReturnArray[i]; | |||||
} | |||||
try { | |||||
opReturnMessage = Buffer.from(message, 'hex'); | |||||
} catch (err) { | |||||
// soft error if an unexpected or invalid cashtab hex is encountered | |||||
opReturnMessage = ''; | |||||
console.log( | |||||
'useBCH.parsedTxData() error: invalid external msg hex: ' + | |||||
substring, | |||||
); | |||||
} | |||||
} | |||||
} | } | ||||
// Find amounts at your wallet's addresses | // Find amounts at your wallet's addresses | ||||
for (let j = 0; j < walletHash160s.length; j += 1) { | for (let j = 0; j < walletHash160s.length; j += 1) { | ||||
const thisWalletHash160 = walletHash160s[j]; | const thisWalletHash160 = walletHash160s[j]; | ||||
if (thisOutputReceivedAtHash160.includes(thisWalletHash160)) { | if (thisOutputReceivedAtHash160.includes(thisWalletHash160)) { | ||||
// If incoming tx, this is amount received by the user's wallet | // If incoming tx, this is amount received by the user's wallet | ||||
// if outgoing tx (incoming === false), then this is a change amount | // if outgoing tx (incoming === false), then this is a change amount | ||||
const thisOutputAmount = new BigNumber(thisOutput.value); | const thisOutputAmount = new BigNumber(thisOutput.value); | ||||
Show All 39 Lines | export const parseChronikTx = (tx, wallet) => { | ||||
// Convert from sats to XEC | // Convert from sats to XEC | ||||
xecAmount = xecAmount.shiftedBy(-1 * currency.cashDecimals); | xecAmount = xecAmount.shiftedBy(-1 * currency.cashDecimals); | ||||
// Convert from BigNumber to string | // Convert from BigNumber to string | ||||
xecAmount = xecAmount.toString(); | xecAmount = xecAmount.toString(); | ||||
etokenAmount = etokenAmount.toString(); | etokenAmount = etokenAmount.toString(); | ||||
// Convert opReturnMessage to string | |||||
opReturnMessage = Buffer.from(opReturnMessage).toString(); | |||||
// Return eToken specific fields if eToken tx | // Return eToken specific fields if eToken tx | ||||
if (isEtokenTx) { | if (isEtokenTx) { | ||||
const { slpMeta } = tx.slpTxData; | const { slpMeta } = tx.slpTxData; | ||||
return { | return { | ||||
incoming, | incoming, | ||||
xecAmount, | xecAmount, | ||||
originatingHash160, | originatingHash160, | ||||
isEtokenTx, | isEtokenTx, | ||||
etokenAmount, | etokenAmount, | ||||
slpMeta, | slpMeta, | ||||
legacy: { | |||||
amountSent: incoming ? 0 : xecAmount, | |||||
amountReceived: incoming ? xecAmount : 0, | |||||
outgoingTx: !incoming, | |||||
tokenTx: true, | |||||
airdropFlag, | |||||
airdropTokenId, | |||||
opReturnMessage: '', | |||||
isCashtabMessage, | |||||
isEncryptedMessage, | |||||
decryptionSuccess, | |||||
}, | |||||
}; | }; | ||||
} | } | ||||
// Otherwise do not include these fields | // Otherwise do not include these fields | ||||
return { incoming, xecAmount, originatingHash160, isEtokenTx }; | return { | ||||
incoming, | |||||
xecAmount, | |||||
originatingHash160, | |||||
isEtokenTx, | |||||
legacy: { | |||||
amountSent: incoming ? 0 : xecAmount, | |||||
amountReceived: incoming ? xecAmount : 0, | |||||
outgoingTx: !incoming, | |||||
tokenTx: false, | |||||
airdropFlag, | |||||
airdropTokenId, | |||||
opReturnMessage, | |||||
isCashtabMessage, | |||||
isEncryptedMessage, | |||||
decryptionSuccess, | |||||
}, | |||||
}; | |||||
}; | }; | ||||
export const getTxHistoryChronik = async ( | export const getTxHistoryChronik = async (chronik, wallet) => { | ||||
chronik, | |||||
hash160AndAddressObjArray, | |||||
) => { | |||||
// Create array of promises to get chronik history for each address | // Create array of promises to get chronik history for each address | ||||
// Combine them all and sort by blockheight and firstSeen | // Combine them all and sort by blockheight and firstSeen | ||||
// Add all the info cashtab needs to make them useful | // Add all the info cashtab needs to make them useful | ||||
const hash160AndAddressObjArray = [ | |||||
{ | |||||
address: wallet.Path145.cashAddress, | |||||
hash160: wallet.Path145.hash160, | |||||
}, | |||||
{ | |||||
address: wallet.Path245.cashAddress, | |||||
hash160: wallet.Path245.hash160, | |||||
}, | |||||
{ | |||||
address: wallet.Path1899.cashAddress, | |||||
hash160: wallet.Path1899.hash160, | |||||
}, | |||||
]; | |||||
let txHistoryPromises = []; | let txHistoryPromises = []; | ||||
for (let i = 0; i < hash160AndAddressObjArray.length; i += 1) { | for (let i = 0; i < hash160AndAddressObjArray.length; i += 1) { | ||||
const txHistoryPromise = returnGetTxHistoryChronikPromise( | const txHistoryPromise = returnGetTxHistoryChronikPromise( | ||||
chronik, | chronik, | ||||
hash160AndAddressObjArray[i], | hash160AndAddressObjArray[i], | ||||
); | ); | ||||
txHistoryPromises.push(txHistoryPromise); | txHistoryPromises.push(txHistoryPromise); | ||||
} | } | ||||
let txHistoryOfAllAddresses; | let txHistoryOfAllAddresses; | ||||
try { | try { | ||||
txHistoryOfAllAddresses = await Promise.all(txHistoryPromises); | txHistoryOfAllAddresses = await Promise.all(txHistoryPromises); | ||||
} catch (err) { | } catch (err) { | ||||
console.log(`Error in Promise.all(txHistoryPromises)`, err); | console.log(`Error in Promise.all(txHistoryPromises)`, err); | ||||
} | } | ||||
console.log(`txHistoryOfAllAddresses`, txHistoryOfAllAddresses); | console.log(`txHistoryOfAllAddresses`, txHistoryOfAllAddresses); | ||||
const flatTxHistoryArray = flattenChronikTxHistory(txHistoryOfAllAddresses); | const flatTxHistoryArray = flattenChronikTxHistory(txHistoryOfAllAddresses); | ||||
console.log(`flatTxHistoryArray`, flatTxHistoryArray); | console.log(`flatTxHistoryArray`, flatTxHistoryArray); | ||||
const sortedTxHistoryArray = sortAndTrimChronikTxHistory( | const sortedTxHistoryArray = sortAndTrimChronikTxHistory( | ||||
flatTxHistoryArray, | flatTxHistoryArray, | ||||
currency.txHistoryCount, | currency.txHistoryCount, | ||||
); | ); | ||||
// Get hash160 array | |||||
const hash160array = []; | |||||
for (let i = 0; i < hash160AndAddressObjArray.length; i += 1) { | |||||
hash160array.push(hash160AndAddressObjArray[i].hash160); | |||||
} | |||||
// Parse txs | // Parse txs | ||||
const parsedTxs = []; | const parsedTxs = []; | ||||
for (let i = 0; i < sortedTxHistoryArray.length; i += 1) { | for (let i = 0; i < sortedTxHistoryArray.length; i += 1) { | ||||
const sortedTx = sortedTxHistoryArray[i]; | const sortedTx = sortedTxHistoryArray[i]; | ||||
sortedTx.parsed = parseChronikTx(sortedTx, hash160array); | sortedTx.parsed = parseChronikTx(sortedTx, wallet); | ||||
parsedTxs.push(sortedTx); | parsedTxs.push(sortedTx); | ||||
} | } | ||||
return parsedTxs; | return parsedTxs; | ||||
}; | }; |