diff --git a/web/cashtab/src/components/Common/Ticker.js b/web/cashtab/src/components/Common/Ticker.js index 509be36bb..ea4d4a5e3 100644 --- a/web/cashtab/src/components/Common/Ticker.js +++ b/web/cashtab/src/components/Common/Ticker.js @@ -1,202 +1,260 @@ import mainLogo from '@assets/logo_primary.png'; import tokenLogo from '@assets/logo_secondary.png'; import cashaddr from 'ecashaddrjs'; import BigNumber from 'bignumber.js'; export const currency = { name: 'eCash', ticker: 'XEC', appUrl: 'cashtab.com', logo: mainLogo, legacyPrefix: 'bitcoincash', prefixes: ['ecash'], coingeckoId: 'ecash', defaultFee: 2.01, dustSats: 550, etokenSats: 546, cashDecimals: 2, blockExplorerUrl: 'https://explorer.bitcoinabc.org', tokenExplorerUrl: 'https://explorer.be.cash', blockExplorerUrlTestnet: 'https://texplorer.bitcoinabc.org', tokenName: 'eToken', tokenTicker: 'eToken', tokenIconSubmitApi: 'https://icons.etokens.cash/new', tokenLogo: tokenLogo, tokenPrefixes: ['etoken'], tokenIconsUrl: 'https://etoken-icons.s3.us-west-2.amazonaws.com/32', txHistoryCount: 5, hydrateUtxoBatchSize: 20, defaultSettings: { fiatCurrency: 'usd' }, notificationDurationShort: 3, notificationDurationLong: 5, newTokenDefaultUrl: 'https://cashtab.com/', + opReturn: { + opReturnPrefixHex: '6a', + opReturnPushDataHex: '04', + opReturnAppPrefixLengthHex: '04', + appPrefixesHex: { + eToken: '534c5000', + cashtab: '00746162', + }, + }, settingsValidation: { fiatCurrency: [ 'usd', 'idr', 'krw', 'cny', 'zar', 'vnd', 'cad', 'nok', 'eur', 'gbp', 'jpy', 'try', 'rub', 'inr', 'brl', 'php', 'ils', 'clp', ], }, fiatCurrencies: { usd: { name: 'US Dollar', symbol: '$', slug: 'usd' }, brl: { name: 'Brazilian Real', symbol: 'R$', slug: 'brl' }, gbp: { name: 'British Pound', symbol: '£', slug: 'gbp' }, cad: { name: 'Canadian Dollar', symbol: '$', slug: 'cad' }, clp: { name: 'Chilean Peso', symbol: '$', slug: 'clp' }, cny: { name: 'Chinese Yuan', symbol: '元', slug: 'cny' }, eur: { name: 'Euro', symbol: '€', slug: 'eur' }, inr: { name: 'Indian Rupee', symbol: '₹', slug: 'inr' }, idr: { name: 'Indonesian Rupiah', symbol: 'Rp', slug: 'idr' }, ils: { name: 'Israeli Shekel', symbol: '₪', slug: 'ils' }, jpy: { name: 'Japanese Yen', symbol: '¥', slug: 'jpy' }, krw: { name: 'Korean Won', symbol: '₩', slug: 'krw' }, nok: { name: 'Norwegian Krone', symbol: 'kr', slug: 'nok' }, php: { name: 'Philippine Peso', symbol: '₱', slug: 'php' }, rub: { name: 'Russian Ruble', symbol: 'р.', slug: 'rub' }, zar: { name: 'South African Rand', symbol: 'R', slug: 'zar' }, try: { name: 'Turkish Lira', symbol: '₺', slug: 'try' }, vnd: { name: 'Vietnamese đồng', symbol: 'đ', slug: 'vnd' }, }, }; +export function getETokenEncodingSubstring() { + let encodingStr = + currency.opReturn.opReturnPrefixHex + // 6a + currency.opReturn.opReturnAppPrefixLengthHex + // 04 + currency.opReturn.appPrefixesHex.eToken; // 534c5000 + + return encodingStr; +} + +export function getCashtabEncodingSubstring() { + let encodingStr = + currency.opReturn.opReturnPrefixHex + // 6a + currency.opReturn.opReturnAppPrefixLengthHex + // 04 + currency.opReturn.appPrefixesHex.cashtab; // 00746162 + + return encodingStr; +} + +export function isCashtabOutput(hexStr) { + if (!hexStr || typeof hexStr !== 'string') { + return false; + } + return hexStr.startsWith(getCashtabEncodingSubstring()); +} + +export function isEtokenOutput(hexStr) { + if (!hexStr || typeof hexStr !== 'string') { + return false; + } + return hexStr.startsWith(getETokenEncodingSubstring()); +} + +export function extractCashtabMessage(hexSubstring) { + if (!hexSubstring || typeof hexSubstring !== 'string') { + return ''; + } + let substring = hexSubstring.replace(getCashtabEncodingSubstring(), ''); // remove the cashtab encoding + substring = substring.slice(2); // remove the 2 bytes indicating the size of the next element on the stack e.g. a0 -> 160 bytes + return substring; +} + +export function extractExternalMessage(hexSubstring) { + if (!hexSubstring || typeof hexSubstring !== 'string') { + return ''; + } + let substring = hexSubstring.slice(4); // remove the preceding OP_RETURN prefixes + return substring; +} + export function isValidCashPrefix(addressString) { // Note that this function validates prefix only // Check for prefix included in currency.prefixes array // For now, validation is handled by converting to bitcoincash: prefix and checksum // and relying on legacy validation methods of bitcoincash: prefix addresses // Also accept an address with no prefix, as some exchanges provide these for (let i = 0; i < currency.prefixes.length; i += 1) { // If the addressString being tested starts with an accepted prefix or no prefix at all if ( addressString.startsWith(currency.prefixes[i] + ':') || !addressString.includes(':') ) { return true; } } return false; } export function isValidTokenPrefix(addressString) { // Check for prefix included in currency.tokenPrefixes array // For now, validation is handled by converting to simpleledger: prefix and checksum // and relying on legacy validation methods of simpleledger: prefix addresses // For token addresses, do not accept an address with no prefix for (let i = 0; i < currency.tokenPrefixes.length; i += 1) { if (addressString.startsWith(currency.tokenPrefixes[i] + ':')) { return true; } } return false; } export function toLegacy(address) { let testedAddress; let legacyAddress; try { if (isValidCashPrefix(address)) { // Prefix-less addresses may be valid, but the cashaddr.decode function used below // will throw an error without a prefix. Hence, must ensure prefix to use that function. const hasPrefix = address.includes(':'); if (!hasPrefix) { testedAddress = currency.legacyPrefix + ':' + address; } else { testedAddress = address; } // Note: an `ecash:` checksum address with no prefix will not be validated by // parseAddress in Send.js // Only handle the case of prefixless address that is valid `bitcoincash:` address const { type, hash } = cashaddr.decode(testedAddress); legacyAddress = cashaddr.encode(currency.legacyPrefix, type, hash); } else { console.log(`Error: ${address} is not a cash address`); throw new Error( 'Address prefix is not a valid cash address with a prefix from the Ticker.prefixes array', ); } } catch (err) { return err; } return legacyAddress; } export function parseAddress(BCH, addressString, isToken = false) { // Build return obj const addressInfo = { address: '', isValid: false, queryString: null, amount: null, }; // Parse address string for parameters const paramCheck = addressString.split('?'); let cleanAddress = paramCheck[0]; addressInfo.address = cleanAddress; // Validate address let isValidAddress; try { isValidAddress = BCH.Address.isCashAddress(cleanAddress); // Only accept addresses with ecash: prefix const { prefix } = cashaddr.decode(cleanAddress); // If the address does not have a valid prefix or token prefix if ( (!isToken && !currency.prefixes.includes(prefix)) || (isToken && !currency.tokenPrefixes.includes(prefix)) ) { // then it is not a valid destination address for XEC sends isValidAddress = false; } } catch (err) { isValidAddress = false; } addressInfo.isValid = isValidAddress; // Check for parameters // only the amount param is currently supported let queryString = null; let amount = null; if (paramCheck.length > 1) { queryString = paramCheck[1]; addressInfo.queryString = queryString; const addrParams = new URLSearchParams(queryString); if (addrParams.has('amount')) { // Amount in satoshis try { amount = new BigNumber(parseInt(addrParams.get('amount'))) .div(10 ** currency.cashDecimals) .toString(); } catch (err) { amount = null; } } } addressInfo.amount = amount; return addressInfo; } diff --git a/web/cashtab/src/components/Common/__tests__/Ticker.test.js b/web/cashtab/src/components/Common/__tests__/Ticker.test.js index 9d7aa3cba..97f978f22 100644 --- a/web/cashtab/src/components/Common/__tests__/Ticker.test.js +++ b/web/cashtab/src/components/Common/__tests__/Ticker.test.js @@ -1,108 +1,191 @@ import { ValidationError } from 'ecashaddrjs'; -import { isValidCashPrefix, isValidTokenPrefix, toLegacy } from '../Ticker'; +import { + isValidCashPrefix, + isValidTokenPrefix, + toLegacy, + isCashtabOutput, + isEtokenOutput, + extractCashtabMessage, + extractExternalMessage, + getETokenEncodingSubstring, + getCashtabEncodingSubstring, +} from '../Ticker'; test('Rejects cash address with bitcoincash: prefix', async () => { const result = isValidCashPrefix( 'bitcoincash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gjykk3wa0', ); expect(result).toStrictEqual(false); }); test('Correctly validates cash address with bitcoincash: checksum but no prefix', async () => { const result = isValidCashPrefix( 'qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', ); expect(result).toStrictEqual(true); }); test('Correctly validates cash address with ecash: checksum but no prefix', async () => { const result = isValidCashPrefix( 'qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc', ); expect(result).toStrictEqual(true); }); test('Correctly validates cash address with ecash: prefix', async () => { const result = isValidCashPrefix( 'ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc', ); expect(result).toStrictEqual(true); }); test('Rejects token address with simpleledger: prefix', async () => { const result = isValidTokenPrefix( 'simpleledger:qpmytrdsakt0axrrlswvaj069nat3p9s7c8w5tu8gm', ); expect(result).toStrictEqual(false); }); test('Does not accept a valid token address without a prefix', async () => { const result = isValidTokenPrefix( 'qpmytrdsakt0axrrlswvaj069nat3p9s7c8w5tu8gm', ); expect(result).toStrictEqual(false); }); test('Correctly validates token address with etoken: prefix (prefix only, not checksum)', async () => { const result = isValidTokenPrefix( 'etoken:qpmytrdsakt0axrrlswvaj069nat3p9s7c8w5tu8gm', ); expect(result).toStrictEqual(true); }); test('Recognizes unaccepted token prefix (prefix only, not checksum)', async () => { const result = isValidTokenPrefix( 'wtftoken:qpmytrdsakt0axrrlswvaj069nat3p9s7c8w5tu8gm', ); expect(result).toStrictEqual(false); }); test('Knows that acceptable cash prefixes are not tokens', async () => { const result = isValidTokenPrefix( 'ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc', ); expect(result).toStrictEqual(false); }); test('Address with unlisted prefix is invalid', async () => { const result = isValidCashPrefix( 'ecashdoge:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc', ); expect(result).toStrictEqual(false); }); test('toLegacy() converts a valid ecash: prefix address to a valid bitcoincash: prefix address', async () => { const result = toLegacy('ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc'); expect(result).toStrictEqual( 'bitcoincash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gjykk3wa0', ); }); test('toLegacy() accepts a valid BCH address with no prefix and returns with prefix', async () => { const result = toLegacy('qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gjykk3wa0'); expect(result).toStrictEqual( 'bitcoincash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gjykk3wa0', ); }); test('toLegacy throws error if input address has invalid checksum', async () => { const result = toLegacy('ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25m'); expect(result).toStrictEqual( new ValidationError( 'Invalid checksum: ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25m.', ), ); }); test('toLegacy throws error if input address has invalid prefix', async () => { const result = toLegacy( 'notecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc', ); expect(result).toStrictEqual( new Error( 'Address prefix is not a valid cash address with a prefix from the Ticker.prefixes array', ), ); }); + +test('getCashtabEncodingSubstring() returns the appropriate substring for cashtab message outputs', async () => { + const result = getCashtabEncodingSubstring(); + expect(result).toStrictEqual('6a0400746162'); +}); + +test('getETokenEncodingSubstring() returns the appropriate substring for eToken outputs', async () => { + const result = getETokenEncodingSubstring(); + expect(result).toStrictEqual('6a04534c5000'); +}); + +test('isCashtabOutput() correctly validates a cashtab message output hex', async () => { + const result = isCashtabOutput('6a04007461620b63617368746162756c6172'); + expect(result).toStrictEqual(true); +}); + +test('isCashtabOutput() correctly invalidates an external message output hex', async () => { + const result = isCashtabOutput('6a0c7069616e6f74656e6e697332'); + expect(result).toStrictEqual(false); +}); + +test('isCashtabOutput() correctly handles null input', async () => { + const result = isCashtabOutput(null); + expect(result).toStrictEqual(false); +}); + +test('isCashtabOutput() correctly handles non-string input', async () => { + const result = isCashtabOutput(7623723323); + expect(result).toStrictEqual(false); +}); + +test('isCashtabOutput() correctly invalidates an external message output hex', async () => { + const result = isCashtabOutput( + '6a202731afddf3b83747943f0e650b938ea0670dcae2e08c415f53bd4c6acfd15e09', + ); + expect(result).toStrictEqual(false); +}); + +test('isEtokenOutput() correctly validates an eToken output hex', async () => { + const result = isEtokenOutput( + '6a04534c500001010453454e442069b8431ddecf775393b1b36aa1d0ddcd7b342f1157b9671a03747378ed35ea0d08000000000000012c080000000000002008', + ); + expect(result).toStrictEqual(true); +}); + +test('isEtokenOutput() correctly invalidates an eToken output hex', async () => { + const result = isEtokenOutput( + '5434c500001010453454e442069b8431ddecf775393b1b36aa1d0ddcd7b342f1157b9671a03747378ed35ea0d08000000000000012c080000000000002008', + ); + expect(result).toStrictEqual(false); +}); + +test('isEtokenOutput() correctly handles null input', async () => { + const result = isEtokenOutput(null); + expect(result).toStrictEqual(false); +}); + +test('isEtokenOutput() correctly handles non-string input', async () => { + const result = isEtokenOutput(7623723323); + expect(result).toStrictEqual(false); +}); + +test('extractCashtabMessage() correctly extracts a Cashtab message', async () => { + const result = extractCashtabMessage( + '6a04007461620b63617368746162756c6172', + ); + expect(result).toStrictEqual('63617368746162756c6172'); +}); + +test('extractExternalMessage() correctly extracts an external message', async () => { + const result = extractExternalMessage('6a0d62696e676f656c65637472756d'); + expect(result).toStrictEqual('62696e676f656c65637472756d'); +}); diff --git a/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap index 1f2ac3565..797543c54 100644 --- a/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap +++ b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap @@ -1,2199 +1,2199 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances 1`] = ` Array [
You currently have 0 XEC
Deposit some funds to use this feature
,
XEC
max
= $ NaN USD
Advanced
,
Signatures
,
Sign Message
, ] `; exports[`Wallet with BCH balances and tokens 1`] = ` Array [
You currently have 0 XEC
Deposit some funds to use this feature
,
XEC
max
= $ NaN USD
Advanced
,
Signatures
,
Sign Message
, ] `; exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [
0.06 XEC
,
$ NaN USD
,
XEC
max
= $ NaN USD
Advanced
,
Signatures
,
Sign Message
, ] `; exports[`Wallet without BCH balance 1`] = ` Array [
You currently have 0 XEC
Deposit some funds to use this feature
,
XEC
max
= $ NaN USD
Advanced
,
Signatures
,
Sign Message
, ] `; exports[`Without wallet defined 1`] = ` Array [
You currently have 0 XEC
Deposit some funds to use this feature
,
XEC
max
= $ NaN USD
Advanced
,
Signatures
,
Sign Message
, ] `; diff --git a/web/cashtab/src/components/Wallet/Tx.js b/web/cashtab/src/components/Wallet/Tx.js index d3e88ee33..97949c4b0 100644 --- a/web/cashtab/src/components/Wallet/Tx.js +++ b/web/cashtab/src/components/Wallet/Tx.js @@ -1,404 +1,423 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; import { ArrowUpOutlined, ArrowDownOutlined, ExperimentOutlined, ExclamationOutlined, } from '@ant-design/icons'; import { currency } from '@components/Common/Ticker'; import makeBlockie from 'ethereum-blockies-base64'; import { Img } from 'react-image'; import { formatBalance, fromLegacyDecimals } from '@utils/cashMethods'; const SentTx = styled(ArrowUpOutlined)` color: ${props => props.theme.secondary} !important; `; const ReceivedTx = styled(ArrowDownOutlined)` color: ${props => props.theme.primary} !important; `; const GenesisTx = styled(ExperimentOutlined)` color: ${props => props.theme.primary} !important; `; const UnparsedTx = styled(ExclamationOutlined)` color: ${props => props.theme.primary} !important; `; const DateType = styled.div` text-align: left; padding: 12px; @media screen and (max-width: 500px) { font-size: 0.8rem; } `; -const OpReturnType = styled.div` +const OpReturnType = styled.span` text-align: left; width: 300%; - max-height: 130px; + max-height: 170px; padding: 3px; - padding-left: 14px; - padding-right: 25px; + margin: auto; word-break: break-word; - overflow: hidden; - text-overflow: ellipsis; + padding-left: 13px; + padding-right: 30px; `; const SentLabel = styled.span` font-weight: bold; color: ${props => props.theme.secondary} !important; `; const ReceivedLabel = styled.span` font-weight: bold; color: ${props => props.theme.primary} !important; `; +const CashtabMessageLabel = styled.span` + text-align: left; + font-weight: bold; + color: ${props => props.theme.primary} !important; + white-space: nowrap; +`; +const MessageLabel = styled.span` + text-align: left; + font-weight: bold; + color: ${props => props.theme.secondary} !important; + white-space: nowrap; +`; const TxIcon = styled.div` svg { width: 32px; height: 32px; } height: 32px; width: 32px; @media screen and (max-width: 500px) { svg { width: 24px; height: 24px; } height: 24px; width: 24px; } `; const TxInfo = styled.div` padding: 12px; font-size: 1rem; text-align: right; color: ${props => props.outgoing ? props.theme.secondary : props.theme.primary}; @media screen and (max-width: 500px) { font-size: 0.8rem; } `; const TxFiatPrice = styled.span` font-size: 0.8rem; `; const TokenInfo = styled.div` display: grid; grid-template-rows: 50%; grid-template-columns: 24px auto; padding: 12px; font-size: 1rem; color: ${props => props.outgoing ? props.theme.secondary : props.theme.primary}; @media screen and (max-width: 500px) { font-size: 0.8rem; grid-template-columns: 16px auto; } `; const TxTokenIcon = styled.div` img { height: 24px; width: 24px; } @media screen and (max-width: 500px) { img { height: 16px; width: 16px; } } grid-column-start: 1; grid-column-end: span 1; grid-row-start: 1; grid-row-end: span 2; align-self: center; `; const TokenTxAmt = styled.div` padding-left: 12px; text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; `; const TokenName = styled.div` padding-left: 12px; font-size: 0.8rem; @media screen and (max-width: 500px) { font-size: 0.6rem; } text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; `; const TxWrapper = styled.div` display: grid; grid-template-columns: 36px 30% 50%; justify-content: space-between; align-items: center; padding: 15px 25px; border-radius: 16px; background: ${props => props.theme.tokenListItem.background}; margin-bottom: 12px; box-shadow: ${props => props.theme.tokenListItem.boxShadow}; :hover { transform: translateY(-2px); box-shadow: rgb(136 172 243 / 25%) 0px 10px 30px, rgb(0 0 0 / 3%) 0px 1px 1px, rgb(0 51 167 / 10%) 0px 10px 20px; transition: all 0.8s cubic-bezier(0.075, 0.82, 0.165, 1) 0s; } @media screen and (max-width: 500px) { grid-template-columns: 24px 30% 50%; padding: 12px 12px; } `; const Tx = ({ data, fiatPrice, fiatCurrency }) => { const txDate = typeof data.blocktime === 'undefined' ? new Date().toLocaleDateString() : new Date(data.blocktime * 1000).toLocaleDateString(); // if data only includes height and txid, then the tx could not be parsed by cashtab // render as such but keep link to block explorer let unparsedTx = false; if (!Object.keys(data).includes('outgoingTx')) { unparsedTx = true; } return ( <> {unparsedTx ? ( Unparsed
{txDate}
Open in Explorer
) : ( {data.outgoingTx ? ( <> {data.tokenTx && data.tokenInfo.transactionType === 'GENESIS' ? ( ) : ( )} ) : ( )} {data.outgoingTx ? ( <> {data.tokenTx && data.tokenInfo.transactionType === 'GENESIS' ? ( Genesis ) : ( Sent )} ) : ( Received )}
{txDate}
{data.tokenTx ? ( {data.tokenTx && data.tokenInfo ? ( <> {currency.tokenIconsUrl !== '' ? ( {`identicon } /> ) : ( {`identicon )} {data.outgoingTx ? ( <> {data.tokenInfo.transactionType === 'GENESIS' ? ( <> +{' '} {data.tokenInfo.qtyReceived.toString()}   { data.tokenInfo .tokenTicker } { data.tokenInfo .tokenName } ) : ( <> -{' '} {data.tokenInfo.qtySent.toString()}   { data.tokenInfo .tokenTicker } { data.tokenInfo .tokenName } )} ) : ( <> +{' '} {data.tokenInfo.qtyReceived.toString()}   {data.tokenInfo.tokenTicker} {data.tokenInfo.tokenName} )} ) : ( Token Tx )} ) : ( <> {data.outgoingTx ? ( <> -{' '} {formatBalance( fromLegacyDecimals(data.amountSent), )}{' '} {currency.ticker}
{fiatPrice !== null && !isNaN(data.amountSent) && ( -{' '} { currency.fiatCurrencies[ fiatCurrency ].symbol } {( fromLegacyDecimals( data.amountSent, ) * fiatPrice ).toFixed(2)}{' '} { currency.fiatCurrencies .fiatCurrency } )} ) : ( <> +{' '} {formatBalance( fromLegacyDecimals( data.amountReceived, ), )}{' '} {currency.ticker}
{fiatPrice !== null && !isNaN(data.amountReceived) && ( +{' '} { currency.fiatCurrencies[ fiatCurrency ].symbol } {( fromLegacyDecimals( data.amountReceived, ) * fiatPrice ).toFixed(2)}{' '} { currency.fiatCurrencies .fiatCurrency } )} )}
)} {data.opReturnMessage && ( <>
- Message: + {data.isCashtabMessage ? ( + + Cashtab Message + + ) : ( + + External Message + + )}
{data.opReturnMessage ? Buffer.from( data.opReturnMessage, ).toString() : ''}
)}
)} ); }; Tx.propTypes = { data: PropTypes.object, fiatPrice: PropTypes.number, fiatCurrency: PropTypes.string, }; export default Tx; diff --git a/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap b/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap index 42daaa070..1e617dd3b 100644 --- a/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap +++ b/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap @@ -1,622 +1,622 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances 1`] = ` Array [
🎉 Congratulations on your new wallet! 🎉
Start using the wallet immediately to receive XEC payments, or load it up with XEC to send to others
,
0 XEC
,
Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
ecash: qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
,
XEC
eToken
, ] `; exports[`Wallet with BCH balances and tokens 1`] = ` Array [
🎉 Congratulations on your new wallet! 🎉
Start using the wallet immediately to receive XEC payments, or load it up with XEC to send to others
,
0 XEC
,
Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
ecash: qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
,
XEC
eToken
, ] `; exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [
0.06 XEC
,
$ NaN USD
,
Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
ecash: qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
,
XEC
eToken
, ] `; exports[`Wallet without BCH balance 1`] = ` Array [
🎉 Congratulations on your new wallet! 🎉
Start using the wallet immediately to receive XEC payments, or load it up with XEC to send to others
,
0 XEC
,
Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
ecash: qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
,
XEC
eToken
, ] `; exports[`Without wallet defined 1`] = ` Array [

Welcome to Cashtab!

,

Cashtab is an open source, non-custodial web wallet for eCash .

Want to learn more? Check out the Cashtab documentation.

, , , ] `; diff --git a/web/cashtab/src/hooks/__mocks__/mockParsedTxs.js b/web/cashtab/src/hooks/__mocks__/mockParsedTxs.js index b7bf2b4e2..5ce2860e7 100644 --- a/web/cashtab/src/hooks/__mocks__/mockParsedTxs.js +++ b/web/cashtab/src/hooks/__mocks__/mockParsedTxs.js @@ -1,91 +1,97 @@ // Expected result of applying parseTxData to mockTxDataWityhPassthrough[0] export const mockSentCashTx = [ { amountReceived: 0, amountSent: 0.000042, blocktime: 1614380741, confirmations: 2721, destinationAddress: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', height: 674993, outgoingTx: true, + isCashtabMessage: false, opReturnMessage: '', tokenTx: false, txid: '089f2188d5771a7de0589def2b8d6c1a1f33f45b6de26d9a0ef32782f019ecf1', }, ]; export const mockReceivedCashTx = [ { amountReceived: 3, amountSent: 0, blocktime: 1612567121, confirmations: 5637, destinationAddress: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', height: 672077, outgoingTx: false, + isCashtabMessage: false, opReturnMessage: '', tokenTx: false, txid: '42d39fbe068a40fe691f987b22fdf04b80f94d71d2fec20a58125e7b1a06d2a9', }, ]; export const mockSentTokenTx = [ { amountReceived: 0, amountSent: 0.00000546, blocktime: 1614027278, confirmations: 3270, destinationAddress: 'bitcoincash:qzj5zu6fgg8v2we82gh76xnrk9njcregluzgaztm45', height: 674444, outgoingTx: true, + isCashtabMessage: false, opReturnMessage: '', tokenTx: true, txid: 'ffe3a7500dbcc98021ad581c98d9947054d1950a7f3416664715066d3d20ad72', }, ]; export const mockReceivedTokenTx = [ { amountReceived: 0.00000546, amountSent: 0, blocktime: 1613859311, confirmations: 3571, destinationAddress: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', height: 674143, outgoingTx: false, + isCashtabMessage: false, opReturnMessage: '', tokenTx: true, txid: '618d0dd8c0c5fa5a34c6515c865dd72bb76f8311cd6ee9aef153bab20dabc0e6', }, ]; export const mockSentOpReturnMessageTx = [ { amountReceived: 0, amountSent: 0, blocktime: 1635507345, confirmations: 59, destinationAddress: undefined, height: undefined, - opReturnMessage: new Buffer('testing message 12'), + opReturnMessage: new Buffer('bingoelectrum'), outgoingTx: false, tokenTx: false, + isCashtabMessage: false, txid: 'dd35690b0cefd24dcc08acba8694ecd49293f365a81372cb66c8f1c1291d97c5', }, ]; export const mockReceivedOpReturnMessageTx = [ { amountReceived: 0, amountSent: 0, blocktime: 1635511136, confirmations: 70, destinationAddress: undefined, height: undefined, - opReturnMessage: new Buffer('testing message 13'), + opReturnMessage: new Buffer('cashtabular'), outgoingTx: false, tokenTx: false, + isCashtabMessage: true, txid: '5adc33b5c0509b31c6da359177b19467c443bdc4dd37c283c0f87244c0ad63af', }, ]; diff --git a/web/cashtab/src/hooks/__mocks__/mockTxDataWithPassthrough.js b/web/cashtab/src/hooks/__mocks__/mockTxDataWithPassthrough.js index 0c310c22c..e7fffc2c7 100644 --- a/web/cashtab/src/hooks/__mocks__/mockTxDataWithPassthrough.js +++ b/web/cashtab/src/hooks/__mocks__/mockTxDataWithPassthrough.js @@ -1,869 +1,870 @@ export default [ { txid: '089f2188d5771a7de0589def2b8d6c1a1f33f45b6de26d9a0ef32782f019ecf1', hash: '089f2188d5771a7de0589def2b8d6c1a1f33f45b6de26d9a0ef32782f019ecf1', version: 2, size: 225, locktime: 0, vin: [ { txid: 'b96da810b15deb312ad4508a165033ca8ffa282f88e5b7b0e79be09a0b0424f9', vout: 1, scriptSig: { asm: '3044022064084d72b1bb7ca148d1950cf07494ffb397cb3df53b72afa8bd844b80369ecd02203ae21f14ba5019f38bc0b80b99e7c8cc1d5d3360ca7bab56be28ef583fe5c6a6[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '473044022064084d72b1bb7ca148d1950cf07494ffb397cb3df53b72afa8bd844b80369ecd02203ae21f14ba5019f38bc0b80b99e7c8cc1d5d3360ca7bab56be28ef583fe5c6a6412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, ], vout: [ { value: 0.000042, n: 0, scriptPubKey: { asm: 'OP_DUP OP_HASH160 6e1da64f04fc29dbe0b8d33a341e05e3afc586eb OP_EQUALVERIFY OP_CHECKSIG', hex: '76a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', ], }, }, { value: 0.6244967, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, ], hex: '0200000001f924040b9ae09be7b0b7e5882f28fa8fca3350168a50d42a31eb5db110a86db9010000006a473044022064084d72b1bb7ca148d1950cf07494ffb397cb3df53b72afa8bd844b80369ecd02203ae21f14ba5019f38bc0b80b99e7c8cc1d5d3360ca7bab56be28ef583fe5c6a6412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795ffffffff0268100000000000001976a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac06e8b803000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac00000000', blockhash: '000000000000000087dd4ca6308e835edfba871fee36d3e53ad3c9545c4b1719', confirmations: 2721, time: 1614380741, blocktime: 1614380741, height: 674993, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: 'ffe3a7500dbcc98021ad581c98d9947054d1950a7f3416664715066d3d20ad72', hash: 'ffe3a7500dbcc98021ad581c98d9947054d1950a7f3416664715066d3d20ad72', version: 2, size: 480, locktime: 0, vin: [ { txid: 'b980b35b794ad73d8aae312385e82d9be8086e7b743e1c6a468db8db8ac74bd8', vout: 3, scriptSig: { asm: '30440220538de8f61d716c899e6a2cd78ca46162edaaa5f0d000ebbbc875608e5639170a02206a7fc8f7c16cef1c56667a8da6d5e480f440ecf43238879ad9f8785a0473a72b[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '4730440220538de8f61d716c899e6a2cd78ca46162edaaa5f0d000ebbbc875608e5639170a02206a7fc8f7c16cef1c56667a8da6d5e480f440ecf43238879ad9f8785a0473a72b412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: 'b980b35b794ad73d8aae312385e82d9be8086e7b743e1c6a468db8db8ac74bd8', vout: 2, scriptSig: { asm: '3045022100ce03e19bd181b903adc6f192d4ad0900e6816f6e62282cefff05c22cf36a647602202b296a2ed1805f0b0a9aa5f99158685298e7a0aff406fedb8abb8e0afaf48ca4[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '483045022100ce03e19bd181b903adc6f192d4ad0900e6816f6e62282cefff05c22cf36a647602202b296a2ed1805f0b0a9aa5f99158685298e7a0aff406fedb8abb8e0afaf48ca4412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, ], vout: [ { value: 0, n: 0, scriptPubKey: { asm: 'OP_RETURN 5262419 1 1145980243 50d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e 0000000000000003 000000000000005e', hex: '6a04534c500001010453454e442050d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e08000000000000000308000000000000005e', type: 'nulldata', }, }, { value: 0.00000546, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 a5417349420ec53b27522fed1a63b1672c0f28ff OP_EQUALVERIFY OP_CHECKSIG', hex: '76a914a5417349420ec53b27522fed1a63b1672c0f28ff88ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qzj5zu6fgg8v2we82gh76xnrk9njcregluzgaztm45', ], }, }, { value: 0.00000546, n: 2, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, { value: 4.99996074, n: 3, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, ], hex: '0200000002d84bc78adbb88d466a1c3e747b6e08e89b2de8852331ae8a3dd74a795bb380b9030000006a4730440220538de8f61d716c899e6a2cd78ca46162edaaa5f0d000ebbbc875608e5639170a02206a7fc8f7c16cef1c56667a8da6d5e480f440ecf43238879ad9f8785a0473a72b412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795ffffffffd84bc78adbb88d466a1c3e747b6e08e89b2de8852331ae8a3dd74a795bb380b9020000006b483045022100ce03e19bd181b903adc6f192d4ad0900e6816f6e62282cefff05c22cf36a647602202b296a2ed1805f0b0a9aa5f99158685298e7a0aff406fedb8abb8e0afaf48ca4412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795ffffffff040000000000000000406a04534c500001010453454e442050d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e08000000000000000308000000000000005e22020000000000001976a914a5417349420ec53b27522fed1a63b1672c0f28ff88ac22020000000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688acaa55cd1d000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac00000000', blockhash: '00000000000000007053867de29516374a23d7adfb08ccb47cfbea0e98a49e5b', confirmations: 3270, time: 1614027278, blocktime: 1614027278, height: 674444, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: 'b980b35b794ad73d8aae312385e82d9be8086e7b743e1c6a468db8db8ac74bd8', hash: 'b980b35b794ad73d8aae312385e82d9be8086e7b743e1c6a468db8db8ac74bd8', version: 2, size: 479, locktime: 0, vin: [ { txid: 'ec9c20c2c5cd5aa4c9261a9f97e68734b175962c4b3d9edc996dd415dd03c2e7', vout: 0, scriptSig: { asm: '3044022075cb93e60ffb792b2715d96f3d31033e8f385bb9bfeadf99f7b1055d749a33cc022028292ee8ffaed64cbc6f9b680db36e031250672a6b0c5cfd23f9a61977d52ed7[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '473044022075cb93e60ffb792b2715d96f3d31033e8f385bb9bfeadf99f7b1055d749a33cc022028292ee8ffaed64cbc6f9b680db36e031250672a6b0c5cfd23f9a61977d52ed7412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: '618d0dd8c0c5fa5a34c6515c865dd72bb76f8311cd6ee9aef153bab20dabc0e6', vout: 1, scriptSig: { asm: '304402203ea0558cd917eb8f6c286e79ffcc5dd1f5accb66c2e5836628d6be6f9d03ca260220120a6da92b6f44bdfcd3ef7b08263d3f73d99ff4b1f83b8f998ff1355f3f0d2e[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '47304402203ea0558cd917eb8f6c286e79ffcc5dd1f5accb66c2e5836628d6be6f9d03ca260220120a6da92b6f44bdfcd3ef7b08263d3f73d99ff4b1f83b8f998ff1355f3f0d2e412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, ], vout: [ { value: 0, n: 0, scriptPubKey: { asm: 'OP_RETURN 5262419 1 1145980243 50d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e 0000000000000003 0000000000000061', hex: '6a04534c500001010453454e442050d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e080000000000000003080000000000000061', type: 'nulldata', }, }, { value: 0.00000546, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 d4fa9121bcd065dd93e58831569cf51ef5a74f61 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a914d4fa9121bcd065dd93e58831569cf51ef5a74f6188ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qr204yfphngxthvnukyrz45u7500tf60vyea48xwmd', ], }, }, { value: 0.00000546, n: 2, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, { value: 4.99998974, n: 3, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, ], hex: '0200000002e7c203dd15d46d99dc9e3d4b2c9675b13487e6979f1a26c9a45acdc5c2209cec000000006a473044022075cb93e60ffb792b2715d96f3d31033e8f385bb9bfeadf99f7b1055d749a33cc022028292ee8ffaed64cbc6f9b680db36e031250672a6b0c5cfd23f9a61977d52ed7412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795ffffffffe6c0ab0db2ba53f1aee96ecd11836fb72bd75d865c51c6345afac5c0d80d8d61010000006a47304402203ea0558cd917eb8f6c286e79ffcc5dd1f5accb66c2e5836628d6be6f9d03ca260220120a6da92b6f44bdfcd3ef7b08263d3f73d99ff4b1f83b8f998ff1355f3f0d2e412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795ffffffff040000000000000000406a04534c500001010453454e442050d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e08000000000000000308000000000000006122020000000000001976a914d4fa9121bcd065dd93e58831569cf51ef5a74f6188ac22020000000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688acfe60cd1d000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac00000000', blockhash: '0000000000000000a9f812d56e2249b7c462ce499a0852bdfe20bb46c1bb9f92', confirmations: 3278, time: 1614021424, blocktime: 1614021424, height: 674436, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: '618d0dd8c0c5fa5a34c6515c865dd72bb76f8311cd6ee9aef153bab20dabc0e6', hash: '618d0dd8c0c5fa5a34c6515c865dd72bb76f8311cd6ee9aef153bab20dabc0e6', version: 2, size: 436, locktime: 0, vin: [ { txid: '50d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e', vout: 3, scriptSig: { asm: '30440220664f988b86035ddcdff6e9c3b8e140712eca297750d056e41577a0bf0059e7ff022030982b3fcab1cab5d6086bc935e941e7d22efbb0ad5ccca0268515c5c8306089[ALL|FORKID] 034509251caa5f01e2787c436949eb94d71dcc451bcde5791ae5b7109255f5f0a3', hex: '4730440220664f988b86035ddcdff6e9c3b8e140712eca297750d056e41577a0bf0059e7ff022030982b3fcab1cab5d6086bc935e941e7d22efbb0ad5ccca0268515c5c83060894121034509251caa5f01e2787c436949eb94d71dcc451bcde5791ae5b7109255f5f0a3', }, sequence: 4294967295, address: 'bitcoincash:qzudj5fd9t0cknnsc3wzdd4sp46u9r42jcnqnwfss0', }, { txid: '50d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e', vout: 1, scriptSig: { asm: '304402203ce88e0a95d5581ad567c0468c87a08027aa5ecdecd614a168d833d7ecc02c1c022013ddd81147b44ad5488107d5c4d535f7f59e9fa46840451d39422aace284b2b7[ALL|FORKID] 034509251caa5f01e2787c436949eb94d71dcc451bcde5791ae5b7109255f5f0a3', hex: '47304402203ce88e0a95d5581ad567c0468c87a08027aa5ecdecd614a168d833d7ecc02c1c022013ddd81147b44ad5488107d5c4d535f7f59e9fa46840451d39422aace284b2b74121034509251caa5f01e2787c436949eb94d71dcc451bcde5791ae5b7109255f5f0a3', }, sequence: 4294967295, address: 'bitcoincash:qzudj5fd9t0cknnsc3wzdd4sp46u9r42jcnqnwfss0', }, ], vout: [ { value: 0, n: 0, scriptPubKey: { asm: 'OP_RETURN 5262419 1 1145980243 50d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e 0000000000000064', hex: '6a04534c500001010453454e442050d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e080000000000000064', type: 'nulldata', }, }, { value: 0.00000546, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, { value: 0.00088064, n: 2, scriptPubKey: { asm: 'OP_DUP OP_HASH160 b8d9512d2adf8b4e70c45c26b6b00d75c28eaa96 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a914b8d9512d2adf8b4e70c45c26b6b00d75c28eaa9688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qzudj5fd9t0cknnsc3wzdd4sp46u9r42jcnqnwfss0', ], }, }, ], hex: '02000000020e41711243954c8ffe409761e994272af43ced6f56c8c6afa7cd55622c29d850030000006a4730440220664f988b86035ddcdff6e9c3b8e140712eca297750d056e41577a0bf0059e7ff022030982b3fcab1cab5d6086bc935e941e7d22efbb0ad5ccca0268515c5c83060894121034509251caa5f01e2787c436949eb94d71dcc451bcde5791ae5b7109255f5f0a3ffffffff0e41711243954c8ffe409761e994272af43ced6f56c8c6afa7cd55622c29d850010000006a47304402203ce88e0a95d5581ad567c0468c87a08027aa5ecdecd614a168d833d7ecc02c1c022013ddd81147b44ad5488107d5c4d535f7f59e9fa46840451d39422aace284b2b74121034509251caa5f01e2787c436949eb94d71dcc451bcde5791ae5b7109255f5f0a3ffffffff030000000000000000376a04534c500001010453454e442050d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e08000000000000006422020000000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac00580100000000001976a914b8d9512d2adf8b4e70c45c26b6b00d75c28eaa9688ac00000000', blockhash: '000000000000000034c77993a35c74fe2dddace27198681ca1e89e928d0c2fff', confirmations: 3571, time: 1613859311, blocktime: 1613859311, height: 674143, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: 'f90631b48521a4147dd9dd7091ce936eddc0c3e6221ec87fa4fabacc453a0b95', hash: 'f90631b48521a4147dd9dd7091ce936eddc0c3e6221ec87fa4fabacc453a0b95', version: 2, size: 437, locktime: 0, vin: [ { txid: 'db464f77ac97deabc28df07a7e4a2e261c854a8ec4dc959b89b10531966f6cbf', vout: 0, scriptSig: { asm: '3044022065622a7aa065f56abe84f3589c983a768e3ef5d72c9352991d6b584a2a16dcb802200c1c0065106207715a024624ed951e851d4f742c55a704e9531bebd2ef84fc14[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '473044022065622a7aa065f56abe84f3589c983a768e3ef5d72c9352991d6b584a2a16dcb802200c1c0065106207715a024624ed951e851d4f742c55a704e9531bebd2ef84fc1441210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, { txid: 'acbb66f826211f40b89e84d9bd2143dfb541d67e1e3c664b17ccd3ba66327a9e', vout: 1, scriptSig: { asm: '3045022100b475cf7d1eaf37641d2107f13be0ef9acbd17b252ed3f9ae349edfdcd6a97cf402202bf2852dfa905e6d50c96a622d2838408ceb979245a4342d5096acc938135804[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '483045022100b475cf7d1eaf37641d2107f13be0ef9acbd17b252ed3f9ae349edfdcd6a97cf402202bf2852dfa905e6d50c96a622d2838408ceb979245a4342d5096acc93813580441210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, ], vout: [ { value: 0, n: 0, scriptPubKey: { asm: 'OP_RETURN 5262419 1 1145980243 bfddfcfc9fb9a8d61ed74fa94b5e32ccc03305797eea461658303df5805578ef 0000000000000001', hex: '6a04534c500001010453454e4420bfddfcfc9fb9a8d61ed74fa94b5e32ccc03305797eea461658303df5805578ef080000000000000001', type: 'nulldata', }, }, { value: 0.00000546, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, { value: 9.99997101, n: 2, scriptPubKey: { asm: 'OP_DUP OP_HASH160 6e1da64f04fc29dbe0b8d33a341e05e3afc586eb OP_EQUALVERIFY OP_CHECKSIG', hex: '76a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', ], }, }, ], hex: '0200000002bf6c6f963105b1899b95dcc48e4a851c262e4a7e7af08dc2abde97ac774f46db000000006a473044022065622a7aa065f56abe84f3589c983a768e3ef5d72c9352991d6b584a2a16dcb802200c1c0065106207715a024624ed951e851d4f742c55a704e9531bebd2ef84fc1441210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff9e7a3266bad3cc174b663c1e7ed641b5df4321bdd9849eb8401f2126f866bbac010000006b483045022100b475cf7d1eaf37641d2107f13be0ef9acbd17b252ed3f9ae349edfdcd6a97cf402202bf2852dfa905e6d50c96a622d2838408ceb979245a4342d5096acc93813580441210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff030000000000000000376a04534c500001010453454e4420bfddfcfc9fb9a8d61ed74fa94b5e32ccc03305797eea461658303df5805578ef08000000000000000122020000000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688acadbe9a3b000000001976a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac00000000', blockhash: '0000000000000000132378b84a7477b7d601faedec302264bde1e89b1480e364', confirmations: 5013, time: 1612966022, blocktime: 1612966022, height: 672701, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: '42d39fbe068a40fe691f987b22fdf04b80f94d71d2fec20a58125e7b1a06d2a9', hash: '42d39fbe068a40fe691f987b22fdf04b80f94d71d2fec20a58125e7b1a06d2a9', version: 2, size: 226, locktime: 0, vin: [ { txid: '5e0436c6741e226d05c5b7e7e23de8213d3583e2669e50a80b908bf4cb471317', vout: 1, scriptSig: { asm: '3045022100f8a8eca8f5d6149511c518d41015512f8164a5be6f01e9efd609db9a429f4872022059121e122043b43eae77b5e132b8f798a290e6eed8a2026a0656540cd1bd752b[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '483045022100f8a8eca8f5d6149511c518d41015512f8164a5be6f01e9efd609db9a429f4872022059121e122043b43eae77b5e132b8f798a290e6eed8a2026a0656540cd1bd752b41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, ], vout: [ { value: 3, n: 0, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, { value: 6.9999586, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 6e1da64f04fc29dbe0b8d33a341e05e3afc586eb OP_EQUALVERIFY OP_CHECKSIG', hex: '76a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', ], }, }, ], hex: '0200000001171347cbf48b900ba8509e66e283353d21e83de2e7b7c5056d221e74c636045e010000006b483045022100f8a8eca8f5d6149511c518d41015512f8164a5be6f01e9efd609db9a429f4872022059121e122043b43eae77b5e132b8f798a290e6eed8a2026a0656540cd1bd752b41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff0200a3e111000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688acd416b929000000001976a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac00000000', blockhash: '00000000000000008f563edf8604e537fe0d1e80f1c7c2d97dd094824f804ba3', confirmations: 5637, time: 1612567121, blocktime: 1612567121, height: 672077, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: 'b96da810b15deb312ad4508a165033ca8ffa282f88e5b7b0e79be09a0b0424f9', hash: 'b96da810b15deb312ad4508a165033ca8ffa282f88e5b7b0e79be09a0b0424f9', version: 2, size: 226, locktime: 0, vin: [ { txid: '9ad75af97f0617a3729c2bd31bf7c4b380230e661cc921a3c6be0febc75a3e49', vout: 1, scriptSig: { asm: '3045022100d59e6fad4d1d57796f229a7d4aa3b01fc3241132dae9bc406c66fa33d7aef21c022036a5f432d6d99f65848ac12c00bde2b5ba7e63a9f9a74349d9ab8ec39db26f8e[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '483045022100d59e6fad4d1d57796f229a7d4aa3b01fc3241132dae9bc406c66fa33d7aef21c022036a5f432d6d99f65848ac12c00bde2b5ba7e63a9f9a74349d9ab8ec39db26f8e412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, ], vout: [ { value: 0.12345, n: 0, scriptPubKey: { asm: 'OP_DUP OP_HASH160 6e1da64f04fc29dbe0b8d33a341e05e3afc586eb OP_EQUALVERIFY OP_CHECKSIG', hex: '76a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', ], }, }, { value: 0.62455003, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, ], hex: '0200000001493e5ac7eb0fbec6a321c91c660e2380b3c4f71bd32b9c72a317067ff95ad79a010000006b483045022100d59e6fad4d1d57796f229a7d4aa3b01fc3241132dae9bc406c66fa33d7aef21c022036a5f432d6d99f65848ac12c00bde2b5ba7e63a9f9a74349d9ab8ec39db26f8e412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795ffffffff02a85ebc00000000001976a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88acdbfcb803000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac00000000', blockhash: '00000000000000008f563edf8604e537fe0d1e80f1c7c2d97dd094824f804ba3', confirmations: 5637, time: 1612567121, blocktime: 1612567121, height: 672077, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: 'db464f77ac97deabc28df07a7e4a2e261c854a8ec4dc959b89b10531966f6cbf', hash: 'db464f77ac97deabc28df07a7e4a2e261c854a8ec4dc959b89b10531966f6cbf', version: 2, size: 225, locktime: 0, vin: [ { txid: '1452267e57429edcfdcb1184b24becea6ddf8f8a4f8e130dad6248545d9f8e75', vout: 1, scriptSig: { asm: '30440220184921bfce634a57b5220f06b11b64c0cb7e67ecd9c634335e3e933e35a7a969022038b2074e1d75aa4f6945d150bae5b8a1d426f4284da2b96336fa0fc741eb6de7[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '4730440220184921bfce634a57b5220f06b11b64c0cb7e67ecd9c634335e3e933e35a7a969022038b2074e1d75aa4f6945d150bae5b8a1d426f4284da2b96336fa0fc741eb6de7412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, ], vout: [ { value: 10.00000001, n: 0, scriptPubKey: { asm: 'OP_DUP OP_HASH160 6e1da64f04fc29dbe0b8d33a341e05e3afc586eb OP_EQUALVERIFY OP_CHECKSIG', hex: '76a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', ], }, }, { value: 2.8830607, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, ], hex: '0200000001758e9f5d544862ad0d138e4f8a8fdf6deaec4bb28411cbfddc9e42577e265214010000006a4730440220184921bfce634a57b5220f06b11b64c0cb7e67ecd9c634335e3e933e35a7a969022038b2074e1d75aa4f6945d150bae5b8a1d426f4284da2b96336fa0fc741eb6de7412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795ffffffff0201ca9a3b000000001976a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac96332f11000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac00000000', blockhash: '00000000000000008f563edf8604e537fe0d1e80f1c7c2d97dd094824f804ba3', confirmations: 5637, time: 1612567121, blocktime: 1612567121, height: 672077, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: 'e32c20137e590f253b8d198608f7fffd428fc0bd7a9a0675bb6af091d1cb2ea4', hash: 'e32c20137e590f253b8d198608f7fffd428fc0bd7a9a0675bb6af091d1cb2ea4', version: 2, size: 373, locktime: 0, vin: [ { txid: 'f63e890423b3bffa6e01be2dcb4942940c2e8a1985926411558a22d1b5dd0e29', vout: 1, scriptSig: { asm: '3045022100c7f51ff0888c182a1a60c08904d8116c9d2e31cb7d2fd5b63c2bf9fd7b246fc102202ee786d2052448621c4a04d18d13c83ac5ee27008dd079e8ba954f8197ff3c6c[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '483045022100c7f51ff0888c182a1a60c08904d8116c9d2e31cb7d2fd5b63c2bf9fd7b246fc102202ee786d2052448621c4a04d18d13c83ac5ee27008dd079e8ba954f8197ff3c6c412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: '42d39fbe068a40fe691f987b22fdf04b80f94d71d2fec20a58125e7b1a06d2a9', vout: 0, scriptSig: { asm: '304402201bbfcd0c120ace9b8c7a6f5e77b61236bb1128e2a757f85ba80101885e9c1212022046fed4006dcd6a236034dede77c566acf74824d14b3ee3da884e9bd93884ff93[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '47304402201bbfcd0c120ace9b8c7a6f5e77b61236bb1128e2a757f85ba80101885e9c1212022046fed4006dcd6a236034dede77c566acf74824d14b3ee3da884e9bd93884ff93412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, ], vout: [ { value: 1.8725994, n: 0, scriptPubKey: { asm: 'OP_DUP OP_HASH160 6e1da64f04fc29dbe0b8d33a341e05e3afc586eb OP_EQUALVERIFY OP_CHECKSIG', hex: '76a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', ], }, }, { value: 1.49237053, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, ], hex: '0200000002290eddb5d1228a5511649285198a2e0c944249cb2dbe016efabfb32304893ef6010000006b483045022100c7f51ff0888c182a1a60c08904d8116c9d2e31cb7d2fd5b63c2bf9fd7b246fc102202ee786d2052448621c4a04d18d13c83ac5ee27008dd079e8ba954f8197ff3c6c412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795ffffffffa9d2061a7b5e12580ac2fed2714df9804bf0fd227b981f69fe408a06be9fd342000000006a47304402201bbfcd0c120ace9b8c7a6f5e77b61236bb1128e2a757f85ba80101885e9c1212022046fed4006dcd6a236034dede77c566acf74824d14b3ee3da884e9bd93884ff93412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795ffffffff02245c290b000000001976a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac3d2de508000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac00000000', blockhash: '00000000000000008f563edf8604e537fe0d1e80f1c7c2d97dd094824f804ba3', confirmations: 5637, time: 1612567121, blocktime: 1612567121, height: 672077, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: 'ec9c20c2c5cd5aa4c9261a9f97e68734b175962c4b3d9edc996dd415dd03c2e7', hash: 'ec9c20c2c5cd5aa4c9261a9f97e68734b175962c4b3d9edc996dd415dd03c2e7', version: 2, size: 1405, locktime: 0, vin: [ { txid: '3507d73b0bb82421d64ae79f469943e56f15d7db954ad235f48ede33c718d860', vout: 0, scriptSig: { asm: '3044022000cc5b79e5da60cf4935f3a172089cd9b631b678462ee29091dc610816d059c4022002e3b6f32e825ac04d2907453d6d647a32a995c798df1c68401cc461f6bfbd3a[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '473044022000cc5b79e5da60cf4935f3a172089cd9b631b678462ee29091dc610816d059c4022002e3b6f32e825ac04d2907453d6d647a32a995c798df1c68401cc461f6bfbd3a41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, { txid: '8f73a718d907d94e60c5f73f299bd01dc5b1c163c4ebc26b5304e37a1a7f34af', vout: 0, scriptSig: { asm: '3045022100ac50553448f2a5fab1177ed0bc64541b2dba063d04f2d69a8a1d216fb1435e5802202c7f6abd1685a6d81f14ac3bdb0874d214a5f4260719f9c5dc519ac5d8dffd37[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '483045022100ac50553448f2a5fab1177ed0bc64541b2dba063d04f2d69a8a1d216fb1435e5802202c7f6abd1685a6d81f14ac3bdb0874d214a5f4260719f9c5dc519ac5d8dffd3741210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, { txid: 'd38b76bacd6aa75ad2d6fcfd994533e54d0541435970eace49486fde9d6ee2e3', vout: 0, scriptSig: { asm: '304502210091836c6cb4c786bd3b74b73e579ddf8b843ba51841e5675fa53608449b67371802203de75f32b684cfe2d2e9cd424ea6eb4f49248e6698365c9364ebf84cd6e50eab[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '48304502210091836c6cb4c786bd3b74b73e579ddf8b843ba51841e5675fa53608449b67371802203de75f32b684cfe2d2e9cd424ea6eb4f49248e6698365c9364ebf84cd6e50eab41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, { txid: 'd47607a72d6bc093556fa7f2cec9d67719bd627751d5d27bc53c4eb8eb6f54e5', vout: 0, scriptSig: { asm: '3045022100e7727d9d26c645282553aef27947ad6795bc89b505ad089d617b6f696399352802206c736524a1410ed3e30cf1127f7f02c9a249392f8f8e7c670250472909d1c0d6[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '483045022100e7727d9d26c645282553aef27947ad6795bc89b505ad089d617b6f696399352802206c736524a1410ed3e30cf1127f7f02c9a249392f8f8e7c670250472909d1c0d641210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, { txid: '33f246811f794c4b64098a64c698ae5811054b13e289256a18e2d142beef57e7', vout: 1, scriptSig: { asm: '304402203fe78ad5aaeefab7b3b2277eefc4a2ace9c2e92694b46bf4a76927bf2b82017102200ded59336aba269a54865d9fdd99e72081c0318ccbc37bc0fc0c72b60ae35382[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '47304402203fe78ad5aaeefab7b3b2277eefc4a2ace9c2e92694b46bf4a76927bf2b82017102200ded59336aba269a54865d9fdd99e72081c0318ccbc37bc0fc0c72b60ae3538241210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, { txid: '25f915d2912524ad602c882211ccaf479d6bf87ef7c24d1be0f325cec3727257', vout: 0, scriptSig: { asm: '30440220670af03605b9495c8ecee357889ceeb137dadaa1662136fdc55c28fe9434e3c60220285195a62811941745a9f93e136e59c96b81d5b0d9525f3d16d001bc0f6fa9bb[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '4730440220670af03605b9495c8ecee357889ceeb137dadaa1662136fdc55c28fe9434e3c60220285195a62811941745a9f93e136e59c96b81d5b0d9525f3d16d001bc0f6fa9bb41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, { txid: 'c9044b4d7438d006a722ef85474c8127265eced4f72c7d71c2f714444bc0e1f2', vout: 0, scriptSig: { asm: '304402203f822a0b207ed49e6918663133a18037c24498c2f770c2649333a32f523e259d02203afc42a79d0da123b67f814effeee7c05c7996ea829b3cfa46c5c2e74209c096[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '47304402203f822a0b207ed49e6918663133a18037c24498c2f770c2649333a32f523e259d02203afc42a79d0da123b67f814effeee7c05c7996ea829b3cfa46c5c2e74209c09641210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, { txid: '045306f0019ae0d977de7ff17dd55e861b3fe94458693ee2b94ce5dd7003aab9', vout: 0, scriptSig: { asm: '3045022100a7a2cf838a13a19f0e443ca35ac5ee3d55f70edca992f98402a84d4ab5ae1ad90220644a02c746eae7b44a4600199ecbf69f3b0f0bdf8479f461c482d67ef4a84e76[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '483045022100a7a2cf838a13a19f0e443ca35ac5ee3d55f70edca992f98402a84d4ab5ae1ad90220644a02c746eae7b44a4600199ecbf69f3b0f0bdf8479f461c482d67ef4a84e7641210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, { txid: '1452267e57429edcfdcb1184b24becea6ddf8f8a4f8e130dad6248545d9f8e75', vout: 0, scriptSig: { asm: '30440220290701c797eb52ad6721db615c7d6f623c0200be0e6d6802df68c527655475450220446c4a4da9a0df5efcb57711ad61cf6167dfdda937bd0477189be8afedaedd05[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '4730440220290701c797eb52ad6721db615c7d6f623c0200be0e6d6802df68c527655475450220446c4a4da9a0df5efcb57711ad61cf6167dfdda937bd0477189be8afedaedd0541210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', }, ], vout: [ { value: 5.00001874, n: 0, scriptPubKey: { asm: 'OP_DUP OP_HASH160 76458db0ed96fe9863fc1ccec9fa2cfab884b0f6 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', ], }, }, { value: 7.52551634, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 6e1da64f04fc29dbe0b8d33a341e05e3afc586eb OP_EQUALVERIFY OP_CHECKSIG', hex: '76a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', ], }, }, ], hex: '020000000960d818c733de8ef435d24a95dbd7156fe54399469fe74ad62124b80b3bd70735000000006a473044022000cc5b79e5da60cf4935f3a172089cd9b631b678462ee29091dc610816d059c4022002e3b6f32e825ac04d2907453d6d647a32a995c798df1c68401cc461f6bfbd3a41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffffaf347f1a7ae304536bc2ebc463c1b1c51dd09b293ff7c5604ed907d918a7738f000000006b483045022100ac50553448f2a5fab1177ed0bc64541b2dba063d04f2d69a8a1d216fb1435e5802202c7f6abd1685a6d81f14ac3bdb0874d214a5f4260719f9c5dc519ac5d8dffd3741210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffffe3e26e9dde6f4849ceea70594341054de5334599fdfcd6d25aa76acdba768bd3000000006b48304502210091836c6cb4c786bd3b74b73e579ddf8b843ba51841e5675fa53608449b67371802203de75f32b684cfe2d2e9cd424ea6eb4f49248e6698365c9364ebf84cd6e50eab41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffffe5546febb84e3cc57bd2d5517762bd1977d6c9cef2a76f5593c06b2da70776d4000000006b483045022100e7727d9d26c645282553aef27947ad6795bc89b505ad089d617b6f696399352802206c736524a1410ed3e30cf1127f7f02c9a249392f8f8e7c670250472909d1c0d641210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffffe757efbe42d1e2186a2589e2134b051158ae98c6648a09644b4c791f8146f233010000006a47304402203fe78ad5aaeefab7b3b2277eefc4a2ace9c2e92694b46bf4a76927bf2b82017102200ded59336aba269a54865d9fdd99e72081c0318ccbc37bc0fc0c72b60ae3538241210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff577272c3ce25f3e01b4dc2f77ef86b9d47afcc1122882c60ad242591d215f925000000006a4730440220670af03605b9495c8ecee357889ceeb137dadaa1662136fdc55c28fe9434e3c60220285195a62811941745a9f93e136e59c96b81d5b0d9525f3d16d001bc0f6fa9bb41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dfffffffff2e1c04b4414f7c2717d2cf7d4ce5e2627814c4785ef22a706d038744d4b04c9000000006a47304402203f822a0b207ed49e6918663133a18037c24498c2f770c2649333a32f523e259d02203afc42a79d0da123b67f814effeee7c05c7996ea829b3cfa46c5c2e74209c09641210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffffb9aa0370dde54cb9e23e695844e93f1b865ed57df17fde77d9e09a01f0065304000000006b483045022100a7a2cf838a13a19f0e443ca35ac5ee3d55f70edca992f98402a84d4ab5ae1ad90220644a02c746eae7b44a4600199ecbf69f3b0f0bdf8479f461c482d67ef4a84e7641210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff758e9f5d544862ad0d138e4f8a8fdf6deaec4bb28411cbfddc9e42577e265214000000006a4730440220290701c797eb52ad6721db615c7d6f623c0200be0e6d6802df68c527655475450220446c4a4da9a0df5efcb57711ad61cf6167dfdda937bd0477189be8afedaedd0541210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff02526ccd1d000000001976a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688acd206db2c000000001976a9146e1da64f04fc29dbe0b8d33a341e05e3afc586eb88ac00000000', blockhash: '00000000000000008f563edf8604e537fe0d1e80f1c7c2d97dd094824f804ba3', confirmations: 5637, time: 1612567121, blocktime: 1612567121, height: 672077, address: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', }, { txid: 'dd35690b0cefd24dcc08acba8694ecd49293f365a81372cb66c8f1c1291d97c5', hash: 'dd35690b0cefd24dcc08acba8694ecd49293f365a81372cb66c8f1c1291d97c5', version: 2, size: 224, locktime: 0, vin: [ { txid: 'd3e1b8a65f9d50363cad9a496f7cecab59c9415dd9bcfd6f56c0c5dd4dffa7af', vout: 1, scriptSig: { asm: '3045022100fb14c794778e33aa66b861e85650f07e802da8b257cc37ac9dc1ac6346a0171d022051d79d2fc81bcb5bc3c7c7025d4222ecc2060cbdbf71a6fb2c7856b2eeaef7dc[ALL|FORKID] 02e4af47715f4db1d2a8d686be40c42bba5e70d715e470314181730e797be2324b', hex: '483045022100fb14c794778e33aa66b861e85650f07e802da8b257cc37ac9dc1ac6346a0171d022051d79d2fc81bcb5bc3c7c7025d4222ecc2060cbdbf71a6fb2c7856b2eeaef7dc412102e4af47715f4db1d2a8d686be40c42bba5e70d715e470314181730e797be2324b', }, sequence: 4294967295, address: 'bitcoincash:qzekdmmurl75aazj6uj4vc68yrxgws0pms30lsw8de', value: 0.61760311, }, ], vout: [ { value: 0, n: 0, scriptPubKey: { - asm: 'OP_RETURN 621 74657374696e67206d657373616765203132', - hex: '6a026d021274657374696e67206d657373616765203132', + asm: 'OP_RETURN 62696e676f656c65637472756d', + hex: '6a0d62696e676f656c65637472756d', type: 'nulldata', }, }, { value: 0.61759811, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 09401a690d52252acd1152c2ddd36c5081dff574 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91409401a690d52252acd1152c2ddd36c5081dff57488ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qqy5qxnfp4fz22kdz9fv9hwnd3ggrhl4wsekqyswf0', ], }, }, ], hex: '0200000001afa7ff4dddc5c0566ffdbcd95d41c959abec7c6f499aad3c36509d5fa6b8e1d3010000006b483045022100fb14c794778e33aa66b861e85650f07e802da8b257cc37ac9dc1ac6346a0171d022051d79d2fc81bcb5bc3c7c7025d4222ecc2060cbdbf71a6fb2c7856b2eeaef7dc412102e4af47715f4db1d2a8d686be40c42bba5e70d715e470314181730e797be2324bffffffff020000000000000000176a026d021274657374696e67206d6573736167652031324361ae03000000001976a91409401a690d52252acd1152c2ddd36c5081dff57488ac00000000', blockhash: '00000000000000000cbd73d616ecdd107a92d33aee5406ce05141231a76d408a', confirmations: 59, time: 1635507345, blocktime: 1635507345, }, { txid: '5adc33b5c0509b31c6da359177b19467c443bdc4dd37c283c0f87244c0ad63af', hash: '5adc33b5c0509b31c6da359177b19467c443bdc4dd37c283c0f87244c0ad63af', version: 2, size: 223, locktime: 0, vin: [ { txid: '2e1c5d1060bf5216678e31ed52d2ca564b81a34ac1a10749c5e124d25ec3c7a2', vout: 0, scriptSig: { asm: '304402205f6f73369ee558a8dd149480dda3d5417aab3c9bc2c4ff97aaebfc2768ceaded022052d7e2cfa0743205db27ac0cd29bcae110f1ca00aeedb6f88694901a7379dc65[ALL|FORKID] 0320b7867e815a2b00fa935a44a4c348299f7171995c8470d8221e6485da521164', hex: '47304402205f6f73369ee558a8dd149480dda3d5417aab3c9bc2c4ff97aaebfc2768ceaded022052d7e2cfa0743205db27ac0cd29bcae110f1ca00aeedb6f88694901a7379dc6541210320b7867e815a2b00fa935a44a4c348299f7171995c8470d8221e6485da521164', }, sequence: 4294967295, address: 'bitcoincash:qrxkkzsmrxcjmz8x90fx2uztt83cuu0u25vrmw66jk', value: 0.00005, }, ], vout: [ { value: 0, n: 0, scriptPubKey: { - asm: 'OP_RETURN 621 74657374696e67206d657373616765203133', - hex: '6a026d021274657374696e67206d657373616765203133', + asm: 'OP_RETURN 1650553856 63617368746162756c6172', + hex: '6a04007461620b63617368746162756c6172', type: 'nulldata', }, + value: '0', }, { value: 0.000045, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 b366ef7c1ffd4ef452d72556634720cc8741e1dc OP_EQUALVERIFY OP_CHECKSIG', hex: '76a914b366ef7c1ffd4ef452d72556634720cc8741e1dc88ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qzekdmmurl75aazj6uj4vc68yrxgws0pms30lsw8de', ], }, }, ], hex: '0200000001a2c7c35ed224e1c54907a1c14aa3814b56cad252ed318e671652bf60105d1c2e000000006a47304402205f6f73369ee558a8dd149480dda3d5417aab3c9bc2c4ff97aaebfc2768ceaded022052d7e2cfa0743205db27ac0cd29bcae110f1ca00aeedb6f88694901a7379dc6541210320b7867e815a2b00fa935a44a4c348299f7171995c8470d8221e6485da521164ffffffff020000000000000000176a026d021274657374696e67206d65737361676520313394110000000000001976a914b366ef7c1ffd4ef452d72556634720cc8741e1dc88ac00000000', blockhash: '000000000000000012c00755aab6cdef0806ebe24da10d78574c67558d3d816b', confirmations: 70, time: 1635511136, blocktime: 1635511136, }, ]; diff --git a/web/cashtab/src/hooks/useBCH.js b/web/cashtab/src/hooks/useBCH.js index ca266e8a0..5176e507e 100644 --- a/web/cashtab/src/hooks/useBCH.js +++ b/web/cashtab/src/hooks/useBCH.js @@ -1,1136 +1,1155 @@ import BigNumber from 'bignumber.js'; -import { currency } from '@components/Common/Ticker'; +import { + currency, + isCashtabOutput, + isEtokenOutput, + extractCashtabMessage, + extractExternalMessage, +} from '@components/Common/Ticker'; import { isValidTokenStats } from '@utils/validation'; import SlpWallet from 'minimal-slp-wallet'; import { toSmallestDenomination, fromSmallestDenomination, batchArray, flattenBatchedHydratedUtxos, isValidStoredWallet, checkNullUtxosForTokenStatus, confirmNonEtokenUtxos, } from '@utils/cashMethods'; export default function useBCH() { const SEND_BCH_ERRORS = { INSUFFICIENT_FUNDS: 0, NETWORK_ERROR: 1, INSUFFICIENT_PRIORITY: 66, // ~insufficient fee DOUBLE_SPENDING: 18, MAX_UNCONFIRMED_TXS: 64, }; const getRestUrl = (apiIndex = 0) => { const apiString = process.env.REACT_APP_NETWORK === `mainnet` ? process.env.REACT_APP_BCHA_APIS : process.env.REACT_APP_BCHA_APIS_TEST; const apiArray = apiString.split(','); return apiArray[apiIndex]; }; - // filter out prefixes for OP_RETURN encoded messages - // Note: only for use with encoded message strings - const removeOpReturnPrefixes = asmStr => { - if (asmStr.includes(' 621')) { - //strip out the 621 (6d02) prefix if exists - asmStr = asmStr.replace(' 621', ''); - } - return asmStr; - }; - const flattenTransactions = ( txHistory, txCount = currency.txHistoryCount, ) => { /* Convert txHistory, format [{address: '', transactions: [{height: '', tx_hash: ''}, ...{}]}, {}, {}] to flatTxHistory [{txid: '', blockheight: '', address: ''}] sorted by blockheight, newest transactions to oldest transactions */ let flatTxHistory = []; let includedTxids = []; for (let i = 0; i < txHistory.length; i += 1) { const { address, transactions } = txHistory[i]; for (let j = transactions.length - 1; j >= 0; j -= 1) { let flatTx = {}; flatTx.address = address; // If tx is unconfirmed, give arbitrarily high blockheight flatTx.height = transactions[j].height <= 0 ? 10000000 : transactions[j].height; flatTx.txid = transactions[j].tx_hash; // Only add this tx if the same transaction is not already in the array // This edge case can happen with older wallets, txs can be on multiple paths if (!includedTxids.includes(flatTx.txid)) { includedTxids.push(flatTx.txid); flatTxHistory.push(flatTx); } } } // Sort with most recent transaction at index 0 flatTxHistory.sort((a, b) => b.height - a.height); // Only return 10 return flatTxHistory.splice(0, txCount); }; const parseTxData = txData => { /* Desired output [ { txid: '', type: send, receive receivingAddress: '', quantity: amount bcha token: true/false tokenInfo: { tokenId: tokenQty: txType: mint, send, other } opReturnMessage: 'message extracted from asm' or '' } ] */ const parsedTxHistory = []; for (let i = 0; i < txData.length; i += 1) { const tx = txData[i]; const parsedTx = {}; // Move over info that does not need to be calculated parsedTx.txid = tx.txid; parsedTx.height = tx.height; let destinationAddress = tx.address; // If this tx had too many inputs to be parsed by getTxDataWithPassThrough, skip it // When this occurs, the tx will only have txid and height // So, it will not have 'vin' if (!Object.keys(tx).includes('vin')) { // Populate as a limited-info tx that can be expanded in a block explorer parsedTxHistory.push(parsedTx); continue; } parsedTx.confirmations = tx.confirmations; parsedTx.blocktime = tx.blocktime; let amountSent = 0; let amountReceived = 0; let opReturnMessage = ''; + let isCashtabMessage = false; // Assume an incoming transaction let outgoingTx = false; let tokenTx = false; + let substring = ''; // If vin includes tx address, this is an outgoing tx // Note that with bch-input data, we do not have input amounts for (let j = 0; j < tx.vin.length; j += 1) { const thisInput = tx.vin[j]; if (thisInput.address === tx.address) { // This is an outgoing transaction outgoingTx = true; } } // Iterate over vout to find how much was sent or received for (let j = 0; j < tx.vout.length; j += 1) { const thisOutput = tx.vout[j]; // If there is no addresses object in the output, it's either an OP_RETURN msg or token tx if ( !Object.keys(thisOutput.scriptPubKey).includes('addresses') ) { - let asm = thisOutput.scriptPubKey.asm; - if (asm.includes('OP_RETURN 5262419')) { - // assume this is an eToken tx for now - // future diffs will add additional NFT parsing logic in this segment + let hex = thisOutput.scriptPubKey.hex; + + if (isEtokenOutput(hex)) { + // this is an eToken transaction tokenTx = true; + } else if (isCashtabOutput(hex)) { + // this is a cashtab.com generated message + try { + substring = extractCashtabMessage(hex); + opReturnMessage = Buffer.from(substring, 'hex'); + isCashtabMessage = true; + } catch (err) { + // soft error if an unexpected or invalid cashtab hex is encountered + opReturnMessage = ''; + console.log( + 'useBCH.parsedTxHistory() error: invalid cashtab msg hex: ' + + substring, + ); + } } else { - // if this is not an eToken tx and does not contain addresses, then assume encoded message - asm = removeOpReturnPrefixes(asm); - let msgBody = asm.substr(asm.indexOf(' ') + 1); // extract everything after the OP_RETURN opcode + // this is an externally generated message try { - opReturnMessage = Buffer.from(msgBody, 'hex'); + substring = extractExternalMessage(hex); + opReturnMessage = Buffer.from(substring, 'hex'); } catch (err) { + // soft error if an unexpected or invalid cashtab hex is encountered opReturnMessage = ''; + console.log( + 'useBCH.parsedTxHistory() error: invalid external msg hex: ' + + substring, + ); } } continue; // skipping the remainder of tx data parsing logic in both token and OP_RETURN tx cases } if ( thisOutput.scriptPubKey.addresses && thisOutput.scriptPubKey.addresses[0] === tx.address ) { if (outgoingTx) { // This amount is change continue; } amountReceived += thisOutput.value; } else if (outgoingTx) { amountSent += thisOutput.value; // Assume there's only one destination address, i.e. it was sent by a Cashtab wallet destinationAddress = thisOutput.scriptPubKey.addresses[0]; } } // Construct parsedTx parsedTx.amountSent = amountSent; parsedTx.amountReceived = amountReceived; parsedTx.tokenTx = tokenTx; parsedTx.outgoingTx = outgoingTx; parsedTx.destinationAddress = destinationAddress; parsedTx.opReturnMessage = opReturnMessage; - + parsedTx.isCashtabMessage = isCashtabMessage; parsedTxHistory.push(parsedTx); } return parsedTxHistory; }; const getTxHistory = async (BCH, addresses) => { let txHistoryResponse; try { //console.log(`API Call: BCH.Electrumx.utxo(addresses)`); //console.log(addresses); txHistoryResponse = await BCH.Electrumx.transactions(addresses); //console.log(`BCH.Electrumx.transactions(addresses) succeeded`); //console.log(`txHistoryResponse`, txHistoryResponse); if (txHistoryResponse.success && txHistoryResponse.transactions) { return txHistoryResponse.transactions; } else { // eslint-disable-next-line no-throw-literal throw new Error('Error in getTxHistory'); } } catch (err) { console.log(`Error in BCH.Electrumx.transactions(addresses):`); console.log(err); return err; } }; const getTxDataWithPassThrough = async (BCH, flatTx) => { // necessary as BCH.RawTransactions.getTxData does not return address or blockheight let txDataWithPassThrough = {}; try { txDataWithPassThrough = await BCH.RawTransactions.getTxData( flatTx.txid, ); } catch (err) { console.log( `Error in BCH.RawTransactions.getTxData(${flatTx.txid})`, ); console.log(err); // Include txid if you don't get it from the attempted response txDataWithPassThrough.txid = flatTx.txid; } txDataWithPassThrough.height = flatTx.height; txDataWithPassThrough.address = flatTx.address; return txDataWithPassThrough; }; const getTxData = async (BCH, txHistory) => { // Flatten tx history let flatTxs = flattenTransactions(txHistory); // Build array of promises to get tx data for all 10 transactions let txDataPromises = []; for (let i = 0; i < flatTxs.length; i += 1) { const txDataPromise = await getTxDataWithPassThrough( BCH, flatTxs[i], ); txDataPromises.push(txDataPromise); } // Get txData for the 10 most recent transactions let txDataPromiseResponse; try { txDataPromiseResponse = await Promise.all(txDataPromises); const parsed = parseTxData(txDataPromiseResponse); return parsed; } catch (err) { console.log(`Error in Promise.all(txDataPromises):`); console.log(err); return err; } }; const parseTokenInfoForTxHistory = (BCH, parsedTx, tokenInfo) => { // Address at which the eToken was received const { destinationAddress } = parsedTx; // Here in cashtab, destinationAddress is in bitcoincash: format // In the API response of tokenInfo, this will be in simpleledger: format // So, must convert to simpleledger const receivingSlpAddress = BCH.SLP.Address.toSLPAddress(destinationAddress); const { transactionType, sendInputsFull, sendOutputsFull } = tokenInfo; const sendingTokenAddresses = []; // Scan over inputs to find out originating addresses for (let i = 0; i < sendInputsFull.length; i += 1) { const sendingAddress = sendInputsFull[i].address; sendingTokenAddresses.push(sendingAddress); } // Scan over outputs to find out how much was sent let qtySent = new BigNumber(0); let qtyReceived = new BigNumber(0); for (let i = 0; i < sendOutputsFull.length; i += 1) { if (sendingTokenAddresses.includes(sendOutputsFull[i].address)) { // token change and should be ignored, unless it's a genesis transaction // then this is the amount created if (transactionType === 'GENESIS') { qtyReceived = qtyReceived.plus( new BigNumber(sendOutputsFull[i].amount), ); } continue; } if (parsedTx.outgoingTx) { qtySent = qtySent.plus( new BigNumber(sendOutputsFull[i].amount), ); } else { // Only if this matches the receiving address if (sendOutputsFull[i].address === receivingSlpAddress) { qtyReceived = qtyReceived.plus( new BigNumber(sendOutputsFull[i].amount), ); } } } const cashtabTokenInfo = {}; cashtabTokenInfo.qtySent = qtySent.toString(); cashtabTokenInfo.qtyReceived = qtyReceived.toString(); cashtabTokenInfo.tokenId = tokenInfo.tokenIdHex; cashtabTokenInfo.tokenName = tokenInfo.tokenName; cashtabTokenInfo.tokenTicker = tokenInfo.tokenTicker; cashtabTokenInfo.transactionType = transactionType; return cashtabTokenInfo; }; const addTokenTxDataToSingleTx = async (BCH, parsedTx) => { // Accept one parsedTx // If it's not a token tx, just return it as is and do not parse for token data if (!parsedTx.tokenTx) { return parsedTx; } // If it could be a token tx, do an API call to get token info and return it let tokenData; try { tokenData = await BCH.SLP.Utils.txDetails(parsedTx.txid); } catch (err) { console.log( `Error in parsing BCH.SLP.Utils.txDetails(${parsedTx.txid})`, ); console.log(err); // This is not a token tx parsedTx.tokenTx = false; return parsedTx; } const { tokenInfo } = tokenData; parsedTx.tokenInfo = parseTokenInfoForTxHistory( BCH, parsedTx, tokenInfo, ); return parsedTx; }; const addTokenTxData = async (BCH, parsedTxs) => { // Collect all txids for token transactions into array of promises // Promise.all to get their tx history // Add a tokeninfo object to parsedTxs for token txs // Get txData for the 10 most recent transactions // Build array of promises to get tx data for all 10 transactions let tokenTxDataPromises = []; for (let i = 0; i < parsedTxs.length; i += 1) { const txDataPromise = await addTokenTxDataToSingleTx( BCH, parsedTxs[i], ); tokenTxDataPromises.push(txDataPromise); } let tokenTxDataPromiseResponse; try { tokenTxDataPromiseResponse = await Promise.all(tokenTxDataPromises); return tokenTxDataPromiseResponse; } catch (err) { console.log(`Error in Promise.all(tokenTxDataPromises):`); console.log(err); return err; } }; // Split out the BCH.Electrumx.utxo(addresses) call from the getSlpBalancesandUtxos function // If utxo set has not changed, you do not need to hydrate the utxo set // This drastically reduces calls to the API const getUtxos = async (BCH, addresses) => { let utxosResponse; try { //console.log(`API Call: BCH.Electrumx.utxo(addresses)`); //console.log(addresses); utxosResponse = await BCH.Electrumx.utxo(addresses); //console.log(`BCH.Electrumx.utxo(addresses) succeeded`); //console.log(`utxosResponse`, utxosResponse); return utxosResponse.utxos; } catch (err) { console.log(`Error in BCH.Electrumx.utxo(addresses):`); return err; } }; const getHydratedUtxoDetails = async (BCH, utxos) => { const hydrateUtxosPromises = []; for (let i = 0; i < utxos.length; i += 1) { let thisAddress = utxos[i].address; let theseUtxos = utxos[i].utxos; const batchedUtxos = batchArray( theseUtxos, currency.hydrateUtxoBatchSize, ); // Iterate over each utxo in this address field for (let j = 0; j < batchedUtxos.length; j += 1) { const utxoSetForThisPromise = [ { utxos: batchedUtxos[j], address: thisAddress }, ]; const thisPromise = BCH.SLP.Utils.hydrateUtxos( utxoSetForThisPromise, ); hydrateUtxosPromises.push(thisPromise); } } let hydratedUtxoDetails; try { hydratedUtxoDetails = await Promise.all(hydrateUtxosPromises); const flattenedBatchedHydratedUtxos = flattenBatchedHydratedUtxos(hydratedUtxoDetails); return flattenedBatchedHydratedUtxos; } catch (err) { console.log(`Error in Promise.all(hydrateUtxosPromises)`); console.log(err); return err; } }; const fetchTxDataForNullUtxos = async (BCH, nullUtxos) => { // Check nullUtxos. If they aren't eToken txs, count them console.log( `Null utxos found, checking OP_RETURN fields to confirm they are not eToken txs.`, ); const txids = []; for (let i = 0; i < nullUtxos.length; i += 1) { // Batch API call to get their OP_RETURN asm info txids.push(nullUtxos[i].tx_hash); } let nullUtxoTxData; try { nullUtxoTxData = await BCH.Electrumx.txData(txids); console.log(`nullUtxoTxData`, nullUtxoTxData.transactions); // Scan tx data for each utxo to confirm they are not eToken txs const txDataResults = nullUtxoTxData.transactions; const nonEtokenUtxos = checkNullUtxosForTokenStatus(txDataResults); return nonEtokenUtxos; } catch (err) { console.log(`Error in checkNullUtxosForTokenStatus(nullUtxos)`); console.log(`nullUtxos`, nullUtxos); // If error, ignore these utxos, will be updated next utxo set refresh return []; } }; const getSlpBalancesAndUtxos = async (BCH, hydratedUtxoDetails) => { let hydratedUtxos = []; for (let i = 0; i < hydratedUtxoDetails.slpUtxos.length; i += 1) { const hydratedUtxosAtAddress = hydratedUtxoDetails.slpUtxos[i]; for (let j = 0; j < hydratedUtxosAtAddress.utxos.length; j += 1) { const hydratedUtxo = hydratedUtxosAtAddress.utxos[j]; hydratedUtxo.address = hydratedUtxosAtAddress.address; hydratedUtxos.push(hydratedUtxo); } } //console.log(`hydratedUtxos`, hydratedUtxos); // WARNING // If you hit rate limits, your above utxos object will come back with `isValid` as null, but otherwise ok // You need to throw an error before setting nonSlpUtxos and slpUtxos in this case const nullUtxos = hydratedUtxos.filter(utxo => utxo.isValid === null); if (nullUtxos.length > 0) { console.log(`${nullUtxos.length} null utxos found!`); console.log('nullUtxos', nullUtxos); const nullNonEtokenUtxos = await fetchTxDataForNullUtxos( BCH, nullUtxos, ); // Set isValid === false for nullUtxos that are confirmed non-eToken hydratedUtxos = confirmNonEtokenUtxos( hydratedUtxos, nullNonEtokenUtxos, ); } // Prevent app from treating slpUtxos as nonSlpUtxos // Must enforce === false as api will occasionally return utxo.isValid === null // Do not classify any utxos that include token information as nonSlpUtxos const nonSlpUtxos = hydratedUtxos.filter( utxo => utxo.isValid === false && utxo.value !== currency.etokenSats && !utxo.tokenName, ); // To be included in slpUtxos, the utxo must // have utxo.isValid = true // If utxo has a utxo.tokenQty field, i.e. not a minting baton, then utxo.tokenQty !== '0' const slpUtxos = hydratedUtxos.filter( utxo => utxo.isValid && !(utxo.tokenQty === '0'), ); let tokensById = {}; slpUtxos.forEach(slpUtxo => { let token = tokensById[slpUtxo.tokenId]; if (token) { // Minting baton does nto have a slpUtxo.tokenQty type if (slpUtxo.tokenQty) { token.balance = token.balance.plus( new BigNumber(slpUtxo.tokenQty), ); } //token.hasBaton = slpUtxo.transactionType === "genesis"; if (slpUtxo.utxoType && !token.hasBaton) { token.hasBaton = slpUtxo.utxoType === 'minting-baton'; } // Examples of slpUtxo /* Genesis transaction: { address: "bitcoincash:qrhzv5t79e2afc3rdutcu0d3q20gl7ul3ue58whah6" decimals: 9 height: 617564 isValid: true satoshis: 546 tokenDocumentHash: "" tokenDocumentUrl: "developer.bitcoin.com" tokenId: "6c41f244676ecfcbe3b4fabee2c72c2dadf8d74f8849afabc8a549157db69199" tokenName: "PiticoLaunch" tokenTicker: "PTCL" tokenType: 1 tx_hash: "6c41f244676ecfcbe3b4fabee2c72c2dadf8d74f8849afabc8a549157db69199" tx_pos: 2 txid: "6c41f244676ecfcbe3b4fabee2c72c2dadf8d74f8849afabc8a549157db69199" utxoType: "minting-baton" value: 546 vout: 2 } Send transaction: { address: "bitcoincash:qrhzv5t79e2afc3rdutcu0d3q20gl7ul3ue58whah6" decimals: 9 height: 655115 isValid: true satoshis: 546 tokenDocumentHash: "" tokenDocumentUrl: "developer.bitcoin.com" tokenId: "6c41f244676ecfcbe3b4fabee2c72c2dadf8d74f8849afabc8a549157db69199" tokenName: "PiticoLaunch" tokenQty: 1.123456789 tokenTicker: "PTCL" tokenType: 1 transactionType: "send" tx_hash: "dea400f963bc9f51e010f88533010f8d1f82fc2bcc485ff8500c3a82b25abd9e" tx_pos: 1 txid: "dea400f963bc9f51e010f88533010f8d1f82fc2bcc485ff8500c3a82b25abd9e" utxoType: "token" value: 546 vout: 1 } */ } else { token = {}; token.info = slpUtxo; token.tokenId = slpUtxo.tokenId; if (slpUtxo.tokenQty) { token.balance = new BigNumber(slpUtxo.tokenQty); } else { token.balance = new BigNumber(0); } if (slpUtxo.utxoType) { token.hasBaton = slpUtxo.utxoType === 'minting-baton'; } else { token.hasBaton = false; } tokensById[slpUtxo.tokenId] = token; } }); const tokens = Object.values(tokensById); // console.log(`tokens`, tokens); return { tokens, nonSlpUtxos, slpUtxos, }; }; const calcFee = ( BCH, utxos, p2pkhOutputNumber = 2, satoshisPerByte = currency.defaultFee, ) => { const byteCount = BCH.BitcoinCash.getByteCount( { P2PKH: utxos.length }, { P2PKH: p2pkhOutputNumber }, ); const txFee = Math.ceil(satoshisPerByte * byteCount); return txFee; }; const createToken = async (BCH, wallet, feeInSatsPerByte, configObj) => { try { // Throw error if wallet does not have utxo set in state if (!isValidStoredWallet(wallet)) { const walletError = new Error(`Invalid wallet`); throw walletError; } const utxos = wallet.state.slpBalancesAndUtxos.nonSlpUtxos; const CREATION_ADDR = wallet.Path1899.cashAddress; const inputUtxos = []; let transactionBuilder; // instance of transaction builder if (process.env.REACT_APP_NETWORK === `mainnet`) transactionBuilder = new BCH.TransactionBuilder(); else transactionBuilder = new BCH.TransactionBuilder('testnet'); let originalAmount = new BigNumber(0); let txFee = 0; for (let i = 0; i < utxos.length; i++) { const utxo = utxos[i]; originalAmount = originalAmount.plus(new BigNumber(utxo.value)); const vout = utxo.vout; const txid = utxo.txid; // add input with txid and index of vout transactionBuilder.addInput(txid, vout); inputUtxos.push(utxo); txFee = calcFee(BCH, inputUtxos, 3, feeInSatsPerByte); if ( originalAmount .minus(new BigNumber(currency.etokenSats)) .minus(new BigNumber(txFee)) .gte(0) ) { break; } } // amount to send back to the remainder address. const remainder = originalAmount .minus(new BigNumber(currency.etokenSats)) .minus(new BigNumber(txFee)); if (remainder.lt(0)) { const error = new Error(`Insufficient funds`); error.code = SEND_BCH_ERRORS.INSUFFICIENT_FUNDS; throw error; } // Generate the OP_RETURN entry for an SLP GENESIS transaction. const script = BCH.SLP.TokenType1.generateGenesisOpReturn(configObj); // OP_RETURN needs to be the first output in the transaction. transactionBuilder.addOutput(script, 0); // add output w/ address and amount to send transactionBuilder.addOutput(CREATION_ADDR, currency.etokenSats); // Send change to own address if (remainder.gte(new BigNumber(currency.etokenSats))) { transactionBuilder.addOutput( CREATION_ADDR, parseInt(remainder), ); } // Sign the transactions with the HD node. for (let i = 0; i < inputUtxos.length; i++) { const utxo = inputUtxos[i]; transactionBuilder.sign( i, BCH.ECPair.fromWIF(utxo.wif), undefined, transactionBuilder.hashTypes.SIGHASH_ALL, utxo.value, ); } // build tx const tx = transactionBuilder.build(); // output rawhex const hex = tx.toHex(); // Broadcast transaction to the network const txidStr = await BCH.RawTransactions.sendRawTransaction([hex]); if (txidStr && txidStr[0]) { console.log(`${currency.ticker} txid`, txidStr[0]); } let link; if (process.env.REACT_APP_NETWORK === `mainnet`) { link = `${currency.tokenExplorerUrl}/tx/${txidStr}`; } else { link = `${currency.blockExplorerUrlTestnet}/tx/${txidStr}`; } //console.log(`link`, link); return link; } catch (err) { if (err.error === 'insufficient priority (code 66)') { err.code = SEND_BCH_ERRORS.INSUFFICIENT_PRIORITY; } else if (err.error === 'txn-mempool-conflict (code 18)') { err.code = SEND_BCH_ERRORS.DOUBLE_SPENDING; } else if (err.error === 'Network Error') { err.code = SEND_BCH_ERRORS.NETWORK_ERROR; } else if ( err.error === 'too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)' ) { err.code = SEND_BCH_ERRORS.MAX_UNCONFIRMED_TXS; } console.log(`error: `, err); throw err; } }; // No unit tests for this function as it is only an API wrapper // Return false if do not get a valid response const getTokenStats = async (BCH, tokenId) => { let tokenStats; try { tokenStats = await BCH.SLP.Utils.tokenStats(tokenId); if (isValidTokenStats(tokenStats)) { return tokenStats; } } catch (err) { console.log(`Error fetching token stats for tokenId ${tokenId}`); console.log(err); return false; } }; const sendToken = async ( BCH, wallet, slpBalancesAndUtxos, { tokenId, amount, tokenReceiverAddress }, ) => { // Handle error of user having no BCH if (slpBalancesAndUtxos.nonSlpUtxos.length === 0) { throw new Error( `You need some ${currency.ticker} to send ${currency.tokenTicker}`, ); } const largestBchUtxo = slpBalancesAndUtxos.nonSlpUtxos.reduce( (previous, current) => previous.value > current.value ? previous : current, ); const bchECPair = BCH.ECPair.fromWIF(largestBchUtxo.wif); const tokenUtxos = slpBalancesAndUtxos.slpUtxos.filter( (utxo, index) => { if ( utxo && // UTXO is associated with a token. utxo.tokenId === tokenId && // UTXO matches the token ID. utxo.utxoType === 'token' // UTXO is not a minting baton. ) { return true; } return false; }, ); if (tokenUtxos.length === 0) { throw new Error( 'No token UTXOs for the specified token could be found.', ); } // BEGIN transaction construction. // instance of transaction builder let transactionBuilder; if (process.env.REACT_APP_NETWORK === 'mainnet') { transactionBuilder = new BCH.TransactionBuilder(); } else transactionBuilder = new BCH.TransactionBuilder('testnet'); const originalAmount = largestBchUtxo.value; transactionBuilder.addInput( largestBchUtxo.tx_hash, largestBchUtxo.tx_pos, ); let finalTokenAmountSent = new BigNumber(0); let tokenAmountBeingSentToAddress = new BigNumber(amount); let tokenUtxosBeingSpent = []; for (let i = 0; i < tokenUtxos.length; i++) { finalTokenAmountSent = finalTokenAmountSent.plus( new BigNumber(tokenUtxos[i].tokenQty), ); transactionBuilder.addInput( tokenUtxos[i].tx_hash, tokenUtxos[i].tx_pos, ); tokenUtxosBeingSpent.push(tokenUtxos[i]); if (tokenAmountBeingSentToAddress.lte(finalTokenAmountSent)) { break; } } const slpSendObj = BCH.SLP.TokenType1.generateSendOpReturn( tokenUtxosBeingSpent, tokenAmountBeingSentToAddress.toString(), ); const slpData = slpSendObj.script; // Add OP_RETURN as first output. transactionBuilder.addOutput(slpData, 0); // Send dust transaction representing tokens being sent. transactionBuilder.addOutput( BCH.SLP.Address.toLegacyAddress(tokenReceiverAddress), currency.etokenSats, ); // Return any token change back to the sender. if (slpSendObj.outputs > 1) { // Change goes back to where slp utxo came from transactionBuilder.addOutput( BCH.SLP.Address.toLegacyAddress( tokenUtxosBeingSpent[0].address, ), currency.etokenSats, ); } // get byte count to calculate fee. paying 1 sat // Note: This may not be totally accurate. Just guessing on the byteCount size. const txFee = calcFee( BCH, tokenUtxosBeingSpent, 5, 1.1 * currency.defaultFee, ); // amount to send back to the sending address. It's the original amount - 1 sat/byte for tx size const remainder = originalAmount - txFee - currency.etokenSats * 2; if (remainder < 1) { throw new Error('Selected UTXO does not have enough satoshis'); } // Last output: send the BCH change back to the wallet. // Send it back from whence it came transactionBuilder.addOutput( BCH.Address.toLegacyAddress(largestBchUtxo.address), remainder, ); // Sign the transaction with the private key for the BCH UTXO paying the fees. let redeemScript; transactionBuilder.sign( 0, bchECPair, redeemScript, transactionBuilder.hashTypes.SIGHASH_ALL, originalAmount, ); // Sign each token UTXO being consumed. for (let i = 0; i < tokenUtxosBeingSpent.length; i++) { const thisUtxo = tokenUtxosBeingSpent[i]; const accounts = [wallet.Path245, wallet.Path145, wallet.Path1899]; const utxoEcPair = BCH.ECPair.fromWIF( accounts .filter(acc => acc.cashAddress === thisUtxo.address) .pop().fundingWif, ); transactionBuilder.sign( 1 + i, utxoEcPair, redeemScript, transactionBuilder.hashTypes.SIGHASH_ALL, thisUtxo.value, ); } // build tx const tx = transactionBuilder.build(); // output rawhex const hex = tx.toHex(); // console.log(`Transaction raw hex: `, hex); // END transaction construction. const txidStr = await BCH.RawTransactions.sendRawTransaction([hex]); if (txidStr && txidStr[0]) { console.log(`${currency.tokenTicker} txid`, txidStr[0]); } let link; if (process.env.REACT_APP_NETWORK === `mainnet`) { link = `${currency.blockExplorerUrl}/tx/${txidStr}`; } else { link = `${currency.blockExplorerUrlTestnet}/tx/${txidStr}`; } //console.log(`link`, link); return link; }; const signPkMessage = async (BCH, pk, message) => { try { let signature = await BCH.BitcoinCash.signMessageWithPrivKey( pk, message, ); return signature; } catch (err) { console.log(`useBCH.signPkMessage() error: `, err); throw err; } }; const sendBch = async ( BCH, wallet, utxos, destinationAddress, sendAmount, feeInSatsPerByte, optionalOpReturnMsg, ) => { try { if (!sendAmount) { return null; } const value = new BigNumber(sendAmount); // If user is attempting to send less than minimum accepted by the backend if ( value.lt( new BigNumber( fromSmallestDenomination(currency.dustSats).toString(), ), ) ) { // Throw the same error given by the backend attempting to broadcast such a tx throw new Error('dust'); } const inputUtxos = []; let transactionBuilder; // instance of transaction builder if (process.env.REACT_APP_NETWORK === `mainnet`) transactionBuilder = new BCH.TransactionBuilder(); else transactionBuilder = new BCH.TransactionBuilder('testnet'); const satoshisToSend = toSmallestDenomination(value); // Throw validation error if toSmallestDenomination returns false if (!satoshisToSend) { const error = new Error( `Invalid decimal places for send amount`, ); throw error; } // Start of building the OP_RETURN output. // only build the OP_RETURN output if the user supplied it if ( typeof optionalOpReturnMsg !== 'undefined' && optionalOpReturnMsg.trim() !== '' ) { const script = [ - BCH.Script.opcodes.OP_RETURN, - Buffer.from('6d02', 'hex'), + BCH.Script.opcodes.OP_RETURN, // 6a + Buffer.from( + currency.opReturn.appPrefixesHex.cashtab, + 'hex', + ), // 00746162 Buffer.from(optionalOpReturnMsg), ]; const data = BCH.Script.encode(script); transactionBuilder.addOutput(data, 0); } // End of building the OP_RETURN output. let originalAmount = new BigNumber(0); let txFee = 0; for (let i = 0; i < utxos.length; i++) { const utxo = utxos[i]; originalAmount = originalAmount.plus(utxo.value); const vout = utxo.vout; const txid = utxo.txid; // add input with txid and index of vout transactionBuilder.addInput(txid, vout); inputUtxos.push(utxo); txFee = calcFee(BCH, inputUtxos, 2, feeInSatsPerByte); if (originalAmount.minus(satoshisToSend).minus(txFee).gte(0)) { break; } } // Get change address from sending utxos // fall back to what is stored in wallet let REMAINDER_ADDR; // Validate address let isValidChangeAddress; try { REMAINDER_ADDR = inputUtxos[0].address; isValidChangeAddress = BCH.Address.isCashAddress(REMAINDER_ADDR); } catch (err) { isValidChangeAddress = false; } if (!isValidChangeAddress) { REMAINDER_ADDR = wallet.Path1899.cashAddress; } // amount to send back to the remainder address. const remainder = originalAmount.minus(satoshisToSend).minus(txFee); if (remainder.lt(0)) { const error = new Error(`Insufficient funds`); error.code = SEND_BCH_ERRORS.INSUFFICIENT_FUNDS; throw error; } // add output w/ address and amount to send transactionBuilder.addOutput( BCH.Address.toCashAddress(destinationAddress), parseInt(toSmallestDenomination(value)), ); if (remainder.gte(new BigNumber(currency.dustSats))) { transactionBuilder.addOutput( REMAINDER_ADDR, parseInt(remainder), ); } // Sign the transactions with the HD node. for (let i = 0; i < inputUtxos.length; i++) { const utxo = inputUtxos[i]; transactionBuilder.sign( i, BCH.ECPair.fromWIF(utxo.wif), undefined, transactionBuilder.hashTypes.SIGHASH_ALL, utxo.value, ); } // build tx const tx = transactionBuilder.build(); // output rawhex const hex = tx.toHex(); // Broadcast transaction to the network const txidStr = await BCH.RawTransactions.sendRawTransaction([hex]); if (txidStr && txidStr[0]) { console.log(`${currency.ticker} txid`, txidStr[0]); } let link; if (process.env.REACT_APP_NETWORK === `mainnet`) { link = `${currency.blockExplorerUrl}/tx/${txidStr}`; } else { link = `${currency.blockExplorerUrlTestnet}/tx/${txidStr}`; } //console.log(`link`, link); return link; } catch (err) { if (err.error === 'insufficient priority (code 66)') { err.code = SEND_BCH_ERRORS.INSUFFICIENT_PRIORITY; } else if (err.error === 'txn-mempool-conflict (code 18)') { err.code = SEND_BCH_ERRORS.DOUBLE_SPENDING; } else if (err.error === 'Network Error') { err.code = SEND_BCH_ERRORS.NETWORK_ERROR; } else if ( err.error === 'too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)' ) { err.code = SEND_BCH_ERRORS.MAX_UNCONFIRMED_TXS; } console.log(`error: `, err); throw err; } }; const getBCH = (apiIndex = 0) => { let ConstructedSlpWallet; ConstructedSlpWallet = new SlpWallet('', { restURL: getRestUrl(apiIndex), }); return ConstructedSlpWallet.bchjs; }; return { getBCH, calcFee, getUtxos, getHydratedUtxoDetails, getSlpBalancesAndUtxos, getTxHistory, flattenTransactions, parseTxData, addTokenTxData, parseTokenInfoForTxHistory, getTxData, getRestUrl, signPkMessage, sendBch, sendToken, createToken, getTokenStats, }; }