diff --git a/web/cashtab/src/components/Airdrop/Airdrop.js b/web/cashtab/src/components/Airdrop/Airdrop.js index e22e5328c..0317e5043 100644 --- a/web/cashtab/src/components/Airdrop/Airdrop.js +++ b/web/cashtab/src/components/Airdrop/Airdrop.js @@ -1,846 +1,848 @@ import React, { useState, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import PropTypes from 'prop-types'; import BigNumber from 'bignumber.js'; import styled from 'styled-components'; import { WalletContext } from 'utils/context'; import { AntdFormWrapper, DestinationAddressMulti, InputAmountSingle, } from 'components/Common/EnhancedInputs'; import { CustomCollapseCtn } from 'components/Common/StyledCollapse'; import { Form, Alert, Input, Modal, Spin, Progress } from 'antd'; const { TextArea } = Input; import { Row, Col, Switch } from 'antd'; import { SmartButton } from 'components/Common/PrimaryButton'; import useBCH from 'hooks/useBCH'; import { errorNotification, generalNotification, } from 'components/Common/Notifications'; import { currency } from 'components/Common/Ticker.js'; import BalanceHeader from 'components/Common/BalanceHeader'; import BalanceHeaderFiat from 'components/Common/BalanceHeaderFiat'; import { getWalletState, convertEtokenToEcashAddr, fromSmallestDenomination, convertToEcashPrefix, convertEcashtoEtokenAddr, } from 'utils/cashMethods'; import { isValidTokenId, isValidXecAirdrop, isValidAirdropOutputsArray, isValidAirdropExclusionArray, } from 'utils/validation'; import { CustomSpinner } from 'components/Common/CustomIcons'; import * as etokenList from 'etoken-list'; import { ZeroBalanceHeader, SidePaddingCtn, WalletInfoCtn, } from 'components/Common/Atoms'; import WalletLabel from 'components/Common/WalletLabel.js'; import { Link } from 'react-router-dom'; const AirdropActions = styled.div` text-align: center; width: 100%; padding: 10px; border-radius: 5px; a { color: ${props => props.theme.contrast}; margin: 0; font-size: 11px; border: 1px solid ${props => props.theme.contrast}; border-radius: 5px; padding: 2px 10px; opacity: 0.6; } a:hover { opacity: 1; border-color: ${props => props.theme.eCashBlue}; color: ${props => props.theme.contrast}; background: ${props => props.theme.eCashBlue}; } ${({ received, ...props }) => received && ` text-align: left; background: ${props.theme.receivedMessage}; `} `; const AirdropOptions = styled.div` text-align: left; color: ${props => props.theme.contrast}; `; const StyledModal = styled(Modal)` .ant-progress-text { color: ${props => props.theme.lightWhite} !important; } `; // Note jestBCH is only used for unit tests; BCHJS must be mocked for jest const Airdrop = ({ jestBCH, passLoadingStatus }) => { const ContextValue = React.useContext(WalletContext); const { wallet, fiatPrice, cashtabSettings } = ContextValue; const location = useLocation(); const walletState = getWalletState(wallet); const { balances } = walletState; const [bchObj, setBchObj] = useState(false); const [isAirdropCalcModalVisible, setIsAirdropCalcModalVisible] = useState(false); const [airdropCalcModalProgress, setAirdropCalcModalProgress] = useState(0); // the dynamic % progress bar useEffect(() => { // jestBCH is only ever specified for unit tests, otherwise app will use getBCH(); const BCH = jestBCH ? jestBCH : getBCH(); // set the BCH instance to state, for other functions to reference setBchObj(BCH); if (location && location.state && location.state.airdropEtokenId) { setFormData({ ...formData, tokenId: location.state.airdropEtokenId, }); handleTokenIdInput({ target: { value: location.state.airdropEtokenId, }, }); } }, []); const [formData, setFormData] = useState({ tokenId: '', totalAirdrop: '', }); const [tokenIdIsValid, setTokenIdIsValid] = useState(null); const [totalAirdropIsValid, setTotalAirdropIsValid] = useState(null); const [airdropRecipients, setAirdropRecipients] = useState(''); const [airdropOutputIsValid, setAirdropOutputIsValid] = useState(true); const [etokenHolders, setEtokenHolders] = useState(parseInt(0)); const [showAirdropOutputs, setShowAirdropOutputs] = useState(false); const [ignoreOwnAddress, setIgnoreOwnAddress] = useState(false); const [ignoreRecipientsBelowDust, setIgnoreRecipientsBelowDust] = useState(false); const [ignoreMintAddress, setIgnoreMintAddress] = useState(false); // flag to reflect the exclusion list checkbox const [ignoreCustomAddresses, setIgnoreCustomAddresses] = useState(false); // the exclusion list values const [ignoreCustomAddressesList, setIgnoreCustomAddressesList] = useState(false); const [ ignoreCustomAddressesListIsValid, setIgnoreCustomAddressesListIsValid, ] = useState(false); const [ignoreCustomAddressListError, setIgnoreCustomAddressListError] = useState(false); // flag to reflect the ignore minimum etoken balance switch const [ignoreMinEtokenBalance, setIgnoreMinEtokenBalance] = useState(false); const [ignoreMinEtokenBalanceAmount, setIgnoreMinEtokenBalanceAmount] = useState(new BigNumber(0)); const [ ignoreMinEtokenBalanceAmountIsValid, setIgnoreMinEtokenBalanceAmountIsValid, ] = useState(false); const [ ignoreMinEtokenBalanceAmountError, setIgnoreMinEtokenBalanceAmountError, ] = useState(false); const { getBCH } = useBCH(); const handleTokenIdInput = e => { const { name, value } = e.target; setTokenIdIsValid(isValidTokenId(value)); setFormData(p => ({ ...p, [name]: value, })); }; const handleTotalAirdropInput = e => { const { name, value } = e.target; setTotalAirdropIsValid(isValidXecAirdrop(value)); setFormData(p => ({ ...p, [name]: value, })); }; const handleMinEtokenBalanceChange = e => { const { value } = e.target; if (new BigNumber(value).gt(new BigNumber(0))) { setIgnoreMinEtokenBalanceAmountIsValid(true); setIgnoreMinEtokenBalanceAmountError(false); } else { setIgnoreMinEtokenBalanceAmountError( 'Minimum eToken balance must be greater than 0', ); setIgnoreMinEtokenBalanceAmountIsValid(false); } setIgnoreMinEtokenBalanceAmount(value); }; const calculateXecAirdrop = async () => { // display airdrop calculation message modal setIsAirdropCalcModalVisible(true); setShowAirdropOutputs(false); // hide any previous airdrop outputs passLoadingStatus(true); setAirdropCalcModalProgress(25); // updated progress bar to 25% let latestBlock; try { latestBlock = await bchObj.Blockchain.getBlockCount(); } catch (err) { errorNotification( err, 'Error retrieving latest block height', 'bchObj.Blockchain.getBlockCount() error', ); setIsAirdropCalcModalVisible(false); passLoadingStatus(false); return; } setAirdropCalcModalProgress(50); etokenList.Config.SetUrl(currency.tokenDbUrl); let airdropList; try { airdropList = await etokenList.List.GetAddressListFor( formData.tokenId, latestBlock, true, ); } catch (err) { errorNotification( err, 'Error retrieving airdrop recipients', 'etokenList.List.GetAddressListFor() error', ); setIsAirdropCalcModalVisible(false); passLoadingStatus(false); return; } // if Ignore Own Address option is checked, then filter out from recipients list if (ignoreOwnAddress) { const ownEtokenAddress = convertToEcashPrefix( wallet.Path1899.slpAddress, ); airdropList.delete(ownEtokenAddress); } // if Ignore eToken Minter option is checked, then filter out from recipients list if (ignoreMintAddress) { // extract the eToken mint address let genesisTx; try { genesisTx = await bchObj.RawTransactions.getRawTransaction( formData.tokenId, true, ); } catch (err) { errorNotification( null, 'Unable to retrieve minting address for eToken ID: ' + formData.tokenId, 'getRawTransaction Error', ); setIsAirdropCalcModalVisible(false); passLoadingStatus(false); return; } const mintEcashAddress = convertToEcashPrefix( genesisTx.vout[1].scriptPubKey.addresses[0], ); //vout[0] is always the OP_RETURN output const mintEtokenAddress = convertEcashtoEtokenAddr(mintEcashAddress); // remove the mint address from the recipients list airdropList.delete(mintEtokenAddress); } // filter out addresses from the exclusion list if the option is checked if (ignoreCustomAddresses && ignoreCustomAddressesListIsValid) { const addressStringArray = ignoreCustomAddressesList.split(','); for (let i = 0; i < addressStringArray.length; i++) { airdropList.delete( convertEcashtoEtokenAddr(addressStringArray[i]), ); } } // if the minimum etoken balance option is enabled if (ignoreMinEtokenBalance) { const minEligibleBalance = ignoreMinEtokenBalanceAmount; // initial filtering of recipients with less than minimum eToken balance for (let [key, value] of airdropList) { if (new BigNumber(value).isLessThan(minEligibleBalance)) { airdropList.delete(key); } } } if (!airdropList) { errorNotification( null, 'No recipients found for tokenId ' + formData.tokenId, 'Airdrop Calculation Error', ); setIsAirdropCalcModalVisible(false); passLoadingStatus(false); return; } // if the ignore minimum payment threshold option is enabled if (ignoreRecipientsBelowDust) { // minimum airdrop threshold const minEligibleAirdrop = new BigNumber( fromSmallestDenomination(currency.dustSats), ); // first calculation on expected pro rata airdrops let initialTotalTokenAmongstRecipients = new BigNumber(0); let initialTotalHolders = new BigNumber(airdropList.size); // amount of addresses that hold this eToken setEtokenHolders(initialTotalHolders); // keep a cumulative total of each eToken holding in each address in airdropList airdropList.forEach( index => (initialTotalTokenAmongstRecipients = initialTotalTokenAmongstRecipients.plus( new BigNumber(index), )), ); let initialCircToAirdropRatio = new BigNumber( formData.totalAirdrop, ).div(initialTotalTokenAmongstRecipients); // initial filtering of recipients with less than minimum payout amount for (let [key, value] of airdropList) { const proRataAirdrop = new BigNumber(value).multipliedBy( initialCircToAirdropRatio, ); if (proRataAirdrop.isLessThan(minEligibleAirdrop)) { airdropList.delete(key); } } // if the list becomes empty after initial filtering if (!airdropList) { errorNotification( null, 'No recipients after filtering minimum payouts', 'Airdrop Calculation Error', ); setIsAirdropCalcModalVisible(false); passLoadingStatus(false); return; } } setAirdropCalcModalProgress(75); let totalTokenAmongstRecipients = new BigNumber(0); let totalHolders = parseInt(airdropList.size); // amount of addresses that hold this eToken setEtokenHolders(totalHolders); // keep a cumulative total of each eToken holding in each address in airdropList airdropList.forEach( index => (totalTokenAmongstRecipients = totalTokenAmongstRecipients.plus( new BigNumber(index), )), ); let circToAirdropRatio = new BigNumber(formData.totalAirdrop).div( totalTokenAmongstRecipients, ); let resultString = ''; airdropList.forEach( (element, index) => (resultString += convertEtokenToEcashAddr(index) + ',' + new BigNumber(element) .multipliedBy(circToAirdropRatio) .decimalPlaces(currency.cashDecimals) + '\n'), ); resultString = resultString.substring(0, resultString.length - 1); // remove the final newline setAirdropRecipients(resultString); setAirdropCalcModalProgress(100); if (!resultString) { errorNotification( null, 'No holders found for eToken ID: ' + formData.tokenId, 'Airdrop Calculation Error', ); setIsAirdropCalcModalVisible(false); passLoadingStatus(false); return; } // validate the airdrop values for each recipient // Note: addresses are not validated as they are retrieved directly from onchain setAirdropOutputIsValid(isValidAirdropOutputsArray(resultString)); setShowAirdropOutputs(true); // display the airdrop outputs TextArea setIsAirdropCalcModalVisible(false); passLoadingStatus(false); }; const handleIgnoreMinEtokenBalanceAmt = e => { setIgnoreMinEtokenBalance(e); }; const handleAirdropCalcModalCancel = () => { setIsAirdropCalcModalVisible(false); passLoadingStatus(false); }; const handleIgnoreOwnAddress = e => { setIgnoreOwnAddress(e); }; const handleIgnoreRecipientBelowDust = e => { setIgnoreRecipientsBelowDust(e); }; const handleIgnoreMintAddress = e => { setIgnoreMintAddress(e); }; const handleIgnoreCustomAddresses = e => { setIgnoreCustomAddresses(e); }; const handleIgnoreCustomAddressesList = e => { // if the checkbox is not checked then skip the input validation if (!ignoreCustomAddresses) { return; } let customAddressList = e.target.value; // remove all whitespaces via regex customAddressList = customAddressList.replace(/ /g, ''); // validate the exclusion list input const addressListIsValid = isValidAirdropExclusionArray(customAddressList); setIgnoreCustomAddressesListIsValid(addressListIsValid); if (!addressListIsValid) { setIgnoreCustomAddressListError( 'Invalid address detected in ignore list', ); } else { setIgnoreCustomAddressListError(false); // needs to be explicitly set in order to refresh the error state from prior invalidation } // commit the ignore list to state setIgnoreCustomAddressesList(customAddressList); }; let airdropCalcInputIsValid = tokenIdIsValid && totalAirdropIsValid; // if the ignore min etoken balance and exclusion list options are in use, add the relevant validation to the total pre-calculation validation if (ignoreMinEtokenBalance && ignoreCustomAddresses) { // both enabled airdropCalcInputIsValid = ignoreMinEtokenBalanceAmountIsValid && tokenIdIsValid && totalAirdropIsValid && ignoreCustomAddressesListIsValid; } else if (ignoreMinEtokenBalance && !ignoreCustomAddresses) { // ignore minimum etoken balance option only airdropCalcInputIsValid = ignoreMinEtokenBalanceAmountIsValid && tokenIdIsValid && totalAirdropIsValid; } else if (!ignoreMinEtokenBalance && ignoreCustomAddresses) { // ignore custom addresses only airdropCalcInputIsValid = tokenIdIsValid && totalAirdropIsValid && ignoreCustomAddressesListIsValid; } return ( <> {!balances.totalBalance ? ( You currently have 0 {currency.ticker} Deposit some funds to use this feature ) : ( <> {fiatPrice !== null && ( )} > )} handleTokenIdInput(e) } /> handleTotalAirdropInput(e) } /> handleIgnoreOwnAddress( prev => !prev, ) } defaultunchecked="true" checked={ignoreOwnAddress} /> Ignore my own address handleIgnoreRecipientBelowDust( prev => !prev, ) } defaultunchecked="true" checked={ ignoreRecipientsBelowDust } /> Ignore airdrops below min. payment ( {fromSmallestDenomination( currency.dustSats, )}{' '} XEC) handleIgnoreMintAddress( prev => !prev, ) } defaultunchecked="true" checked={ignoreMintAddress} /> Ignore eToken minter address handleIgnoreMinEtokenBalanceAmt( prev => !prev, ) } defaultunchecked="true" checked={ignoreMinEtokenBalance} style={{ marginBottom: '5px', }} /> Minimum eToken holder balance {ignoreMinEtokenBalance && ( handleMinEtokenBalanceChange( e, ), value: ignoreMinEtokenBalanceAmount, }} /> )} handleIgnoreCustomAddresses( prev => !prev, ) } defaultunchecked="true" checked={ignoreCustomAddresses} style={{ marginBottom: '5px', }} /> Ignore custom addresses {ignoreCustomAddresses && ( handleIgnoreCustomAddressesList( e, ), required: ignoreCustomAddresses, disabled: !ignoreCustomAddresses, }} /> )} calculateXecAirdrop() } disabled={ !airdropCalcInputIsValid || !tokenIdIsValid } > Calculate Airdrop {showAirdropOutputs && ( <> {!ignoreRecipientsBelowDust && !airdropOutputIsValid && etokenHolders > 0 && ( <> > )} One to Many Airdrop Payment Outputs Copy to Send screen { navigator.clipboard.writeText( airdropRecipients, ); generalNotification( 'Airdrop recipients copied to clipboard', 'Copied', ); }} > Copy to Clipboard > )} > ); }; /* passLoadingStatus must receive a default prop that is a function in order to pass the rendering unit test in Airdrop.test.js status => {console.log(status)} is an arbitrary stub function */ Airdrop.defaultProps = { passLoadingStatus: status => { console.log(status); }, }; Airdrop.propTypes = { jestBCH: PropTypes.object, passLoadingStatus: PropTypes.func, }; export default Airdrop; diff --git a/web/cashtab/src/components/Common/Ticker.js b/web/cashtab/src/components/Common/Ticker.js index eb5e5b28c..594373b90 100644 --- a/web/cashtab/src/components/Common/Ticker.js +++ b/web/cashtab/src/components/Common/Ticker.js @@ -1,142 +1,142 @@ import mainLogo from 'assets/logo_primary.png'; import tokenLogo from 'assets/logo_secondary.png'; import BigNumber from 'bignumber.js'; export const currency = { name: 'eCash', ticker: 'XEC', 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', tokenDbUrl: 'https://tokendb.kingbch.com', txHistoryCount: 10, xecApiBatchSize: 20, defaultSettings: { fiatCurrency: 'usd', sendModal: false }, notificationDurationShort: 3, notificationDurationLong: 5, localStorageMaxCharacters: 24, newTokenDefaultUrl: 'https://cashtab.com/', opReturn: { opReturnPrefixHex: '6a', opReturnAppPrefixLengthHex: '04', opPushDataOne: '4c', appPrefixesHex: { eToken: '534c5000', cashtab: '00746162', cashtabEncrypted: '65746162', airdrop: '64726f70', }, encryptedMsgCharLimit: 94, - unencryptedMsgCharLimit: 160, + unencryptedMsgCharLimit: 145, }, settingsValidation: { fiatCurrency: [ 'usd', 'idr', 'krw', 'cny', 'zar', 'vnd', 'cad', 'nok', 'eur', 'gbp', 'jpy', 'try', 'rub', 'inr', 'brl', 'php', 'ils', 'clp', 'twd', 'hkd', 'bhd', 'sar', 'aud', 'nzd', 'chf', ], sendModal: [true, false], }, fiatCurrencies: { usd: { name: 'US Dollar', symbol: '$', slug: 'usd' }, aud: { name: 'Australian Dollar', symbol: '$', slug: 'aud' }, bhd: { name: 'Bahraini Dinar', symbol: 'BD', slug: 'bhd' }, 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' }, hkd: { name: 'Hong Kong Dollar', symbol: 'HK$', slug: 'hkd' }, 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' }, nzd: { name: 'New Zealand Dollar', symbol: '$', slug: 'nzd' }, nok: { name: 'Norwegian Krone', symbol: 'kr', slug: 'nok' }, php: { name: 'Philippine Peso', symbol: '₱', slug: 'php' }, rub: { name: 'Russian Ruble', symbol: 'р.', slug: 'rub' }, twd: { name: 'New Taiwan Dollar', symbol: 'NT$', slug: 'twd' }, sar: { name: 'Saudi Riyal', symbol: 'SAR', slug: 'sar' }, zar: { name: 'South African Rand', symbol: 'R', slug: 'zar' }, chf: { name: 'Swiss Franc', symbol: 'Fr.', slug: 'chf' }, try: { name: 'Turkish Lira', symbol: '₺', slug: 'try' }, vnd: { name: 'Vietnamese đồng', symbol: 'đ', slug: 'vnd' }, }, }; export function parseAddressForParams(addressString) { // Build return obj const addressInfo = { address: '', queryString: null, amount: null, }; // Parse address string for parameters const paramCheck = addressString.split('?'); let cleanAddress = paramCheck[0]; addressInfo.address = cleanAddress; // 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 XEC try { amount = new BigNumber( parseFloat(addrParams.get('amount')), ).toString(); } catch (err) { amount = null; } } } addressInfo.amount = amount; return addressInfo; } diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js index b48495f96..5c870c04b 100644 --- a/web/cashtab/src/components/Send/Send.js +++ b/web/cashtab/src/components/Send/Send.js @@ -1,1284 +1,1292 @@ import React, { useState, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import PropTypes from 'prop-types'; import { WalletContext } from 'utils/context'; import { AntdFormWrapper, SendBchInput, DestinationAddressSingle, DestinationAddressMulti, DestinationAddressSingleWithoutQRScan, } from 'components/Common/EnhancedInputs'; import { CustomCollapseCtn } from 'components/Common/StyledCollapse'; import { Form, message, Modal, Alert, Input } from 'antd'; import { Row, Col, Switch } from 'antd'; import PrimaryButton, { DisabledButton, SmartButton, } from 'components/Common/PrimaryButton'; import useBCH from 'hooks/useBCH'; import useWindowDimensions from 'hooks/useWindowDimensions'; import { sendXecNotification, errorNotification, messageSignedNotification, generalNotification, } from 'components/Common/Notifications'; import { isMobile, isIOS, isSafari } from 'react-device-detect'; import { currency, parseAddressForParams } from 'components/Common/Ticker.js'; import { Event } from 'utils/GoogleAnalytics'; import { fiatToCrypto, shouldRejectAmountInput, isValidXecAddress, isValidEtokenAddress, isValidXecSendAmount, } from 'utils/validation'; import BalanceHeader from 'components/Common/BalanceHeader'; import BalanceHeaderFiat from 'components/Common/BalanceHeaderFiat'; import { ZeroBalanceHeader, ConvertAmount, AlertMsg, WalletInfoCtn, SidePaddingCtn, FormLabel, } from 'components/Common/Atoms'; import { getWalletState, convertToEcashPrefix, toLegacyCash, toLegacyCashArray, fromSmallestDenomination, } from 'utils/cashMethods'; import ApiError from 'components/Common/ApiError'; import { formatFiatBalance, formatBalance } from 'utils/formatting'; import { TokenParamLabel, MessageVerificationParamLabel, } from 'components/Common/Atoms'; import { PlusSquareOutlined } from '@ant-design/icons'; import styled from 'styled-components'; import { CopyToClipboard } from 'react-copy-to-clipboard'; import WalletLabel from 'components/Common/WalletLabel.js'; const { TextArea } = Input; const SignMessageLabel = styled.div` text-align: left; color: ${props => props.theme.forms.text}; `; const SignatureValidation = styled.div` color: ${props => props.theme.encryptionRed}; `; const VerifyMessageLabel = styled.div` text-align: left; color: ${props => props.theme.forms.text}; `; const TextAreaLabel = styled.div` text-align: left; color: ${props => props.theme.forms.text}; padding-left: 1px; `; const AmountPreviewCtn = styled.div` margin-top: -30px; `; const SendInputCtn = styled.div` .ant-form-item-with-help { margin-bottom: 32px; } `; const LocaleFormattedValue = styled.h3` color: ${props => props.theme.contrast}; font-weight: bold; margin-bottom: 0; `; // Note jestBCH is only used for unit tests; BCHJS must be mocked for jest const SendBCH = ({ jestBCH, passLoadingStatus }) => { // use balance parameters from wallet.state object and not legacy balances parameter from walletState, if user has migrated wallet // this handles edge case of user with old wallet who has not opened latest Cashtab version yet // If the wallet object from ContextValue has a `state key`, then check which keys are in the wallet object // Else set it as blank const ContextValue = React.useContext(WalletContext); const location = useLocation(); const { wallet, fiatPrice, apiError, cashtabSettings } = ContextValue; const walletState = getWalletState(wallet); const { balances, slpBalancesAndUtxos } = walletState; // Modal settings const [showConfirmMsgToSign, setShowConfirmMsgToSign] = useState(false); const [msgToSign, setMsgToSign] = useState(''); const [signMessageIsValid, setSignMessageIsValid] = useState(null); const [isOneToManyXECSend, setIsOneToManyXECSend] = useState(false); const [opReturnMsg, setOpReturnMsg] = useState(false); const [isEncryptedOptionalOpReturnMsg, setIsEncryptedOptionalOpReturnMsg] = useState(false); const [bchObj, setBchObj] = useState(false); // Get device window width // If this is less than 769, the page will open with QR scanner open const { width } = useWindowDimensions(); // Load with QR code open if device is mobile and NOT iOS + anything but safari const scannerSupported = width < 769 && isMobile && !(isIOS && !isSafari); const [formData, setFormData] = useState({ value: '', address: '', + airdropTokenId: '', }); const [queryStringText, setQueryStringText] = useState(null); const [sendBchAddressError, setSendBchAddressError] = useState(false); const [sendBchAmountError, setSendBchAmountError] = useState(false); const [selectedCurrency, setSelectedCurrency] = useState(currency.ticker); // Support cashtab button from web pages const [txInfoFromUrl, setTxInfoFromUrl] = useState(false); // Show a confirmation modal on transactions created by populating form from web page button const [isModalVisible, setIsModalVisible] = useState(false); const [messageSignature, setMessageSignature] = useState(''); const [sigCopySuccess, setSigCopySuccess] = useState(''); const [showConfirmMsgToVerify, setShowConfirmMsgToVerify] = useState(false); const [messageVerificationAddr, setMessageVerificationAddr] = useState(''); const [messageVerificationSig, setMessageVerificationSig] = useState(''); const [messageVerificationMsg, setMessageVerificationMsg] = useState(''); const [messageVerificationAddrIsValid, setMessageVerificationAddrIsValid] = useState(false); const [messageVerificationSigIsValid, setMessageVerificationSigIsValid] = useState(false); const [messageVerificationMsgIsValid, setMessageVerificationMsgIsValid] = useState(false); const [messageVerificationAddrError, setMessageVerificationAddrError] = useState(false); const [messageVerificationSigError, setMessageVerificationSigError] = useState(false); const [airdropFlag, setAirdropFlag] = useState(false); const userLocale = navigator.language; const clearInputForms = () => { setFormData({ value: '', address: '', }); setOpReturnMsg(''); // OP_RETURN message has its own state field }; const checkForConfirmationBeforeSendXec = () => { if (txInfoFromUrl) { setIsModalVisible(true); } else if (cashtabSettings.sendModal) { setIsModalVisible(cashtabSettings.sendModal); } else { // if the user does not have the send confirmation enabled in settings then send directly send(); } }; const handleOk = () => { setIsModalVisible(false); send(); }; const handleCancel = () => { setIsModalVisible(false); }; const { getBCH, getRestUrl, sendXec, calcFee, signPkMessage } = useBCH(); // If the balance has changed, unlock the UI // This is redundant, if backend has refreshed in 1.75s timeout below, UI will already be unlocked useEffect(() => { passLoadingStatus(false); }, [balances.totalBalance]); useEffect(() => { // jestBCH is only ever specified for unit tests, otherwise app will use getBCH(); const BCH = jestBCH ? jestBCH : getBCH(); // set the BCH instance to state, for other functions to reference setBchObj(BCH); // Manually parse for txInfo object on page load when Send.js is loaded with a query string // if this was routed from Wallet screen's Reply to message link then prepopulate the address and value field if (location && location.state && location.state.replyAddress) { setFormData({ address: location.state.replyAddress, value: `${fromSmallestDenomination(currency.dustSats)}`, }); } // if this was routed from the Contact List if (location && location.state && location.state.contactSend) { setFormData({ address: location.state.contactSend, }); } // if this was routed from the Airdrop screen's Airdrop Calculator then // switch to multiple recipient mode and prepopulate the recipients field - if (location && location.state && location.state.airdropRecipients) { + if ( + location && + location.state && + location.state.airdropRecipients && + location.state.airdropTokenId + ) { setIsOneToManyXECSend(true); setFormData({ address: location.state.airdropRecipients, + airdropTokenId: location.state.airdropTokenId, }); // validate the airdrop outputs from the calculator handleMultiAddressChange({ target: { value: location.state.airdropRecipients, }, }); setAirdropFlag(true); } // Do not set txInfo in state if query strings are not present if ( !window.location || !window.location.hash || window.location.hash === '#/send' ) { return; } const txInfoArr = window.location.hash.split('?')[1].split('&'); // Iterate over this to create object const txInfo = {}; for (let i = 0; i < txInfoArr.length; i += 1) { let txInfoKeyValue = txInfoArr[i].split('='); let key = txInfoKeyValue[0]; let value = txInfoKeyValue[1]; txInfo[key] = value; } console.log(`txInfo from page params`, txInfo); setTxInfoFromUrl(txInfo); populateFormsFromUrl(txInfo); }, []); function populateFormsFromUrl(txInfo) { if (txInfo && txInfo.address && txInfo.value) { setFormData({ address: txInfo.address, value: txInfo.value, }); } } function handleSendXecError(errorObj, oneToManyFlag) { // Set loading to false here as well, as balance may not change depending on where error occured in try loop passLoadingStatus(false); let message; if (!errorObj.error && !errorObj.message) { message = `Transaction failed: no response from ${getRestUrl()}.`; } else if ( /Could not communicate with full node or other external service/.test( errorObj.error, ) ) { message = 'Could not communicate with API. Please try again.'; } else if ( errorObj.error && errorObj.error.includes( 'too-long-mempool-chain, too many unconfirmed ancestors [limit: 50] (code 64)', ) ) { message = `The ${currency.ticker} you are trying to send has too many unconfirmed ancestors to send (limit 50). Sending will be possible after a block confirmation. Try again in about 10 minutes.`; } else { message = errorObj.message || errorObj.error || JSON.stringify(errorObj); } if (oneToManyFlag) { errorNotification(errorObj, message, 'Sending XEC one to many'); } else { errorNotification(errorObj, message, 'Sending XEC'); } } async function send() { setFormData({ ...formData, }); if (isOneToManyXECSend) { // this is a one to many XEC send transactions // ensure multi-recipient input is not blank if (!formData.address) { return; } // Event("Category", "Action", "Label") // Track number of XEC send-to-many transactions Event('Send.js', 'SendToMany', selectedCurrency); passLoadingStatus(true); const { address } = formData; //convert each line from TextArea input let addressAndValueArray = address.split('\n'); try { // construct array of XEC->BCH addresses due to bch-api constraint let cleanAddressAndValueArray = toLegacyCashArray(addressAndValueArray); const link = await sendXec( bchObj, wallet, slpBalancesAndUtxos.nonSlpUtxos, currency.defaultFee, opReturnMsg, true, // indicate send mode is one to many cleanAddressAndValueArray, null, null, false, // one to many tx msg can't be encrypted airdropFlag, + formData.airdropTokenId, ); sendXecNotification(link); clearInputForms(); setAirdropFlag(false); } catch (e) { handleSendXecError(e, isOneToManyXECSend); } } else { // standard one to one XEC send transaction if ( !formData.address || !formData.value || Number(formData.value) <= 0 ) { return; } // Event("Category", "Action", "Label") // Track number of BCHA send transactions and whether users // are sending BCHA or USD Event('Send.js', 'Send', selectedCurrency); passLoadingStatus(true); const { address, value } = formData; // Get the param-free address let cleanAddress = address.split('?')[0]; // Ensure address has bitcoincash: prefix and checksum cleanAddress = toLegacyCash(cleanAddress); // Calculate the amount in BCH let bchValue = value; if (selectedCurrency !== 'XEC') { bchValue = fiatToCrypto(value, fiatPrice); } // encrypted message limit truncation let optionalOpReturnMsg; if (isEncryptedOptionalOpReturnMsg) { optionalOpReturnMsg = opReturnMsg.substring( 0, currency.opReturn.encryptedMsgCharLimit, ); } else { optionalOpReturnMsg = opReturnMsg; } try { const link = await sendXec( bchObj, wallet, slpBalancesAndUtxos.nonSlpUtxos, currency.defaultFee, optionalOpReturnMsg, false, // sendToMany boolean flag null, // address array not applicable for one to many tx cleanAddress, bchValue, isEncryptedOptionalOpReturnMsg, ); sendXecNotification(link); clearInputForms(); } catch (e) { handleSendXecError(e, isOneToManyXECSend); } } } const handleAddressChange = e => { const { value, name } = e.target; let error = false; let addressString = value; // parse address for parameters const addressInfo = parseAddressForParams(addressString); // validate address const isValid = isValidXecAddress(addressInfo.address); /* Model addressInfo = { address: '', queryString: '', amount: null, }; */ const { address, queryString, amount } = addressInfo; // If query string, // Show an alert that only amount and currency.ticker are supported setQueryStringText(queryString); // Is this valid address? if (!isValid) { error = `Invalid ${currency.ticker} address`; // If valid address but token format if (isValidEtokenAddress(address)) { error = `eToken addresses are not supported for ${currency.ticker} sends`; } } setSendBchAddressError(error); // Set amount if it's in the query string if (amount !== null) { // Set currency to BCHA setSelectedCurrency(currency.ticker); // Use this object to mimic user input and get validation for the value let amountObj = { target: { name: 'value', value: amount, }, }; handleBchAmountChange(amountObj); setFormData({ ...formData, value: amount, }); } // Set address field to user input setFormData(p => ({ ...p, [name]: value, })); }; const handleMessageVerificationAddrChange = e => { const { value } = e.target; let error = false; let addressString = value; // parse address for parameters const addressInfo = parseAddressForParams(addressString); // validate address const isValid = isValidXecAddress(addressInfo.address); const { address } = addressInfo; // Is this valid address? if (!isValid) { error = `Invalid ${currency.ticker} address`; // If valid address but token format if (isValidEtokenAddress(address)) { error = `eToken addresses are not supported for signature verifications`; } setMessageVerificationAddrIsValid(false); } else { setMessageVerificationAddrIsValid(true); } setMessageVerificationAddrError(error); setMessageVerificationAddr(address); }; const handleMultiAddressChange = e => { const { value, name } = e.target; let error; if (!value) { error = 'Input must not be blank'; setSendBchAddressError(error); return setFormData(p => ({ ...p, [name]: value, })); } //convert each line from the input into array let addressStringArray = value.split('\n'); const arrayLength = addressStringArray.length; // loop through each row in the input for (let i = 0; i < arrayLength; i++) { if (addressStringArray[i].trim() === '') { // if this line is a line break or bunch of spaces error = 'Empty spaces and rows must be removed'; setSendBchAddressError(error); return setFormData(p => ({ ...p, [name]: value, })); } let addressString = addressStringArray[i].split(',')[0]; let valueString = addressStringArray[i].split(',')[1]; const validAddress = isValidXecAddress(addressString); const validValueString = isValidXecSendAmount(valueString); if (!validAddress) { error = `Invalid XEC address: ${addressString}, ${valueString}`; setSendBchAddressError(error); return setFormData(p => ({ ...p, [name]: value, })); } if (!validValueString) { error = `Amount must be at least ${fromSmallestDenomination( currency.dustSats, )} XEC: ${addressString}, ${valueString}`; setSendBchAddressError(error); return setFormData(p => ({ ...p, [name]: value, })); } } // If iterate to end of array with no errors, then there is no error msg setSendBchAddressError(false); // Set address field to user input setFormData(p => ({ ...p, [name]: value, })); }; const handleSelectedCurrencyChange = e => { setSelectedCurrency(e); // Clear input field to prevent accidentally sending 1 BCH instead of 1 USD setFormData(p => ({ ...p, value: '', })); }; const handleBchAmountChange = e => { const { value, name } = e.target; let bchValue = value; const error = shouldRejectAmountInput( bchValue, selectedCurrency, fiatPrice, balances.totalBalance, ); setSendBchAmountError(error); setFormData(p => ({ ...p, [name]: value, })); }; const handleSignMsgChange = e => { const { value } = e.target; // validation if (value && value.length && value.length < 150) { setMsgToSign(value); setSignMessageIsValid(true); } else { setSignMessageIsValid(false); } }; const handleVerifyMsgChange = e => { const { value } = e.target; // validation if (value && value.length && value.length < 150) { setMessageVerificationMsgIsValid(true); } else { setMessageVerificationMsgIsValid(false); } setMessageVerificationMsg(value); }; const handleVerifySigChange = e => { const { value } = e.target; // validation if (value && value.length && value.length === 88) { setMessageVerificationSigIsValid(true); setMessageVerificationSigError(false); } else { setMessageVerificationSigIsValid(false); setMessageVerificationSigError('Invalid signature'); } setMessageVerificationSig(value); }; const verifyMessageBySig = async () => { let verification; try { verification = await bchObj.BitcoinCash.verifyMessage( toLegacyCash(messageVerificationAddr), messageVerificationSig, messageVerificationMsg, ); } catch (err) { errorNotification( 'Error', 'Unable to execute signature verification', ); } if (verification) { generalNotification('Signature successfully verified', 'Verified'); } else { errorNotification( 'Error', 'Signature does not match address and message', ); } setShowConfirmMsgToVerify(false); }; const signMessageByPk = async () => { try { const messageSignature = await signPkMessage( bchObj, wallet.Path1899.fundingWif, msgToSign, ); setMessageSignature(messageSignature); messageSignedNotification(messageSignature); } catch (err) { let message; if (!err.error && !err.message) { message = err.message || err.error || JSON.stringify(err); } errorNotification(err, message, 'Message Signing Error'); throw err; } // Hide the modal setShowConfirmMsgToSign(false); setSigCopySuccess(''); }; const handleOnSigCopy = () => { if (messageSignature != '') { setSigCopySuccess('Signature copied to clipboard'); } }; const onMax = async () => { // Clear amt error setSendBchAmountError(false); // Set currency to BCH setSelectedCurrency(currency.ticker); try { const txFeeSats = calcFee(bchObj, slpBalancesAndUtxos.nonSlpUtxos); const txFeeBch = txFeeSats / 10 ** currency.cashDecimals; let value = balances.totalBalance - txFeeBch >= 0 ? (balances.totalBalance - txFeeBch).toFixed( currency.cashDecimals, ) : 0; setFormData({ ...formData, value, }); } catch (err) { console.log(`Error in onMax:`); console.log(err); message.error( 'Unable to calculate the max value due to network errors', ); } }; // Display price in USD below input field for send amount, if it can be calculated let fiatPriceString = ''; if (fiatPrice !== null && !isNaN(formData.value)) { if (selectedCurrency === currency.ticker) { // calculate conversion to fiatPrice fiatPriceString = `${(fiatPrice * Number(formData.value)).toFixed( 2, )}`; // formats to fiat locale style fiatPriceString = formatFiatBalance( Number(fiatPriceString), userLocale, ); // insert symbol and currency before/after the locale formatted fiat balance fiatPriceString = `${ cashtabSettings ? `${ currency.fiatCurrencies[cashtabSettings.fiatCurrency] .symbol } ` : '$ ' } ${fiatPriceString} ${ cashtabSettings && cashtabSettings.fiatCurrency ? cashtabSettings.fiatCurrency.toUpperCase() : 'USD' }`; } else { fiatPriceString = `${ formData.value ? formatFiatBalance( Number(fiatToCrypto(formData.value, fiatPrice)), userLocale, ) : formatFiatBalance(0, userLocale) } ${currency.ticker}`; } } const priceApiError = fiatPrice === null && selectedCurrency !== 'XEC'; return ( <> {isOneToManyXECSend ? `are you sure you want to send the following One to Many transaction? ${formData.address}` : `Are you sure you want to send ${formData.value}${' '} ${currency.ticker} to ${formData.address}?`} {!balances.totalBalance ? ( You currently have 0 {currency.ticker} Deposit some funds to use this feature ) : ( <> > )} {!isOneToManyXECSend ? ( Send to handleAddressChange({ target: { name: 'address', value: result, }, }) } inputProps={{ placeholder: `${currency.ticker} Address`, name: 'address', onChange: e => handleAddressChange(e), required: true, value: formData.address, }} > Amount handleBchAmountChange(e), required: true, value: formData.value, disabled: priceApiError, }} selectProps={{ value: selectedCurrency, disabled: queryStringText !== null, onChange: e => handleSelectedCurrencyChange(e), }} > {priceApiError && ( Error fetching fiat price. Setting send by{' '} {currency.fiatCurrencies[ cashtabSettings.fiatCurrency ].slug.toUpperCase()}{' '} disabled )} ) : ( <> Send to handleMultiAddressChange(e), required: true, value: formData.address, }} > > )} {!priceApiError && !isOneToManyXECSend && ( {formatBalance( formData.value, userLocale, )}{' '} {selectedCurrency} {fiatPriceString !== '' && '='}{' '} {fiatPriceString} )} {queryStringText && ( )} {!balances.totalBalance || apiError || sendBchAmountError || sendBchAddressError || priceApiError ? ( Send ) : ( <> {txInfoFromUrl ? ( checkForConfirmationBeforeSendXec() } > Send ) : ( { checkForConfirmationBeforeSendXec(); }} > Send )} > )} Multiple Recipients: { setIsOneToManyXECSend( !isOneToManyXECSend, ); setIsEncryptedOptionalOpReturnMsg( false, ); }} style={{ marginBottom: '7px', }} /> Message: { setIsEncryptedOptionalOpReturnMsg( prev => !prev, ); setIsOneToManyXECSend(false); }} /> {isEncryptedOptionalOpReturnMsg ? ( ) : ( )} setOpReturnMsg(e.target.value) } showCount maxLength={ isEncryptedOptionalOpReturnMsg ? currency.opReturn .encryptedMsgCharLimit : currency.opReturn .unencryptedMsgCharLimit } onKeyDown={e => e.keyCode == 13 ? e.preventDefault() : '' } /> {apiError && } setShowConfirmMsgToSign(false)} > Message: {msgToSign} Message: handleSignMsgChange(e)} showCount maxLength={150} /> Address: setShowConfirmMsgToSign(true)} disabled={!signMessageIsValid} > Sign Message Signature: handleOnSigCopy()} /> {sigCopySuccess} setShowConfirmMsgToVerify(false)} > Message: {' '} {messageVerificationMsg} Address: {' '} {messageVerificationAddr} Signature: {' '} {messageVerificationSig} Message: handleVerifyMsgChange(e)} showCount maxLength={150} /> Address: handleMessageVerificationAddrChange( e, ), required: true, }} > Signature: handleVerifySigChange(e)} showCount /> {messageVerificationSigError} setShowConfirmMsgToVerify(true)} disabled={ !messageVerificationAddrIsValid || !messageVerificationSigIsValid || !messageVerificationMsgIsValid } > Verify Message > ); }; /* passLoadingStatus must receive a default prop that is a function in order to pass the rendering unit test in Send.test.js status => {console.log(status)} is an arbitrary stub function */ SendBCH.defaultProps = { passLoadingStatus: status => { console.log(status); }, }; SendBCH.propTypes = { jestBCH: PropTypes.object, passLoadingStatus: PropTypes.func, }; export default SendBCH; diff --git a/web/cashtab/src/hooks/__mocks__/mockParsedTxs.js b/web/cashtab/src/hooks/__mocks__/mockParsedTxs.js index 6b71a314f..a8de2579f 100644 --- a/web/cashtab/src/hooks/__mocks__/mockParsedTxs.js +++ b/web/cashtab/src/hooks/__mocks__/mockParsedTxs.js @@ -1,140 +1,168 @@ // 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, isEncryptedMessage: false, opReturnMessage: '', replyAddress: null, tokenTx: false, decryptionSuccess: false, airdropFlag: false, + airdropTokenId: '', txid: '089f2188d5771a7de0589def2b8d6c1a1f33f45b6de26d9a0ef32782f019ecf1', }, ]; export const mockReceivedCashTx = [ { amountReceived: 3, amountSent: 0, blocktime: 1612567121, confirmations: 5637, destinationAddress: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', height: 672077, outgoingTx: false, isCashtabMessage: false, isEncryptedMessage: false, opReturnMessage: '', replyAddress: null, tokenTx: false, decryptionSuccess: false, airdropFlag: false, + airdropTokenId: '', txid: '42d39fbe068a40fe691f987b22fdf04b80f94d71d2fec20a58125e7b1a06d2a9', }, ]; export const mockSentTokenTx = [ { amountReceived: 0, amountSent: 0.00000546, blocktime: 1614027278, confirmations: 3270, destinationAddress: 'bitcoincash:qzj5zu6fgg8v2we82gh76xnrk9njcregluzgaztm45', height: 674444, outgoingTx: true, isCashtabMessage: false, isEncryptedMessage: false, opReturnMessage: '', replyAddress: null, tokenTx: true, decryptionSuccess: false, airdropFlag: false, + airdropTokenId: '', txid: 'ffe3a7500dbcc98021ad581c98d9947054d1950a7f3416664715066d3d20ad72', }, ]; export const mockReceivedTokenTx = [ { amountReceived: 0.00000546, amountSent: 0, blocktime: 1613859311, confirmations: 3571, destinationAddress: 'bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9', height: 674143, outgoingTx: false, isCashtabMessage: false, isEncryptedMessage: false, opReturnMessage: '', replyAddress: null, tokenTx: true, decryptionSuccess: false, airdropFlag: false, + airdropTokenId: '', txid: '618d0dd8c0c5fa5a34c6515c865dd72bb76f8311cd6ee9aef153bab20dabc0e6', }, ]; export const mockSentOpReturnMessageTx = [ { amountReceived: 0, amountSent: 0, blocktime: 1635507345, confirmations: 59, destinationAddress: undefined, height: undefined, opReturnMessage: 'bingoelectrum', replyAddress: null, outgoingTx: false, tokenTx: false, isCashtabMessage: false, isEncryptedMessage: false, decryptionSuccess: false, airdropFlag: false, + airdropTokenId: '', txid: 'dd35690b0cefd24dcc08acba8694ecd49293f365a81372cb66c8f1c1291d97c5', }, ]; export const mockReceivedOpReturnMessageTx = [ { amountReceived: 0, amountSent: 0, blocktime: 1635511136, confirmations: 70, destinationAddress: undefined, height: undefined, opReturnMessage: 'cashtabular', replyAddress: 'ecash:qrxkkzsmrxcjmz8x90fx2uztt83cuu0u254w09pq5p', outgoingTx: false, tokenTx: false, isCashtabMessage: true, isEncryptedMessage: false, decryptionSuccess: false, airdropFlag: false, + airdropTokenId: '', txid: '5adc33b5c0509b31c6da359177b19467c443bdc4dd37c283c0f87244c0ad63af', }, ]; export const mockBurnEtokenTx = [ { amountReceived: 0, amountSent: 0, blocktime: 1643286535, confirmations: 3, destinationAddress: undefined, decryptionSuccess: false, height: undefined, isCashtabMessage: false, isEncryptedMessage: false, opReturnMessage: '', outgoingTx: false, replyAddress: null, tokenTx: true, airdropFlag: false, + airdropTokenId: '', txid: '8b569d64a7e51d1d3cf1cf2b99d8b34451bbebc7df6b67232e5b770418b0428c', }, ]; +export const mockSentAirdropOpReturnMessageTx = [ + { + amountReceived: 0, + amountSent: 0, + blocktime: 1651757835, + confirmations: 2, + destinationAddress: undefined, + height: undefined, + opReturnMessage: 'banana', + replyAddress: null, + outgoingTx: false, + tokenTx: false, + isCashtabMessage: true, + isEncryptedMessage: false, + decryptionSuccess: false, + airdropFlag: true, + airdropTokenId: + '31633663396336346437306232383562656665373333663137356430663338343533383537363837366264323830623130353837646638313237396433663565', + txid: 'ff253d06a1f3cce088d541dc28d06cf08cebc0288c2ae21e8985df08109b45d8', + }, +]; diff --git a/web/cashtab/src/hooks/__mocks__/mockTxDataWithPassthrough.js b/web/cashtab/src/hooks/__mocks__/mockTxDataWithPassthrough.js index d944856de..af7801fed 100644 --- a/web/cashtab/src/hooks/__mocks__/mockTxDataWithPassthrough.js +++ b/web/cashtab/src/hooks/__mocks__/mockTxDataWithPassthrough.js @@ -1,967 +1,1042 @@ 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, }, ], 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, }, { txid: 'b980b35b794ad73d8aae312385e82d9be8086e7b743e1c6a468db8db8ac74bd8', vout: 2, scriptSig: { asm: '3045022100ce03e19bd181b903adc6f192d4ad0900e6816f6e62282cefff05c22cf36a647602202b296a2ed1805f0b0a9aa5f99158685298e7a0aff406fedb8abb8e0afaf48ca4[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '483045022100ce03e19bd181b903adc6f192d4ad0900e6816f6e62282cefff05c22cf36a647602202b296a2ed1805f0b0a9aa5f99158685298e7a0aff406fedb8abb8e0afaf48ca4412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, }, ], 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, }, ], 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, }, { txid: '50d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e', vout: 1, scriptSig: { asm: '304402203ce88e0a95d5581ad567c0468c87a08027aa5ecdecd614a168d833d7ecc02c1c022013ddd81147b44ad5488107d5c4d535f7f59e9fa46840451d39422aace284b2b7[ALL|FORKID] 034509251caa5f01e2787c436949eb94d71dcc451bcde5791ae5b7109255f5f0a3', hex: '47304402203ce88e0a95d5581ad567c0468c87a08027aa5ecdecd614a168d833d7ecc02c1c022013ddd81147b44ad5488107d5c4d535f7f59e9fa46840451d39422aace284b2b74121034509251caa5f01e2787c436949eb94d71dcc451bcde5791ae5b7109255f5f0a3', }, sequence: 4294967295, }, ], 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, }, { txid: 'acbb66f826211f40b89e84d9bd2143dfb541d67e1e3c664b17ccd3ba66327a9e', vout: 1, scriptSig: { asm: '3045022100b475cf7d1eaf37641d2107f13be0ef9acbd17b252ed3f9ae349edfdcd6a97cf402202bf2852dfa905e6d50c96a622d2838408ceb979245a4342d5096acc938135804[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '483045022100b475cf7d1eaf37641d2107f13be0ef9acbd17b252ed3f9ae349edfdcd6a97cf402202bf2852dfa905e6d50c96a622d2838408ceb979245a4342d5096acc93813580441210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, }, ], 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, }, ], 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, }, ], 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, }, ], 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, }, { txid: '42d39fbe068a40fe691f987b22fdf04b80f94d71d2fec20a58125e7b1a06d2a9', vout: 0, scriptSig: { asm: '304402201bbfcd0c120ace9b8c7a6f5e77b61236bb1128e2a757f85ba80101885e9c1212022046fed4006dcd6a236034dede77c566acf74824d14b3ee3da884e9bd93884ff93[ALL|FORKID] 02c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', hex: '47304402201bbfcd0c120ace9b8c7a6f5e77b61236bb1128e2a757f85ba80101885e9c1212022046fed4006dcd6a236034dede77c566acf74824d14b3ee3da884e9bd93884ff93412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', }, sequence: 4294967295, }, ], 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, }, { txid: '8f73a718d907d94e60c5f73f299bd01dc5b1c163c4ebc26b5304e37a1a7f34af', vout: 0, scriptSig: { asm: '3045022100ac50553448f2a5fab1177ed0bc64541b2dba063d04f2d69a8a1d216fb1435e5802202c7f6abd1685a6d81f14ac3bdb0874d214a5f4260719f9c5dc519ac5d8dffd37[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '483045022100ac50553448f2a5fab1177ed0bc64541b2dba063d04f2d69a8a1d216fb1435e5802202c7f6abd1685a6d81f14ac3bdb0874d214a5f4260719f9c5dc519ac5d8dffd3741210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, }, { txid: 'd38b76bacd6aa75ad2d6fcfd994533e54d0541435970eace49486fde9d6ee2e3', vout: 0, scriptSig: { asm: '304502210091836c6cb4c786bd3b74b73e579ddf8b843ba51841e5675fa53608449b67371802203de75f32b684cfe2d2e9cd424ea6eb4f49248e6698365c9364ebf84cd6e50eab[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '48304502210091836c6cb4c786bd3b74b73e579ddf8b843ba51841e5675fa53608449b67371802203de75f32b684cfe2d2e9cd424ea6eb4f49248e6698365c9364ebf84cd6e50eab41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, }, { txid: 'd47607a72d6bc093556fa7f2cec9d67719bd627751d5d27bc53c4eb8eb6f54e5', vout: 0, scriptSig: { asm: '3045022100e7727d9d26c645282553aef27947ad6795bc89b505ad089d617b6f696399352802206c736524a1410ed3e30cf1127f7f02c9a249392f8f8e7c670250472909d1c0d6[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '483045022100e7727d9d26c645282553aef27947ad6795bc89b505ad089d617b6f696399352802206c736524a1410ed3e30cf1127f7f02c9a249392f8f8e7c670250472909d1c0d641210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, }, { txid: '33f246811f794c4b64098a64c698ae5811054b13e289256a18e2d142beef57e7', vout: 1, scriptSig: { asm: '304402203fe78ad5aaeefab7b3b2277eefc4a2ace9c2e92694b46bf4a76927bf2b82017102200ded59336aba269a54865d9fdd99e72081c0318ccbc37bc0fc0c72b60ae35382[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '47304402203fe78ad5aaeefab7b3b2277eefc4a2ace9c2e92694b46bf4a76927bf2b82017102200ded59336aba269a54865d9fdd99e72081c0318ccbc37bc0fc0c72b60ae3538241210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, }, { txid: '25f915d2912524ad602c882211ccaf479d6bf87ef7c24d1be0f325cec3727257', vout: 0, scriptSig: { asm: '30440220670af03605b9495c8ecee357889ceeb137dadaa1662136fdc55c28fe9434e3c60220285195a62811941745a9f93e136e59c96b81d5b0d9525f3d16d001bc0f6fa9bb[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '4730440220670af03605b9495c8ecee357889ceeb137dadaa1662136fdc55c28fe9434e3c60220285195a62811941745a9f93e136e59c96b81d5b0d9525f3d16d001bc0f6fa9bb41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, }, { txid: 'c9044b4d7438d006a722ef85474c8127265eced4f72c7d71c2f714444bc0e1f2', vout: 0, scriptSig: { asm: '304402203f822a0b207ed49e6918663133a18037c24498c2f770c2649333a32f523e259d02203afc42a79d0da123b67f814effeee7c05c7996ea829b3cfa46c5c2e74209c096[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '47304402203f822a0b207ed49e6918663133a18037c24498c2f770c2649333a32f523e259d02203afc42a79d0da123b67f814effeee7c05c7996ea829b3cfa46c5c2e74209c09641210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, }, { txid: '045306f0019ae0d977de7ff17dd55e861b3fe94458693ee2b94ce5dd7003aab9', vout: 0, scriptSig: { asm: '3045022100a7a2cf838a13a19f0e443ca35ac5ee3d55f70edca992f98402a84d4ab5ae1ad90220644a02c746eae7b44a4600199ecbf69f3b0f0bdf8479f461c482d67ef4a84e76[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '483045022100a7a2cf838a13a19f0e443ca35ac5ee3d55f70edca992f98402a84d4ab5ae1ad90220644a02c746eae7b44a4600199ecbf69f3b0f0bdf8479f461c482d67ef4a84e7641210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, }, { txid: '1452267e57429edcfdcb1184b24becea6ddf8f8a4f8e130dad6248545d9f8e75', vout: 0, scriptSig: { asm: '30440220290701c797eb52ad6721db615c7d6f623c0200be0e6d6802df68c527655475450220446c4a4da9a0df5efcb57711ad61cf6167dfdda937bd0477189be8afedaedd05[ALL|FORKID] 0352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', hex: '4730440220290701c797eb52ad6721db615c7d6f623c0200be0e6d6802df68c527655475450220446c4a4da9a0df5efcb57711ad61cf6167dfdda937bd0477189be8afedaedd0541210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22d', }, sequence: 4294967295, }, ], 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, }, ], vout: [ { value: 0, n: 0, scriptPubKey: { 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, }, ], vout: [ { value: 0, n: 0, scriptPubKey: { 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, }, { txid: '2e1c5d1060bf5216678e31ed52d2ca564b81a34ac1a10749c5e124d25ec3c7a2', hash: '2e1c5d1060bf5216678e31ed52d2ca564b81a34ac1a10749c5e124d25ec3c7a2', version: 2, size: 225, locktime: 0, vin: [ { txid: 'cb879adc0a388416eda7289020b9d7930e9df589db375e95947968673edfb780', vout: 0, scriptSig: { asm: '304402202950a75c2833d0400b38d4f47ad4af727388057bdffba90fe8950e2a69a1df8502206955582de9c6c99663895a114634e4fc03e7f2ede79555a4ad747888cdf1250d[ALL|FORKID] 0295236f3c965a760068a021a45a85f551adc2626ddc51d7539497d568affc148a', hex: '47304402202950a75c2833d0400b38d4f47ad4af727388057bdffba90fe8950e2a69a1df8502206955582de9c6c99663895a114634e4fc03e7f2ede79555a4ad747888cdf1250d41210295236f3c965a760068a021a45a85f551adc2626ddc51d7539497d568affc148a', }, sequence: 4294967295, }, ], vout: [ { value: 0.00005, n: 0, scriptPubKey: { asm: 'OP_DUP OP_HASH160 cd6b0a1b19b12d88e62bd265704b59e38e71fc55 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a914cd6b0a1b19b12d88e62bd265704b59e38e71fc5588ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qrxkkzsmrxcjmz8x90fx2uztt83cuu0u25vrmw66jk', ], }, }, { value: 0.00004545, n: 1, scriptPubKey: { asm: 'OP_DUP OP_HASH160 09401a690d52252acd1152c2ddd36c5081dff574 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a91409401a690d52252acd1152c2ddd36c5081dff57488ac', reqSigs: 1, type: 'pubkeyhash', addresses: [ 'bitcoincash:qqy5qxnfp4fz22kdz9fv9hwnd3ggrhl4wsekqyswf0', ], }, }, ], hex: '020000000180b7df3e67687994955e37db89f59d0e93d7b9209028a7ed1684380adc9a87cb000000006a47304402202950a75c2833d0400b38d4f47ad4af727388057bdffba90fe8950e2a69a1df8502206955582de9c6c99663895a114634e4fc03e7f2ede79555a4ad747888cdf1250d41210295236f3c965a760068a021a45a85f551adc2626ddc51d7539497d568affc148affffffff0288130000000000001976a914cd6b0a1b19b12d88e62bd265704b59e38e71fc5588acc1110000000000001976a91409401a690d52252acd1152c2ddd36c5081dff57488ac00000000', blockhash: '00000000000000000ee5254eb809e121138497bdd9b5053cf2738714220b1ce2', confirmations: 5625, time: 1635509252, blocktime: 1635509252, }, { blockhash: '0000000000000000005aa0636bbc6b0117417d1db091b911259023885b5c0d12', blocktime: 1643286535, confirmations: 3, hash: '8b569d64a7e51d1d3cf1cf2b99d8b34451bbebc7df6b67232e5b770418b0428c', hex: '0200000002682a61fe5fe1f6468f5ebd9b6bfecd87d37281709e44222583f1868912f9b9ea020000006b483045022100d9bcfbeb1f27565b4cd3609eded0ce36cbac6472aa4fba0551cb9f5eae7ca3460220712263d13a6033a133eb67a96dba1ee626cfc6024ccd66825963692e2abd1144412102394542bf928bc707dcc156acf72e87c9d2fef77eaefc5f6b836d9ceeb0fc6a3effffffff5d4905ee2f09b0c3c131fe1b862f9882c2b8d79c9abf4a0351bbd03bacffd40a020000006a473044022059c39ed0798da7a4788355120d09737468ab182940ec78c3de1a2a23995c99aa02201bde53d7155892a145966149eedba665fbe02475a34b15a84c5d9a3d4b787d97412102394542bf928bc707dcc156acf72e87c9d2fef77eaefc5f6b836d9ceeb0fc6a3effffffff030000000000000000376a04534c500001010453454e44200203c768a66eba24affb19db1375b19388b6a0f9e1103b772de4d9f8f63ba79e08000000000000196422020000000000001976a9140b7d35fda03544a08e65464d54cfae4257eb6db788aca80a0000000000001976a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac00000000', locktime: 0, size: 437, time: 1643286535, txid: '8b569d64a7e51d1d3cf1cf2b99d8b34451bbebc7df6b67232e5b770418b0428c', version: 2, vin: [ { scriptSig: { asm: '3045022100d9bcfbeb1f27565b4cd3609eded0ce36cbac6472aa4fba0551cb9f5eae7ca3460220712263d13a6033a133eb67a96dba1ee626cfc6024ccd66825963692e2abd1144[ALL|FORKID] 02394542bf928bc707dcc156acf72e87c9d2fef77eaefc5f6b836d9ceeb0fc6a3e', hex: '483045022100d9bcfbeb1f27565b4cd3609eded0ce36cbac6472aa4fba0551cb9f5eae7ca3460220712263d13a6033a133eb67a96dba1ee626cfc6024ccd66825963692e2abd1144412102394542bf928bc707dcc156acf72e87c9d2fef77eaefc5f6b836d9ceeb0fc6a3e', }, sequence: 4294967295, txid: 'eab9f9128986f1832522449e708172d387cdfe6b9bbd5e8f46f6e15ffe612a68', vout: 2, }, { scriptSig: { asm: '3044022059c39ed0798da7a4788355120d09737468ab182940ec78c3de1a2a23995c99aa02201bde53d7155892a145966149eedba665fbe02475a34b15a84c5d9a3d4b787d97[ALL|FORKID] 02394542bf928bc707dcc156acf72e87c9d2fef77eaefc5f6b836d9ceeb0fc6a3e', hex: '473044022059c39ed0798da7a4788355120d09737468ab182940ec78c3de1a2a23995c99aa02201bde53d7155892a145966149eedba665fbe02475a34b15a84c5d9a3d4b787d97412102394542bf928bc707dcc156acf72e87c9d2fef77eaefc5f6b836d9ceeb0fc6a3e', }, sequence: 4294967295, txid: '0ad4ffac3bd0bb51034abf9a9cd7b8c282982f861bfe31c1c3b0092fee05495d', vout: 2, }, ], vout: [ { n: 0, scriptPubKey: { asm: 'OP_RETURN 5262419 1 1145980243 0203c768a66eba24affb19db1375b19388b6a0f9e1103b772de4d9f8f63ba79e 0000000000001964', hex: '6a04534c500001010453454e44200203c768a66eba24affb19db1375b19388b6a0f9e1103b772de4d9f8f63ba79e080000000000001964', type: 'nulldata', }, value: 0, }, { n: 1, scriptPubKey: { addresses: [ 'bitcoincash:qq9h6d0a5q65fgywv4ry64x04ep906mdku7ymranw3', ], asm: 'OP_DUP OP_HASH160 0b7d35fda03544a08e65464d54cfae4257eb6db7 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac', reqSigs: 1, type: 'pubkeyhash', }, value: 0.00000546, }, { n: 2, scriptPubKey: { addresses: [ 'bitcoincash:qq9h6d0a5q65fgywv4ry64x04ep906mdku7ymranw3', ], asm: 'OP_DUP OP_HASH160 0b7d35fda03544a08e65464d54cfae4257eb6db7 OP_EQUALVERIFY OP_CHECKSIG', hex: '76a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac', reqSigs: 1, type: 'pubkeyhash', }, value: 0.00002728, }, ], }, { txid: 'd3e1b8a65f9d50363cad9a496f7cecab59c9415dd9bcfd6f56c0c5dd4dffa7af', hash: 'd3e1b8a65f9d50363cad9a496f7cecab59c9415dd9bcfd6f56c0c5dd4dffa7af', version: 2, size: 223, locktime: 0, vin: [ { txid: 'af49a2d631d42c499ea84a2c87610107745c68a7a20db279ac05f4b338ddde41', vout: 1, scriptSig: [Object], sequence: 4294967295, }, ], vout: [ { value: 0, n: 0, scriptPubKey: [Object] }, { value: 0.61760311, n: 1, scriptPubKey: [Object] }, ], hex: '020000000141dedd38b3f405ac79b20da2a7685c74070161872c4aa89e492cd431d6a249af010000006a4730440220420ce5b6e48e94908b989dc188f8d63c61c683b11efa31a7e953badf0c2f5541022070d11bec6a06b2b2229bf4aaf0cb7217e932b8746c0d9de42609bd5e24530f73412102e4af47715f4db1d2a8d686be40c42bba5e70d715e470314181730e797be2324bffffffff020000000000000000176a026d021274657374696e67206d6573736167652031323763ae03000000001976a914b366ef7c1ffd4ef452d72556634720cc8741e1dc88ac00000000', blockhash: '0000000000000000018f17add8efc906069972863332a841d623d247d3a7a331', confirmations: 23746, time: 1635504376, blocktime: 1635504376, }, + { + blockhash: + '0000000000000000059e4631cc62dd5cab414a0ff7e6686146f6ea299308cd73', + blocktime: 1651757835, + confirmations: 2, + hash: 'ff253d06a1f3cce088d541dc28d06cf08cebc0288c2ae21e8985df08109b45d8', + hex: '020000000493eb64a7c34b16924dd9e4242578127f304589dd3f3f83e6cb55735f2e990c1c120000006a473044022021fc8cd1215e556a60fded985365bc6120c2d2a0b431e7fa74318707e5b6720b0220170ce94e71d6aa3ac29a7c51eb23b27ab623b8a5ed40af52493915562c77f6464121031e9483074a9f0ee7380131a870edbe9403e7b807a4b5611b01540a150f6aa454ffffffff93eb64a7c34b16924dd9e4242578127f304589dd3f3f83e6cb55735f2e990c1c150000006a47304402200d449f668a887d374dc21b9a4e0d2be02b3490267d15f776e7c49e4121ce4dbb022037cbff8f59bdbb1d3d6c4557cd39a1700950bb0a29aefff6e4c164b32bf648ad4121031e9483074a9f0ee7380131a870edbe9403e7b807a4b5611b01540a150f6aa454ffffffffc54af56fdc3ed4d85cd347e3a82ae138bdb86def53483a3d796d1c9216e895bd020000006b483045022100a5f625d5f5d4bc97cb37ba769300702161d0a0b9a038e423066e63b9c9ad337702201190cb938ad3b3d25421355c822b145407a6b5e6c212dd77a213331ee7ce276e4121031e9483074a9f0ee7380131a870edbe9403e7b807a4b5611b01540a150f6aa454ffffffff8bbc356cfac1b9853b469bb2c6b8b36d8cd6010252f589aca6fb4f6a0f4f0154020000006b4830450221008ea97a9e9a9803bcc7b4ea97922d1f868d50383dd6f635384e1b7f7f36fc56250220608abed6720c74b699be918ba485b8268a5958f51955ce9fac85c25793f599824121031e9483074a9f0ee7380131a870edbe9403e7b807a4b5611b01540a150f6aa454ffffffff020000000000000000536a0464726f70403163366339633634643730623238356265666537333366313735643066333834353338353736383736626432383062313035383764663831323739643366356504007461620662616e616e61c0030000000000001976a914f627e51001a51a1a92d8927808701373cf29267f88ac00000000', + locktime: 0, + size: 726, + time: 1651757835, + txid: 'ff253d06a1f3cce088d541dc28d06cf08cebc0288c2ae21e8985df08109b45d8', + version: 2, + vin: [ + { + scriptSig: { + asm: '3044022021fc8cd1215e556a60fded985365bc6120c2d2a0b431e7fa74318707e5b6720b0220170ce94e71d6aa3ac29a7c51eb23b27ab623b8a5ed40af52493915562c77f646[ALL|FORKID] 031e9483074a9f0ee7380131a870edbe9403e7b807a4b5611b01540a150f6aa454', + hex: '473044022021fc8cd1215e556a60fded985365bc6120c2d2a0b431e7fa74318707e5b6720b0220170ce94e71d6aa3ac29a7c51eb23b27ab623b8a5ed40af52493915562c77f6464121031e9483074a9f0ee7380131a870edbe9403e7b807a4b5611b01540a150f6aa454', + }, + sequence: 4294967295, + txid: '1c0c992e5f7355cbe6833f3fdd8945307f12782524e4d94d92164bc3a764eb93', + vout: 18, + }, + { + scriptSig: { + asm: '304402200d449f668a887d374dc21b9a4e0d2be02b3490267d15f776e7c49e4121ce4dbb022037cbff8f59bdbb1d3d6c4557cd39a1700950bb0a29aefff6e4c164b32bf648ad[ALL|FORKID] 031e9483074a9f0ee7380131a870edbe9403e7b807a4b5611b01540a150f6aa454', + hex: '47304402200d449f668a887d374dc21b9a4e0d2be02b3490267d15f776e7c49e4121ce4dbb022037cbff8f59bdbb1d3d6c4557cd39a1700950bb0a29aefff6e4c164b32bf648ad4121031e9483074a9f0ee7380131a870edbe9403e7b807a4b5611b01540a150f6aa454', + }, + sequence: 4294967295, + txid: '1c0c992e5f7355cbe6833f3fdd8945307f12782524e4d94d92164bc3a764eb93', + vout: 21, + }, + { + scriptSig: { + asm: '3045022100a5f625d5f5d4bc97cb37ba769300702161d0a0b9a038e423066e63b9c9ad337702201190cb938ad3b3d25421355c822b145407a6b5e6c212dd77a213331ee7ce276e[ALL|FORKID] 031e9483074a9f0ee7380131a870edbe9403e7b807a4b5611b01540a150f6aa454', + hex: '483045022100a5f625d5f5d4bc97cb37ba769300702161d0a0b9a038e423066e63b9c9ad337702201190cb938ad3b3d25421355c822b145407a6b5e6c212dd77a213331ee7ce276e4121031e9483074a9f0ee7380131a870edbe9403e7b807a4b5611b01540a150f6aa454', + }, + sequence: 4294967295, + txid: 'bd95e816921c6d793d3a4853ef6db8bd38e12aa8e347d35cd8d43edc6ff54ac5', + vout: 2, + }, + { + scriptSig: { + asm: '30450221008ea97a9e9a9803bcc7b4ea97922d1f868d50383dd6f635384e1b7f7f36fc56250220608abed6720c74b699be918ba485b8268a5958f51955ce9fac85c25793f59982[ALL|FORKID] 031e9483074a9f0ee7380131a870edbe9403e7b807a4b5611b01540a150f6aa454', + hex: '4830450221008ea97a9e9a9803bcc7b4ea97922d1f868d50383dd6f635384e1b7f7f36fc56250220608abed6720c74b699be918ba485b8268a5958f51955ce9fac85c25793f599824121031e9483074a9f0ee7380131a870edbe9403e7b807a4b5611b01540a150f6aa454', + }, + sequence: 4294967295, + txid: '54014f0f6a4ffba6ac89f5520201d68c6db3b8c6b29b463b85b9c1fa6c35bc8b', + vout: 2, + }, + ], + vout: [ + { + n: 0, + scriptPubKey: { + asm: 'OP_RETURN 1886351972 31633663396336346437306232383562656665373333663137356430663338343533383537363837366264323830623130353837646638313237396433663565 1650553856 62616e616e61', + hex: '6a0464726f70403163366339633634643730623238356265666537333366313735643066333834353338353736383736626432383062313035383764663831323739643366356504007461620662616e616e61', + type: 'nulldata', + }, + value: 0, + }, + { + n: 1, + scriptPubKey: { + addresses: [ + 'bitcoincash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0ul96a2ens', + ], + asm: 'OP_DUP OP_HASH160 f627e51001a51a1a92d8927808701373cf29267f OP_EQUALVERIFY OP_CHECKSIG', + hex: '76a914f627e51001a51a1a92d8927808701373cf29267f88ac', + reqSigs: 1, + type: 'pubkeyhash', + }, + value: 0.0000096, + }, + ], + }, ]; diff --git a/web/cashtab/src/hooks/__tests__/useBCH.test.js b/web/cashtab/src/hooks/__tests__/useBCH.test.js index 7abf00a2e..9e670f1cc 100644 --- a/web/cashtab/src/hooks/__tests__/useBCH.test.js +++ b/web/cashtab/src/hooks/__tests__/useBCH.test.js @@ -1,659 +1,675 @@ /* eslint-disable no-native-reassign */ import useBCH from '../useBCH'; import mockReturnGetHydratedUtxoDetails from '../__mocks__/mockReturnGetHydratedUtxoDetails'; import mockReturnGetSlpBalancesAndUtxos from '../__mocks__/mockReturnGetSlpBalancesAndUtxos'; import mockReturnGetHydratedUtxoDetailsWithZeroBalance from '../__mocks__/mockReturnGetHydratedUtxoDetailsWithZeroBalance'; import mockReturnGetSlpBalancesAndUtxosNoZeroBalance from '../__mocks__/mockReturnGetSlpBalancesAndUtxosNoZeroBalance'; import sendBCHMock from '../__mocks__/sendBCH'; import createTokenMock from '../__mocks__/createToken'; import mockTxHistory from '../__mocks__/mockTxHistory'; import mockFlatTxHistory from '../__mocks__/mockFlatTxHistory'; import mockTxDataWithPassthrough from '../__mocks__/mockTxDataWithPassthrough'; import mockPublicKeys from '../__mocks__/mockPublicKeys'; import { flattenedHydrateUtxosResponse, legacyHydrateUtxosResponse, } from '../__mocks__/mockHydrateUtxosBatched'; import { tokenSendWdt, tokenReceiveGarmonbozia, tokenReceiveTBS, tokenGenesisCashtabMintAlpha, } from '../__mocks__/mockParseTokenInfoForTxHistory'; import { mockSentCashTx, mockReceivedCashTx, mockSentTokenTx, mockReceivedTokenTx, mockSentOpReturnMessageTx, mockReceivedOpReturnMessageTx, mockBurnEtokenTx, + mockSentAirdropOpReturnMessageTx, } from '../__mocks__/mockParsedTxs'; import BCHJS from '@psf/bch-js'; // TODO: should be removed when external lib not needed anymore import { currency } from '../../components/Common/Ticker'; import BigNumber from 'bignumber.js'; import { fromSmallestDenomination } from 'utils/cashMethods'; describe('useBCH hook', () => { it('gets Rest Api Url on testnet', () => { process = { env: { REACT_APP_NETWORK: `testnet`, REACT_APP_BCHA_APIS: 'https://rest.kingbch.com/v3/,https://wallet-service-prod.bitframe.org/v3/,notevenaurl,https://rest.kingbch.com/v3/', REACT_APP_BCHA_APIS_TEST: 'https://free-test.fullstack.cash/v3/', }, }; const { getRestUrl } = useBCH(); const expectedApiUrl = `https://free-test.fullstack.cash/v3/`; expect(getRestUrl(0)).toBe(expectedApiUrl); }); it('gets primary Rest API URL on mainnet', () => { process = { env: { REACT_APP_BCHA_APIS: 'https://rest.kingbch.com/v3/,https://wallet-service-prod.bitframe.org/v3/,notevenaurl,https://rest.kingbch.com/v3/', REACT_APP_NETWORK: 'mainnet', }, }; const { getRestUrl } = useBCH(); const expectedApiUrl = `https://rest.kingbch.com/v3/`; expect(getRestUrl(0)).toBe(expectedApiUrl); }); it('calculates fee correctly for 2 P2PKH outputs', () => { const { calcFee } = useBCH(); const BCH = new BCHJS(); const utxosMock = [{}, {}]; expect(calcFee(BCH, utxosMock, 2, 1.01)).toBe(378); }); it('gets SLP and BCH balances and utxos from hydrated utxo details', async () => { const { getSlpBalancesAndUtxos } = useBCH(); const BCH = new BCHJS(); const result = await getSlpBalancesAndUtxos( BCH, mockReturnGetHydratedUtxoDetails, ); expect(result).toStrictEqual(mockReturnGetSlpBalancesAndUtxos); }); it(`Ignores SLP utxos with utxo.tokenQty === '0'`, async () => { const { getSlpBalancesAndUtxos } = useBCH(); const BCH = new BCHJS(); const result = await getSlpBalancesAndUtxos( BCH, mockReturnGetHydratedUtxoDetailsWithZeroBalance, ); expect(result).toStrictEqual( mockReturnGetSlpBalancesAndUtxosNoZeroBalance, ); }); it(`Parses flattened batched hydrateUtxosResponse to yield same result as legacy unbatched hydrateUtxosResponse`, async () => { const { getSlpBalancesAndUtxos } = useBCH(); const BCH = new BCHJS(); const batchedResult = await getSlpBalancesAndUtxos( BCH, flattenedHydrateUtxosResponse, ); const legacyResult = await getSlpBalancesAndUtxos( BCH, legacyHydrateUtxosResponse, ); expect(batchedResult).toStrictEqual(legacyResult); }); it('sends XEC correctly', async () => { const { sendXec } = useBCH(); const BCH = new BCHJS(); const { expectedTxId, expectedHex, utxos, wallet, destinationAddress, sendAmount, } = sendBCHMock; BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); expect( await sendXec( BCH, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, sendAmount, ), ).toBe(`${currency.blockExplorerUrl}/tx/${expectedTxId}`); expect(BCH.RawTransactions.sendRawTransaction).toHaveBeenCalledWith( expectedHex, ); }); it('sends XEC correctly with an encrypted OP_RETURN message', async () => { const { sendXec } = useBCH(); const BCH = new BCHJS(); const { expectedTxId, utxos, wallet, destinationAddress, sendAmount } = sendBCHMock; const expectedPubKeyResponse = { success: true, publicKey: '03451a3e61ae8eb76b8d4cd6057e4ebaf3ef63ae3fe5f441b72c743b5810b6a389', }; BCH.encryption.getPubKey = jest .fn() .mockResolvedValue(expectedPubKeyResponse); BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); expect( await sendXec( BCH, wallet, utxos, currency.defaultFee, 'This is an encrypted opreturn message', false, null, destinationAddress, sendAmount, true, // encryption flag for the OP_RETURN message ), ).toBe(`${currency.blockExplorerUrl}/tx/${expectedTxId}`); }); it('sends one to many XEC correctly', async () => { const { sendXec } = useBCH(); const BCH = new BCHJS(); const { expectedTxId, expectedHex, utxos, wallet, destinationAddress, sendAmount, } = sendBCHMock; const addressAndValueArray = [ 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05,6', 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05,6.8', 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05,7', 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05,6', ]; BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); expect( await sendXec( BCH, wallet, utxos, currency.defaultFee, '', true, addressAndValueArray, ), ).toBe(`${currency.blockExplorerUrl}/tx/${expectedTxId}`); }); it(`Throws error if called trying to send one base unit ${currency.ticker} more than available in utxo set`, async () => { const { sendXec } = useBCH(); const BCH = new BCHJS(); const { expectedTxId, utxos, wallet, destinationAddress } = sendBCHMock; const expectedTxFeeInSats = 229; BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); const oneBaseUnitMoreThanBalance = new BigNumber(utxos[0].value) .minus(expectedTxFeeInSats) .plus(1) .div(10 ** currency.cashDecimals) .toString(); const failedSendBch = sendXec( BCH, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, oneBaseUnitMoreThanBalance, ); expect(failedSendBch).rejects.toThrow(new Error('Insufficient funds')); const nullValuesSendBch = await sendXec( BCH, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, null, ); expect(nullValuesSendBch).toBe(null); }); it('Throws error on attempt to send one satoshi less than backend dust limit', async () => { const { sendXec } = useBCH(); const BCH = new BCHJS(); const { expectedTxId, utxos, wallet, destinationAddress } = sendBCHMock; BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); const failedSendBch = sendXec( BCH, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, new BigNumber( fromSmallestDenomination(currency.dustSats).toString(), ) .minus(new BigNumber('0.00000001')) .toString(), ); expect(failedSendBch).rejects.toThrow(new Error('dust')); const nullValuesSendBch = await sendXec( BCH, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, null, ); expect(nullValuesSendBch).toBe(null); }); it("throws error attempting to burn an eToken ID that is not within the wallet's utxo", async () => { const { burnEtoken } = useBCH(); const BCH = new BCHJS(); const { wallet } = sendBCHMock; const burnAmount = 10; const eTokenId = '0203c768a66eba24affNOTVALID103b772de4d9f8f63ba79e'; const expectedError = 'No token UTXOs for the specified token could be found.'; let thrownError; try { await burnEtoken(BCH, wallet, mockReturnGetSlpBalancesAndUtxos, { eTokenId, burnAmount, }); } catch (err) { thrownError = err; } expect(thrownError).toStrictEqual(new Error(expectedError)); }); it('receives errors from the network and parses it', async () => { const { sendXec } = useBCH(); const BCH = new BCHJS(); const { sendAmount, utxos, wallet, destinationAddress } = sendBCHMock; BCH.RawTransactions.sendRawTransaction = jest .fn() .mockImplementation(async () => { throw new Error('insufficient priority (code 66)'); }); const insufficientPriority = sendXec( BCH, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, sendAmount, ); await expect(insufficientPriority).rejects.toThrow( new Error('insufficient priority (code 66)'), ); BCH.RawTransactions.sendRawTransaction = jest .fn() .mockImplementation(async () => { throw new Error('txn-mempool-conflict (code 18)'); }); const txnMempoolConflict = sendXec( BCH, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, sendAmount, ); await expect(txnMempoolConflict).rejects.toThrow( new Error('txn-mempool-conflict (code 18)'), ); BCH.RawTransactions.sendRawTransaction = jest .fn() .mockImplementation(async () => { throw new Error('Network Error'); }); const networkError = sendXec( BCH, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, sendAmount, ); await expect(networkError).rejects.toThrow(new Error('Network Error')); BCH.RawTransactions.sendRawTransaction = jest .fn() .mockImplementation(async () => { const err = new Error( 'too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)', ); throw err; }); const tooManyAncestorsMempool = sendXec( BCH, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, sendAmount, ); await expect(tooManyAncestorsMempool).rejects.toThrow( new Error( 'too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)', ), ); }); it('creates a token correctly', async () => { const { createToken } = useBCH(); const BCH = new BCHJS(); const { expectedTxId, expectedHex, wallet, configObj } = createTokenMock; BCH.RawTransactions.sendRawTransaction = jest .fn() .mockResolvedValue(expectedTxId); expect(await createToken(BCH, wallet, 5.01, configObj)).toBe( `${currency.tokenExplorerUrl}/tx/${expectedTxId}`, ); expect(BCH.RawTransactions.sendRawTransaction).toHaveBeenCalledWith( expectedHex, ); }); it('Throws correct error if user attempts to create a token with an invalid wallet', async () => { const { createToken } = useBCH(); const BCH = new BCHJS(); const { invalidWallet, configObj } = createTokenMock; const invalidWalletTokenCreation = createToken( BCH, invalidWallet, currency.defaultFee, configObj, ); await expect(invalidWalletTokenCreation).rejects.toThrow( new Error('Invalid wallet'), ); }); it('Correctly flattens transaction history', () => { const { flattenTransactions } = useBCH(); expect(flattenTransactions(mockTxHistory, 10)).toStrictEqual( mockFlatTxHistory, ); }); it(`Correctly parses a "send ${currency.ticker}" transaction`, async () => { const { parseTxData } = useBCH(); const BCH = new BCHJS(); expect( await parseTxData( BCH, [mockTxDataWithPassthrough[0]], mockPublicKeys, ), ).toStrictEqual(mockSentCashTx); }); it(`Correctly parses a "receive ${currency.ticker}" transaction`, async () => { const { parseTxData } = useBCH(); const BCH = new BCHJS(); expect( await parseTxData( BCH, [mockTxDataWithPassthrough[5]], mockPublicKeys, ), ).toStrictEqual(mockReceivedCashTx); }); it(`Correctly parses a "send ${currency.tokenTicker}" transaction`, async () => { const { parseTxData } = useBCH(); const BCH = new BCHJS(); expect( await parseTxData( BCH, [mockTxDataWithPassthrough[1]], mockPublicKeys, ), ).toStrictEqual(mockSentTokenTx); }); it(`Correctly parses a "burn ${currency.tokenTicker}" transaction`, async () => { const { parseTxData } = useBCH(); const BCH = new BCHJS(); expect( await parseTxData( BCH, [mockTxDataWithPassthrough[13]], mockPublicKeys, ), ).toStrictEqual(mockBurnEtokenTx); }); it(`Correctly parses a "receive ${currency.tokenTicker}" transaction`, async () => { const { parseTxData } = useBCH(); const BCH = new BCHJS(); expect( await parseTxData( BCH, [mockTxDataWithPassthrough[3]], mockPublicKeys, ), ).toStrictEqual(mockReceivedTokenTx); }); it(`Correctly parses a "send ${currency.tokenTicker}" transaction with token details`, () => { const { parseTokenInfoForTxHistory } = useBCH(); const BCH = new BCHJS(); expect( parseTokenInfoForTxHistory( BCH, tokenSendWdt.parsedTx, tokenSendWdt.tokenInfo, ), ).toStrictEqual(tokenSendWdt.cashtabTokenInfo); }); it(`Correctly parses a "receive ${currency.tokenTicker}" transaction with token details and 9 decimals of precision`, () => { const { parseTokenInfoForTxHistory } = useBCH(); const BCH = new BCHJS(); expect( parseTokenInfoForTxHistory( BCH, tokenReceiveTBS.parsedTx, tokenReceiveTBS.tokenInfo, ), ).toStrictEqual(tokenReceiveTBS.cashtabTokenInfo); }); it(`Correctly parses a "receive ${currency.tokenTicker}" transaction from an HD wallet (change address different from sending address)`, () => { const { parseTokenInfoForTxHistory } = useBCH(); const BCH = new BCHJS(); expect( parseTokenInfoForTxHistory( BCH, tokenReceiveGarmonbozia.parsedTx, tokenReceiveGarmonbozia.tokenInfo, ), ).toStrictEqual(tokenReceiveGarmonbozia.cashtabTokenInfo); }); it(`Correctly parses a "GENESIS ${currency.tokenTicker}" transaction with token details`, () => { const { parseTokenInfoForTxHistory } = useBCH(); const BCH = new BCHJS(); expect( parseTokenInfoForTxHistory( BCH, tokenGenesisCashtabMintAlpha.parsedTx, tokenGenesisCashtabMintAlpha.tokenInfo, ), ).toStrictEqual(tokenGenesisCashtabMintAlpha.cashtabTokenInfo); }); it(`Correctly parses a "send ${currency.ticker}" transaction with an OP_RETURN message`, async () => { const { parseTxData } = useBCH(); const BCH = new BCHJS(); BCH.RawTransactions.getRawTransaction = jest .fn() .mockResolvedValue(mockTxDataWithPassthrough[14]); expect( await parseTxData( BCH, [mockTxDataWithPassthrough[10]], mockPublicKeys, ), ).toStrictEqual(mockSentOpReturnMessageTx); }); + it(`Correctly parses a "send ${currency.ticker}" airdrop transaction with an OP_RETURN message`, async () => { + const { parseTxData } = useBCH(); + const BCH = new BCHJS(); + BCH.RawTransactions.getRawTransaction = jest + .fn() + .mockResolvedValue(mockTxDataWithPassthrough[15]); + expect( + await parseTxData( + BCH, + [mockTxDataWithPassthrough[15]], + mockPublicKeys, + ), + ).toStrictEqual(mockSentAirdropOpReturnMessageTx); + }); + it(`Correctly parses a "receive ${currency.ticker}" transaction with an OP_RETURN message`, async () => { const { parseTxData } = useBCH(); const BCH = new BCHJS(); BCH.RawTransactions.getRawTransaction = jest .fn() .mockResolvedValue(mockTxDataWithPassthrough[12]); expect( await parseTxData( BCH, [mockTxDataWithPassthrough[11]], mockPublicKeys, ), ).toStrictEqual(mockReceivedOpReturnMessageTx); }); it(`handleEncryptedOpReturn() correctly encrypts a message based on a valid cash address`, async () => { const { handleEncryptedOpReturn } = useBCH(); const BCH = new BCHJS(); const destinationAddress = 'bitcoincash:qqvuj09f80sw9j7qru84ptxf0hyqffc38gstxfs5ru'; const message = 'This message is encrypted by ecies-lite with default parameters'; const expectedPubKeyResponse = { success: true, publicKey: '03208c4f52229e021ddec5fc6e07a59fd66388ac52bc2a2c1e0f1afb24b0e275ac', }; BCH.encryption.getPubKey = jest .fn() .mockResolvedValue(expectedPubKeyResponse); const result = await handleEncryptedOpReturn( BCH, destinationAddress, Buffer.from(message), ); // loop through each ecies encryption parameter from the object returned from the handleEncryptedOpReturn() call for (const k of Object.keys(result)) { switch (result[k].toString()) { case 'epk': // verify the sender's ephemeral public key buffer expect(result[k].toString()).toEqual( 'BPxEy0o7QsRok2GSpuLU27g0EqLIhf6LIxHx7P5UTZF9EFuQbqGzr5cCA51qVnvIJ9CZ84iW1DeDdvhg/EfPSas=', ); break; case 'iv': // verify the initialization vector for the cipher algorithm expect(result[k].toString()).toEqual( '2FcU3fRZUOBt7dqshZjd+g==', ); break; case 'ct': // verify the encrypted message buffer expect(result[k].toString()).toEqual( 'wVxPjv/ZiQ4etHqqTTIEoKvYYf4po05I/kNySrdsN3verxlHI07Rbob/VfF4MDfYHpYmDwlR9ax1shhdSzUG/A==', ); break; case 'mac': // verify integrity of the message (checksum) expect(result[k].toString()).toEqual( 'F9KxuR48O0wxa9tFYq6/Hy3joI2edKxLFSeDVk6JKZE=', ); break; } } }); it(`getRecipientPublicKey() correctly retrieves the public key of a cash address`, async () => { const { getRecipientPublicKey } = useBCH(); const BCH = new BCHJS(); const expectedPubKeyResponse = { success: true, publicKey: '03208c4f52229e021ddec5fc6e07a59fd66388ac52bc2a2c1e0f1afb24b0e275ac', }; const expectedPubKey = '03208c4f52229e021ddec5fc6e07a59fd66388ac52bc2a2c1e0f1afb24b0e275ac'; const destinationAddress = 'bitcoincash:qqvuj09f80sw9j7qru84ptxf0hyqffc38gstxfs5ru'; BCH.encryption.getPubKey = jest .fn() .mockResolvedValue(expectedPubKeyResponse); expect(await getRecipientPublicKey(BCH, destinationAddress)).toBe( expectedPubKey, ); }); }); diff --git a/web/cashtab/src/hooks/useBCH.js b/web/cashtab/src/hooks/useBCH.js index d1d434871..e707f85ff 100644 --- a/web/cashtab/src/hooks/useBCH.js +++ b/web/cashtab/src/hooks/useBCH.js @@ -1,1723 +1,1733 @@ import BigNumber from 'bignumber.js'; import { currency } from 'components/Common/Ticker'; import { isValidTokenStats } from 'utils/validation'; import SlpWallet from 'minimal-slp-wallet'; import { toSmallestDenomination, fromSmallestDenomination, batchArray, flattenBatchedHydratedUtxos, isValidStoredWallet, checkNullUtxosForTokenStatus, confirmNonEtokenUtxos, convertToEncryptStruct, getPublicKey, parseOpReturn, } from 'utils/cashMethods'; import cashaddr from 'ecashaddrjs'; import ecies from 'ecies-lite'; import wif from 'wif'; 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]; }; 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 = async (BCH, txData, publicKeys, wallet) => { /* 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 there was an error in getting the tx data from api, 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; let isEncryptedMessage = false; let decryptionSuccess = false; // Assume an incoming transaction let outgoingTx = false; let tokenTx = false; let substring = ''; let airdropFlag = false; + let airdropTokenId = ''; // If vin's scriptSig contains one of the publicKeys of this wallet // This is an outgoing tx for (let j = 0; j < tx.vin.length; j += 1) { // Since Cashtab only concerns with utxos of Path145, Path245 and Path1899 addresses, // which are hashes of thier public keys. We can safely assume that Cashtab can only // consumes utxos of type 'pubkeyhash' // Therefore, only tx with vin's scriptSig of type 'pubkeyhash' can potentially be an outgoing tx. // any other scriptSig type indicates that the tx is incoming. try { const thisInputScriptSig = tx.vin[j].scriptSig; let inputPubKey = undefined; const inputType = BCH.Script.classifyInput( BCH.Script.decode( Buffer.from(thisInputScriptSig.hex, 'hex'), ), ); if (inputType === 'pubkeyhash') { inputPubKey = thisInputScriptSig.hex.substring( thisInputScriptSig.hex.length - 66, ); } publicKeys.forEach(pubKey => { if (pubKey === inputPubKey) { // This is an outgoing transaction outgoingTx = true; } }); if (outgoingTx === true) break; } catch (err) { console.log( "useBCH.parsedTxHistory() error: in trying to classify Input' scriptSig", ); } } // 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 hex = thisOutput.scriptPubKey.hex; let parsedOpReturnArray = parseOpReturn(hex); if (!parsedOpReturnArray) { console.log( 'useBCH.parsedTxData() error: parsed array is empty', ); break; } let message = ''; let txType = parsedOpReturnArray[0]; if (txType === currency.opReturn.appPrefixesHex.airdrop) { // this is to facilitate special Cashtab-specific cases of airdrop txs, both with and without msgs // The UI via Tx.js can check this airdropFlag attribute in the parsedTx object to conditionally render airdrop-specific formatting if it's true airdropFlag = true; - txType = parsedOpReturnArray[1]; + + txType = parsedOpReturnArray[2]; // 0 is drop, 1 is etoken ID // remove the first airdrop prefix from array so the array parsing logic below can remain unchanged parsedOpReturnArray.shift(); + + airdropTokenId = parsedOpReturnArray[0]; // with the first element removed, the eToken ID is now pos. 0 + // remove the 2nd airdrop prefix for the etoken that the airdrop is based on + parsedOpReturnArray.shift(); } if (txType === currency.opReturn.appPrefixesHex.eToken) { // this is an eToken transaction tokenTx = true; } else if ( txType === currency.opReturn.appPrefixesHex.cashtab ) { // this is a Cashtab message try { opReturnMessage = Buffer.from( parsedOpReturnArray[1], 'hex', ); isCashtabMessage = true; } catch (err) { // soft error if an unexpected or invalid cashtab hex is encountered opReturnMessage = ''; console.log( 'useBCH.parsedTxData() error: invalid cashtab msg hex: ' + parsedOpReturnArray[1], ); } } else if ( txType === currency.opReturn.appPrefixesHex.cashtabEncrypted ) { // this is an encrypted Cashtab message let msgString = parsedOpReturnArray[1]; let fundingWif, privateKeyObj, privateKeyBuff; if ( wallet && wallet.state && wallet.state.slpBalancesAndUtxos && wallet.state.slpBalancesAndUtxos.nonSlpUtxos[0] ) { fundingWif = wallet.state.slpBalancesAndUtxos.nonSlpUtxos[0] .wif; privateKeyObj = wif.decode(fundingWif); privateKeyBuff = privateKeyObj.privateKey; if (!privateKeyBuff) { throw new Error('Private key extraction error'); } } else { break; } let structData; let decryptedMessage; try { // Convert the hex encoded message to a buffer const msgBuf = Buffer.from(msgString, 'hex'); // Convert the bufer into a structured object. structData = convertToEncryptStruct(msgBuf); decryptedMessage = await ecies.decrypt( privateKeyBuff, structData, ); decryptionSuccess = true; } catch (err) { console.log( 'useBCH.parsedTxData() decryption error: ' + err, ); decryptedMessage = 'Only the message recipient can view this'; } isCashtabMessage = true; isEncryptedMessage = true; opReturnMessage = decryptedMessage; } else { // this is an externally generated message message = txType; // index 0 is the message content in this instance // if there are more than one part to the external message const arrayLength = parsedOpReturnArray.length; for (let i = 1; i < arrayLength; i++) { message = message + parsedOpReturnArray[i]; } try { opReturnMessage = Buffer.from(message, 'hex'); } catch (err) { // soft error if an unexpected or invalid cashtab hex is encountered opReturnMessage = ''; console.log( 'useBCH.parsedTxData() error: invalid external msg hex: ' + substring, ); } } 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]; } } // If the tx is incoming get the address of the sender for this tx and encode into eCash address. // This is used for both Reply To Message and Contact List functions. let senderAddress = null; if (!outgoingTx) { const firstVin = tx.vin[0]; try { // get the tx that generated the first vin of this tx const firstVinTxData = await BCH.RawTransactions.getRawTransaction( firstVin.txid, true, ); // extract the address of the tx output let senderBchAddress = firstVinTxData.vout[firstVin.vout].scriptPubKey .addresses[0]; const { type, hash } = cashaddr.decode(senderBchAddress); senderAddress = cashaddr.encode('ecash', type, hash); } catch (err) { console.log( `Error in BCH.RawTransactions.getRawTransaction(${firstVin.txid}, true)`, ); } } // Construct parsedTx parsedTx.amountSent = amountSent; parsedTx.amountReceived = amountReceived; parsedTx.tokenTx = tokenTx; parsedTx.outgoingTx = outgoingTx; parsedTx.replyAddress = senderAddress; parsedTx.destinationAddress = destinationAddress; parsedTx.opReturnMessage = Buffer.from(opReturnMessage).toString(); parsedTx.isCashtabMessage = isCashtabMessage; parsedTx.isEncryptedMessage = isEncryptedMessage; parsedTx.decryptionSuccess = decryptionSuccess; parsedTx.airdropFlag = airdropFlag; + parsedTx.airdropTokenId = airdropTokenId; 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.getRawTransaction does not return address or blockheight let txDataWithPassThrough = {}; try { txDataWithPassThrough = await BCH.RawTransactions.getRawTransaction( flatTx.txid, true, ); } catch (err) { console.log( `Error in BCH.RawTransactions.getRawTransaction(${flatTx.txid}, true)`, ); 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, publicKeys, wallet) => { // Flatten tx history let flatTxs = flattenTransactions(txHistory); // Build array of promises to get tx data for all 10 transactions let getTxDataWithPassThroughPromises = []; for (let i = 0; i < flatTxs.length; i += 1) { const getTxDataWithPassThroughPromise = returnGetTxDataWithPassThroughPromise(BCH, flatTxs[i]); getTxDataWithPassThroughPromises.push( getTxDataWithPassThroughPromise, ); } // Get txData for the 10 most recent transactions let getTxDataWithPassThroughPromisesResponse; try { getTxDataWithPassThroughPromisesResponse = await Promise.all( getTxDataWithPassThroughPromises, ); const parsed = parseTxData( BCH, getTxDataWithPassThroughPromisesResponse, publicKeys, wallet, ); return parsed; } catch (err) { console.log( `Error in Promise.all(getTxDataWithPassThroughPromises):`, ); 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 addTokenTxDataToSingleTxPromises = []; for (let i = 0; i < parsedTxs.length; i += 1) { const addTokenTxDataToSingleTxPromise = returnAddTokenTxDataToSingleTxPromise(BCH, parsedTxs[i]); addTokenTxDataToSingleTxPromises.push( addTokenTxDataToSingleTxPromise, ); } let addTokenTxDataToSingleTxPromisesResponse; try { addTokenTxDataToSingleTxPromisesResponse = await Promise.all( addTokenTxDataToSingleTxPromises, ); return addTokenTxDataToSingleTxPromisesResponse; } catch (err) { console.log( `Error in Promise.all(addTokenTxDataToSingleTxPromises):`, ); 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.xecApiBatchSize, ); // 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 hydrateUtxosPromise = returnHydrateUtxosPromise( BCH, utxoSetForThisPromise, ); hydrateUtxosPromises.push(hydrateUtxosPromise); } } let hydrateUtxosPromisesResponse; try { hydrateUtxosPromisesResponse = await Promise.all( hydrateUtxosPromises, ); const flattenedBatchedHydratedUtxos = flattenBatchedHydratedUtxos( hydrateUtxosPromisesResponse, ); return flattenedBatchedHydratedUtxos; } catch (err) { console.log(`Error in Promise.all(hydrateUtxosPromises)`); console.log(err); return err; } }; const returnTxDataPromise = (BCH, txidBatch) => { return new Promise((resolve, reject) => { BCH.Electrumx.txData(txidBatch).then( result => { resolve(result); }, err => { reject(err); }, ); }); }; const returnGetTxDataWithPassThroughPromise = (BCH, flatTx) => { return new Promise((resolve, reject) => { getTxDataWithPassThrough(BCH, flatTx).then( result => { resolve(result); }, err => { reject(err); }, ); }); }; const returnAddTokenTxDataToSingleTxPromise = (BCH, parsedTx) => { return new Promise((resolve, reject) => { addTokenTxDataToSingleTx(BCH, parsedTx).then( result => { resolve(result); }, err => { reject(err); }, ); }); }; const returnHydrateUtxosPromise = (BCH, utxoSetForThisPromise) => { return new Promise((resolve, reject) => { BCH.SLP.Utils.hydrateUtxos(utxoSetForThisPromise).then( result => { resolve(result); }, err => { reject(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); } // segment the txids array into chunks under the api limit const batchedTxids = batchArray(txids, currency.xecApiBatchSize); // build an array of promises let txDataPromises = []; // loop through each batch of 20 txids for (let j = 0; j < batchedTxids.length; j += 1) { const txidsForThisPromise = batchedTxids[j]; // build the promise for the api call with the 20 txids in current batch const txDataPromise = returnTxDataPromise(BCH, txidsForThisPromise); txDataPromises.push(txDataPromise); } try { const txDataPromisesResponse = await Promise.all(txDataPromises); // Scan tx data for each utxo to confirm they are not eToken txs let thisTxDataResult; let nonEtokenUtxos = []; for (let k = 0; k < txDataPromisesResponse.length; k += 1) { thisTxDataResult = txDataPromisesResponse[k].transactions; nonEtokenUtxos = nonEtokenUtxos.concat( checkNullUtxosForTokenStatus(thisTxDataResult), ); } return nonEtokenUtxos; } catch (err) { console.log( `Error in checkNullUtxosForTokenStatus(nullUtxos)` + err, ); 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 => { 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 burnEtoken = async ( BCH, wallet, slpBalancesAndUtxos, { tokenId, amount }, ) => { // Handle error of user having no XEC if (slpBalancesAndUtxos.nonSlpUtxos.length === 0) { throw new Error(`You need some ${currency.ticker} to burn eTokens`); } 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 => { 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 finalTokenAmountBurnt = new BigNumber(0); let tokenAmountBeingBurnt = new BigNumber(amount); let tokenUtxosBeingBurnt = []; for (let i = 0; i < tokenUtxos.length; i++) { finalTokenAmountBurnt = finalTokenAmountBurnt.plus( new BigNumber(tokenUtxos[i].tokenQty), ); transactionBuilder.addInput( tokenUtxos[i].tx_hash, tokenUtxos[i].tx_pos, ); tokenUtxosBeingBurnt.push(tokenUtxos[i]); if (tokenAmountBeingBurnt.lte(finalTokenAmountBurnt)) { break; } } const slpBurnObj = BCH.SLP.TokenType1.generateBurnOpReturn( tokenUtxosBeingBurnt, tokenAmountBeingBurnt, ); if (!slpBurnObj) { throw new Error(`Invalid eToken burn transaction.`); } // Add OP_RETURN as first output. transactionBuilder.addOutput(slpBurnObj, 0); // Send dust transaction representing tokens being burnt. transactionBuilder.addOutput( BCH.SLP.Address.toLegacyAddress(largestBchUtxo.address), currency.etokenSats, ); // get byte count to calculate fee. paying 1 sat const txFee = calcFee( BCH, tokenUtxosBeingBurnt, 3, currency.defaultFee, ); // amount to send back to the address requesting the burn. 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'); } // Send it back from whence it came transactionBuilder.addOutput( BCH.Address.toLegacyAddress(largestBchUtxo.address), remainder, ); // Sign the transaction with the private key for the XEC 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 < tokenUtxosBeingBurnt.length; i++) { const thisUtxo = tokenUtxosBeingBurnt[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.tokenExplorerUrl}/tx/${txidStr}`; } else { link = `${currency.blockExplorerUrlTestnet}/tx/${txidStr}`; } 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 getRecipientPublicKey = async (BCH, recipientAddress) => { let recipientPubKey; try { recipientPubKey = await getPublicKey(BCH, recipientAddress); } catch (err) { console.log(`useBCH.getRecipientPublicKey() error: ` + err); throw err; } return recipientPubKey; }; const handleEncryptedOpReturn = async ( BCH, destinationAddress, optionalOpReturnMsg, ) => { let recipientPubKey, encryptedEj; try { recipientPubKey = await getRecipientPublicKey( BCH, destinationAddress, ); } catch (err) { console.log(`useBCH.handleEncryptedOpReturn() error: ` + err); throw err; } if (recipientPubKey === 'not found') { // if the API can't find a pub key, it is due to the wallet having no outbound tx throw new Error( 'Cannot send an encrypted message to a wallet with no outgoing transactions', ); } try { const pubKeyBuf = Buffer.from(recipientPubKey, 'hex'); const bufferedFile = Buffer.from(optionalOpReturnMsg); const structuredEj = await ecies.encrypt(pubKeyBuf, bufferedFile); // Serialize the encrypted data object encryptedEj = Buffer.concat([ structuredEj.epk, structuredEj.iv, structuredEj.ct, structuredEj.mac, ]); } catch (err) { console.log(`useBCH.handleEncryptedOpReturn() error: ` + err); throw err; } return encryptedEj; }; const sendXec = async ( BCH, wallet, utxos, feeInSatsPerByte, optionalOpReturnMsg, isOneToMany, destinationAddressAndValueArray, destinationAddress, sendAmount, encryptionFlag, airdropFlag, + airdropTokenId, ) => { try { let value = new BigNumber(0); if (isOneToMany) { // this is a one to many XEC transaction if ( !destinationAddressAndValueArray || !destinationAddressAndValueArray.length ) { throw new Error('Invalid destinationAddressAndValueArray'); } const arrayLength = destinationAddressAndValueArray.length; for (let i = 0; i < arrayLength; i++) { // add the total value being sent in this array of recipients value = BigNumber.sum( value, new BigNumber( destinationAddressAndValueArray[i].split(',')[1], ), ); } // If user is attempting to send an aggregate value that is 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'); } } else { // this is a one to one XEC transaction then check sendAmount // note: one to many transactions won't be sending a single sendAmount if (!sendAmount) { return null; } 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; } let script; // Start of building the OP_RETURN output. // only build the OP_RETURN output if the user supplied it if ( (optionalOpReturnMsg && typeof optionalOpReturnMsg !== 'undefined' && optionalOpReturnMsg.trim() !== '') || airdropFlag ) { if (encryptionFlag) { // if the user has opted to encrypt this message let encryptedEj; try { encryptedEj = await handleEncryptedOpReturn( BCH, destinationAddress, optionalOpReturnMsg, ); } catch (err) { console.log(`useBCH.sendXec() encryption error.`); throw err; } // build the OP_RETURN script with the encryption prefix script = [ BCH.Script.opcodes.OP_RETURN, // 6a Buffer.from( currency.opReturn.appPrefixesHex.cashtabEncrypted, 'hex', ), // 65746162 Buffer.from(encryptedEj), ]; } else { // this is an un-encrypted message if (airdropFlag) { // un-encrypted airdrop tx if (optionalOpReturnMsg) { // airdrop tx with message script = [ BCH.Script.opcodes.OP_RETURN, // 6a Buffer.from( currency.opReturn.appPrefixesHex.airdrop, 'hex', ), // drop + Buffer.from(airdropTokenId), Buffer.from( currency.opReturn.appPrefixesHex.cashtab, 'hex', ), // 00746162 Buffer.from(optionalOpReturnMsg), ]; } else { // airdrop tx with no message script = [ BCH.Script.opcodes.OP_RETURN, // 6a Buffer.from( currency.opReturn.appPrefixesHex.airdrop, 'hex', ), // drop + Buffer.from(airdropTokenId), Buffer.from( currency.opReturn.appPrefixesHex.cashtab, 'hex', ), // 00746162 ]; } } else { // non-airdrop un-encrypted message script = [ 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; // A normal tx will have 2 outputs, destination and change // A one to many tx will have n outputs + 1 change output, where n is the number of recipients const txOutputs = isOneToMany ? destinationAddressAndValueArray.length + 1 : 2; 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, txOutputs, 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; } if (isOneToMany) { // for one to many mode, add the multiple outputs from the array let arrayLength = destinationAddressAndValueArray.length; for (let i = 0; i < arrayLength; i++) { // add each send tx from the array as an output let outputAddress = destinationAddressAndValueArray[i].split(',')[0]; let outputValue = new BigNumber( destinationAddressAndValueArray[i].split(',')[1], ); transactionBuilder.addOutput( BCH.Address.toCashAddress(outputAddress), parseInt(toSmallestDenomination(outputValue)), ); } } else { // for one to one mode, add output w/ single 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, sendXec, sendToken, createToken, getTokenStats, handleEncryptedOpReturn, getRecipientPublicKey, burnEtoken, }; }
{isOneToManyXECSend ? `are you sure you want to send the following One to Many transaction? ${formData.address}` : `Are you sure you want to send ${formData.value}${' '} ${currency.ticker} to ${formData.address}?`}