diff --git a/web/cashtab/src/hooks/useWallet.js b/web/cashtab/src/hooks/useWallet.js index 4567f7bf1..02638063c 100644 --- a/web/cashtab/src/hooks/useWallet.js +++ b/web/cashtab/src/hooks/useWallet.js @@ -1,1490 +1,1489 @@ import { useState, useEffect } from 'react'; import usePrevious from 'hooks/usePrevious'; import useInterval from './useInterval'; import useBCH from 'hooks/useBCH'; import BigNumber from 'bignumber.js'; import Bitcoin from '@psf/bitcoincashjs-lib'; import coininfo from 'utils/coininfo'; import { loadStoredWallet, isValidStoredWallet, isLegacyMigrationRequired, getHashArrayFromWallet, isActiveWebsocket, getWalletBalanceFromUtxos, toHash160, } from 'utils/cashMethods'; import { isValidCashtabSettings, isValidCashtabCache, isValidContactList, parseInvalidSettingsForMigration, } from 'utils/validation'; import localforage from 'localforage'; import { currency } from 'components/Common/Ticker'; import { xecReceivedNotification, xecReceivedNotificationWebsocket, eTokenReceivedNotification, } from 'components/Common/Notifications'; import { getUtxosChronik, organizeUtxosByType, getPreliminaryTokensArray, finalizeTokensArray, finalizeSlpUtxos, getTxHistoryChronik, parseChronikTx, } from 'utils/chronik'; import { ChronikClient } from 'chronik-client'; // For XEC, eCash chain: const chronik = new ChronikClient(currency.chronikUrl); import cashaddr from 'ecashaddrjs'; import * as bip39 from 'bip39'; import * as randomBytes from 'randombytes'; const useWallet = () => { const [walletRefreshInterval, setWalletRefreshInterval] = useState( currency.websocketDisconnectedRefreshInterval, ); const [wallet, setWallet] = useState(false); const [chronikWebsocket, setChronikWebsocket] = useState(null); const [contactList, setContactList] = useState([{}]); const [cashtabSettings, setCashtabSettings] = useState(false); const [cashtabCache, setCashtabCache] = useState( currency.defaultCashtabCache, ); const [fiatPrice, setFiatPrice] = useState(null); const [apiError, setApiError] = useState(false); const [checkFiatInterval, setCheckFiatInterval] = useState(null); const [hasUpdated, setHasUpdated] = useState(false); const { getBCH } = useBCH(); const [loading, setLoading] = useState(true); const [apiIndex, setApiIndex] = useState(0); const [BCH, setBCH] = useState(getBCH(apiIndex)); const { balances, tokens } = isValidStoredWallet(wallet) ? wallet.state : { balances: {}, tokens: [], utxos: null, }; const previousBalances = usePrevious(balances); const previousTokens = usePrevious(tokens); // If you catch API errors, call this function const tryNextAPI = () => { let currentApiIndex = apiIndex; // How many APIs do you have? const apiString = process.env.REACT_APP_BCHA_APIS; const apiArray = apiString.split(','); console.log(`You have ${apiArray.length} APIs to choose from`); console.log(`Current selection: ${apiIndex}`); // If only one, exit if (apiArray.length === 0) { console.log( `There are no backup APIs, you are stuck with this error`, ); return; } else if (currentApiIndex < apiArray.length - 1) { currentApiIndex += 1; console.log( `Incrementing API index from ${apiIndex} to ${currentApiIndex}`, ); } else { // Otherwise use the first option again console.log(`Retrying first API index`); currentApiIndex = 0; } //return setApiIndex(currentApiIndex); console.log(`Setting Api Index to ${currentApiIndex}`); setApiIndex(currentApiIndex); return setBCH(getBCH(currentApiIndex)); // If you have more than one, use the next one // If you are at the "end" of the array, use the first one }; const deriveAccount = async (BCH, { masterHDNode, path }) => { const node = masterHDNode.derivePath(path); const publicKey = node.getPublicKeyBuffer().toString('hex'); const cashAddress = cashaddr.encode( 'bitcoincash', 'P2PKH', node.getIdentifier(), ); const hash160 = toHash160(cashAddress); const slpAddress = BCH.SLP.Address.toSLPAddress(cashAddress); return { publicKey, hash160, cashAddress, slpAddress, fundingWif: node.keyPair.toWIF(), fundingAddress: BCH.SLP.Address.toSLPAddress(cashAddress), legacyAddress: BCH.SLP.Address.toLegacyAddress(cashAddress), }; }; const loadWalletFromStorageOnStartup = async setWallet => { // get wallet object from localforage const wallet = await getWallet(); // If wallet object in storage is valid, use it to set state on startup if (isValidStoredWallet(wallet)) { // Convert all the token balance figures to big numbers const liveWalletState = loadStoredWallet(wallet.state); wallet.state = liveWalletState; setWallet(wallet); return setLoading(false); } console.log(`Active wallet is not valid, loading params from API`); // Loading will remain true until API calls populate this legacy wallet setWallet(wallet); }; const update = async ({ wallet }) => { if (wallet && wallet.name) { console.log(`update loop called on ${wallet.name}`); } //console.log(`tick()`); //console.time("update"); // Check if walletRefreshInterval is set to 10, i.e. this was called by websocket tx detection // If walletRefreshInterval is 10, set it back to the usual refresh rate if (walletRefreshInterval === 10) { setWalletRefreshInterval( currency.websocketConnectedRefreshInterval, ); } try { if (!wallet) { return; } /* This strange data structure is necessary because chronik requires the hash160 of an address to tell you what utxos are at that address */ const hash160AndAddressObjArray = [ { address: wallet.Path145.cashAddress, hash160: wallet.Path145.hash160, }, { address: wallet.Path245.cashAddress, hash160: wallet.Path245.hash160, }, { address: wallet.Path1899.cashAddress, hash160: wallet.Path1899.hash160, }, ]; // Check that server is live try { await BCH.Blockchain.getBlockCount(); } catch (err) { console.log( `Error in BCH.Blockchain.getBlockCount, the full node is likely down`, err, ); throw new Error(`Node unavailable`); } const chronikUtxos = await getUtxosChronik( chronik, hash160AndAddressObjArray, ); const { preliminarySlpUtxos, nonSlpUtxos } = organizeUtxosByType(chronikUtxos); const preliminaryTokensArray = getPreliminaryTokensArray(preliminarySlpUtxos); const { finalTokenArray, updatedTokenInfoById, newTokensToCache } = await finalizeTokensArray( chronik, preliminaryTokensArray, cashtabCache.tokenInfoById, ); // If you have more token info now, write this to local storage if (newTokensToCache) { writeTokenInfoByIdToCache(updatedTokenInfoById); // Update the tokenInfoById key in cashtabCache setCashtabCache({ ...cashtabCache, tokenInfoById: updatedTokenInfoById, }); } const finalizedSlpUtxos = finalizeSlpUtxos( preliminarySlpUtxos, updatedTokenInfoById, ); const { chronikTxHistory, txHistoryUpdatedTokenInfoById, txHistoryNewTokensToCache, } = await getTxHistoryChronik( chronik, BCH, wallet, updatedTokenInfoById, ); if (txHistoryNewTokensToCache) { console.log( `Uncached token info found in tx history, adding to cache`, ); writeTokenInfoByIdToCache(txHistoryUpdatedTokenInfoById); // Update the tokenInfoById key in cashtabCache setCashtabCache({ ...cashtabCache, tokenInfoById: txHistoryUpdatedTokenInfoById, }); } // If you were missing any token info for tokens in this tx history, get it const newState = { balances: getWalletBalanceFromUtxos(nonSlpUtxos), tokens: finalTokenArray, slpBalancesAndUtxos: { slpUtxos: finalizedSlpUtxos, nonSlpUtxos, }, parsedTxHistory: chronikTxHistory, - utxos: chronikUtxos, }; const walletStateAfterThisStack = { balances: getWalletBalanceFromUtxos(nonSlpUtxos), slpUtxos: finalizedSlpUtxos, nonSlpUtxos, tokens: finalTokenArray, parsedTxHistory: chronikTxHistory, }; console.log(`walletStateAfterThisStack`, walletStateAfterThisStack); // Set wallet with new state field wallet.state = newState; setWallet(wallet); // Write this state to indexedDb using localForage writeWalletState(wallet, newState); // If everything executed correctly, remove apiError setApiError(false); } catch (error) { console.log(`Error in update({wallet})`); console.log(error); // Set this in state so that transactions are disabled until the issue is resolved setApiError(true); //console.timeEnd("update"); // Try another endpoint console.log(`Trying next API...`); tryNextAPI(); } //console.timeEnd("update"); }; const getActiveWalletFromLocalForage = async () => { let wallet; try { wallet = await localforage.getItem('wallet'); } catch (err) { console.log(`Error in getActiveWalletFromLocalForage`, err); wallet = null; } return wallet; }; const getContactListFromLocalForage = async () => { let contactListArray = []; try { contactListArray = await localforage.getItem('contactList'); } catch (err) { console.log('Error in getContactListFromLocalForage', err); contactListArray = null; } return contactListArray; }; const updateContactList = async contactListArray => { let updateSuccess = true; try { await localforage.setItem('contactList', contactListArray); setContactList(contactListArray); } catch (err) { console.log('Error in updateContactList', err); updateSuccess = false; } return updateSuccess; }; const getWallet = async () => { let wallet; let existingWallet; try { existingWallet = await getActiveWalletFromLocalForage(); // existing wallet will be // 1 - the 'wallet' value from localForage, if it exists // 2 - false if it does not exist in localForage // 3 - null if error // If the wallet does not have Path1899, add it // or each Path1899, Path145, Path245 does not have a public key, add them if (existingWallet) { if (isLegacyMigrationRequired(existingWallet)) { console.log( `Wallet does not have Path1899 or does not have public key`, ); existingWallet = await migrateLegacyWallet( BCH, existingWallet, ); } } // If not in localforage then existingWallet = false, check localstorage if (!existingWallet) { console.log(`no existing wallet, checking local storage`); existingWallet = JSON.parse( window.localStorage.getItem('wallet'), ); console.log(`existingWallet from localStorage`, existingWallet); // If you find it here, move it to indexedDb if (existingWallet !== null) { wallet = await getWalletDetails(existingWallet); await localforage.setItem('wallet', wallet); return wallet; } } } catch (err) { console.log(`Error in getWallet()`, err); /* Error here implies problem interacting with localForage or localStorage API Have not seen this error in testing In this case, you still want to return 'wallet' using the logic below based on the determination of 'existingWallet' from the logic above */ } if (existingWallet === null || !existingWallet) { wallet = await getWalletDetails(existingWallet); await localforage.setItem('wallet', wallet); } else { wallet = existingWallet; } return wallet; }; const migrateLegacyWallet = async (BCH, wallet) => { console.log(`migrateLegacyWallet`); console.log(`legacyWallet`, wallet); const mnemonic = wallet.mnemonic; const rootSeedBuffer = await bip39.mnemonicToSeed(mnemonic, ''); const masterHDNode = Bitcoin.HDNode.fromSeedBuffer( rootSeedBuffer, coininfo.bitcoincash.main.toBitcoinJS(), ); const Path245 = await deriveAccount(BCH, { masterHDNode, path: "m/44'/245'/0'/0/0", }); const Path145 = await deriveAccount(BCH, { masterHDNode, path: "m/44'/145'/0'/0/0", }); const Path1899 = await deriveAccount(BCH, { masterHDNode, path: "m/44'/1899'/0'/0/0", }); wallet.Path245 = Path245; wallet.Path145 = Path145; wallet.Path1899 = Path1899; try { await localforage.setItem('wallet', wallet); } catch (err) { console.log( `Error setting wallet to wallet indexedDb in migrateLegacyWallet()`, ); console.log(err); } return wallet; }; const writeTokenInfoByIdToCache = async tokenInfoById => { console.log(`writeTokenInfoByIdToCache`); const cashtabCache = currency.defaultCashtabCache; cashtabCache.tokenInfoById = tokenInfoById; try { await localforage.setItem('cashtabCache', cashtabCache); console.log(`cashtabCache successfully updated`); } catch (err) { console.log(`Error in writeCashtabCache()`, err); } }; const writeWalletState = async (wallet, newState) => { // Add new state as an object on the active wallet wallet.state = newState; try { await localforage.setItem('wallet', wallet); console.log( `Wallet ${wallet.name} saved without duplicate token object`, ); } catch (err) { console.log(`Error in writeWalletState()`); console.log(err); } }; const getWalletDetails = async wallet => { if (!wallet) { return false; } // Since this info is in localforage now, only get the var const mnemonic = wallet.mnemonic; const rootSeedBuffer = await bip39.mnemonicToSeed(mnemonic, ''); const masterHDNode = Bitcoin.HDNode.fromSeedBuffer( rootSeedBuffer, coininfo.bitcoincash.main.toBitcoinJS(), ); const Path245 = await deriveAccount(BCH, { masterHDNode, path: "m/44'/245'/0'/0/0", }); const Path145 = await deriveAccount(BCH, { masterHDNode, path: "m/44'/145'/0'/0/0", }); const Path1899 = await deriveAccount(BCH, { masterHDNode, path: "m/44'/1899'/0'/0/0", }); let name = Path1899.cashAddress.slice(12, 17); // Only set the name if it does not currently exist if (wallet && wallet.name) { name = wallet.name; } return { mnemonic: wallet.mnemonic, name, Path245, Path145, Path1899, }; }; const getSavedWallets = async activeWallet => { let savedWallets; try { savedWallets = await localforage.getItem('savedWallets'); if (savedWallets === null) { savedWallets = []; } } catch (err) { console.log(`Error in getSavedWallets`); console.log(err); savedWallets = []; } // Even though the active wallet is still stored in savedWallets, don't return it in this function for (let i = 0; i < savedWallets.length; i += 1) { if ( typeof activeWallet !== 'undefined' && activeWallet.name && savedWallets[i].name === activeWallet.name ) { savedWallets.splice(i, 1); } } return savedWallets; }; const activateWallet = async walletToActivate => { /* If the user is migrating from old version to this version, make sure to save the activeWallet 1 - check savedWallets for the previously active wallet 2 - If not there, add it */ console.log(`Activating wallet ${walletToActivate.name}`); setHasUpdated(false); let currentlyActiveWallet; try { //TODO this should just be a param used to call the function currentlyActiveWallet = await localforage.getItem('wallet'); console.log( `Currently active wallet is ${currentlyActiveWallet.name}`, ); } catch (err) { console.log( `Error in localforage.getItem("wallet") in activateWallet()`, ); return false; } // Get savedwallets let savedWallets; try { savedWallets = await localforage.getItem('savedWallets'); } catch (err) { console.log( `Error in localforage.getItem("savedWallets") in activateWallet()`, ); return false; } /* When a legacy user runs cashtab.com/, their active wallet will be migrated to Path1899 by the getWallet function. getWallet function also makes sure that each Path has a public key Wallets in savedWallets are migrated when they are activated, in this function Two cases to handle 1 - currentlyActiveWallet is valid but its stored keyvalue pair in savedWallets is not > Update savedWallets so this saved wallet is valid 2 - walletToActivate is not valid (because it's a legacy saved wallet) > Update walletToActivate before activation */ // Check savedWallets for currentlyActiveWallet let walletInSavedWallets = false; for (let i = 0; i < savedWallets.length; i += 1) { if (savedWallets[i].name === currentlyActiveWallet.name) { walletInSavedWallets = true; // Make sure the savedWallet entry matches the currentlyActiveWallet entry savedWallets[i] = currentlyActiveWallet; console.log( `Updating savedWallet ${savedWallets[i].name} to match state as currentlyActiveWallet ${currentlyActiveWallet.name}`, ); } } // resave savedWallets try { // Set walletName as the active wallet console.log(`Saving updated savedWallets`); await localforage.setItem('savedWallets', savedWallets); } catch (err) { console.log( `Error in localforage.setItem("savedWallets") in activateWallet() for unmigrated wallet`, ); } if (!walletInSavedWallets) { console.log(`Wallet is not in saved Wallets, adding`); savedWallets.push(currentlyActiveWallet); // resave savedWallets try { // Set walletName as the active wallet await localforage.setItem('savedWallets', savedWallets); } catch (err) { console.log( `Error in localforage.setItem("savedWallets") in activateWallet()`, ); } } // If wallet does not have Path1899, add it // or each of the Path1899, Path145, Path245 does not have a public key, add them // by calling migrateLagacyWallet() if (isLegacyMigrationRequired(walletToActivate)) { // Case 2, described above console.log( `Case 2: Wallet to activate is not in the most up to date Cashtab format`, ); console.log(`walletToActivate`, walletToActivate); walletToActivate = await migrateLegacyWallet(BCH, walletToActivate); } else { // Otherwise activate it as normal // Now that we have verified the last wallet was saved, we can activate the new wallet try { await localforage.setItem('wallet', walletToActivate); } catch (err) { console.log( `Error in localforage.setItem("wallet", walletToActivate) in activateWallet()`, ); return false; } } // Convert all the token balance figures to big numbers // localforage does not preserve BigNumber type; loadStoredWallet restores BigNumber type const liveWalletState = loadStoredWallet(walletToActivate.state); walletToActivate.state = liveWalletState; console.log(`Returning walletToActivate ${walletToActivate.name}`); return walletToActivate; }; const renameSavedWallet = async (oldName, newName) => { // Load savedWallets let savedWallets; try { savedWallets = await localforage.getItem('savedWallets'); } catch (err) { console.log( `Error in await localforage.getItem("savedWallets") in renameSavedWallet`, ); console.log(err); return false; } // Verify that no existing wallet has this name for (let i = 0; i < savedWallets.length; i += 1) { if (savedWallets[i].name === newName) { // return an error return false; } } // change name of desired wallet for (let i = 0; i < savedWallets.length; i += 1) { if (savedWallets[i].name === oldName) { // Replace the name of this entry with the new name savedWallets[i].name = newName; } } // resave savedWallets try { // Set walletName as the active wallet await localforage.setItem('savedWallets', savedWallets); } catch (err) { console.log( `Error in localforage.setItem("savedWallets", savedWallets) in renameSavedWallet()`, ); return false; } return true; }; const renameActiveWallet = async (wallet, oldName, newName) => { // Load savedWallets let savedWallets; try { savedWallets = await localforage.getItem('savedWallets'); } catch (err) { console.log( `Error in await localforage.getItem("savedWallets") in renameSavedWallet`, ); console.log(err); return false; } // Verify that no existing wallet has this name for (let i = 0; i < savedWallets.length; i += 1) { if (savedWallets[i].name === newName) { // return an error return false; } } if (wallet.name === oldName) { wallet.name = newName; setWallet(wallet); } // change name of desired wallet for (let i = 0; i < savedWallets.length; i += 1) { if (savedWallets[i].name === oldName) { // Replace the name of this entry with the new name savedWallets[i].name = newName; } } // resave savedWallets try { // Set walletName as the active wallet await localforage.setItem('savedWallets', savedWallets); await localforage.setItem('wallet', wallet); } catch (err) { console.log( `Error in localforage.setItem("wallet", wallet) in renameActiveWallet()`, ); return false; } return true; }; const deleteWallet = async walletToBeDeleted => { // delete a wallet // returns true if wallet is successfully deleted // otherwise returns false // Load savedWallets let savedWallets; try { savedWallets = await localforage.getItem('savedWallets'); } catch (err) { console.log( `Error in await localforage.getItem("savedWallets") in deleteWallet`, ); console.log(err); return false; } // Iterate over to find the wallet to be deleted // Verify that no existing wallet has this name let walletFoundAndRemoved = false; for (let i = 0; i < savedWallets.length; i += 1) { if (savedWallets[i].name === walletToBeDeleted.name) { // Verify it has the same mnemonic too, that's a better UUID if (savedWallets[i].mnemonic === walletToBeDeleted.mnemonic) { // Delete it savedWallets.splice(i, 1); walletFoundAndRemoved = true; } } } // If you don't find the wallet, return false if (!walletFoundAndRemoved) { return false; } // Resave savedWallets less the deleted wallet try { // Set walletName as the active wallet await localforage.setItem('savedWallets', savedWallets); } catch (err) { console.log( `Error in localforage.setItem("savedWallets", savedWallets) in deleteWallet()`, ); return false; } return true; }; const addNewSavedWallet = async importMnemonic => { // Add a new wallet to savedWallets from importMnemonic or just new wallet const lang = 'english'; // create 128 bit BIP39 mnemonic const Bip39128BitMnemonic = importMnemonic ? importMnemonic : bip39.generateMnemonic(128, randomBytes, bip39.wordlists[lang]); const newSavedWallet = await getWalletDetails({ mnemonic: Bip39128BitMnemonic.toString(), }); // Get saved wallets let savedWallets; try { savedWallets = await localforage.getItem('savedWallets'); // If this doesn't exist yet, savedWallets === null if (savedWallets === null) { savedWallets = []; } } catch (err) { console.log( `Error in savedWallets = await localforage.getItem("savedWallets") in addNewSavedWallet()`, ); console.log(err); console.log(`savedWallets in error state`, savedWallets); } // If this wallet is from an imported mnemonic, make sure it does not already exist in savedWallets if (importMnemonic) { for (let i = 0; i < savedWallets.length; i += 1) { // Check for condition "importing new wallet that is already in savedWallets" if (savedWallets[i].mnemonic === importMnemonic) { // set this as the active wallet to keep name history console.log( `Error: this wallet already exists in savedWallets`, ); console.log(`Wallet not being added.`); return false; } } } // add newSavedWallet savedWallets.push(newSavedWallet); // update savedWallets try { await localforage.setItem('savedWallets', savedWallets); } catch (err) { console.log( `Error in localforage.setItem("savedWallets", activeWallet) called in createWallet with ${importMnemonic}`, ); console.log(`savedWallets`, savedWallets); console.log(err); } return true; }; const createWallet = async importMnemonic => { const lang = 'english'; // create 128 bit BIP39 mnemonic const Bip39128BitMnemonic = importMnemonic ? importMnemonic : bip39.generateMnemonic(128, randomBytes, bip39.wordlists[lang]); const wallet = await getWalletDetails({ mnemonic: Bip39128BitMnemonic.toString(), }); try { await localforage.setItem('wallet', wallet); } catch (err) { console.log( `Error setting wallet to wallet indexedDb in createWallet()`, ); console.log(err); } // Since this function is only called from OnBoarding.js, also add this to the saved wallet try { await localforage.setItem('savedWallets', [wallet]); } catch (err) { console.log( `Error setting wallet to savedWallets indexedDb in createWallet()`, ); console.log(err); } return wallet; }; // Parse chronik ws message for incoming tx notifications const processChronikWsMsg = async (msg, wallet, fiatPrice) => { // get the message type const { type } = msg; // For now, only act on "first seen" transactions, as the only logic to happen is first seen notifications // Dev note: Other chronik msg types // "BlockConnected", arrives as new blocks are found // "Confirmed", arrives as subscribed + seen txid is confirmed in a block if (type !== 'AddedToMempool') { return; } // If you see a tx from your subscribed addresses added to the mempool, then the wallet utxo set has changed // Update it setWalletRefreshInterval(10); // get txid info const txid = msg.txid; let incomingTxDetails; try { incomingTxDetails = await chronik.tx(txid); } catch (err) { // In this case, no notification return console.log( `Error in chronik.tx(${txid} while processing an incoming websocket tx`, err, ); } // Get tokenInfoById from cashtabCache to parse this tx let tokenInfoById = {}; try { tokenInfoById = cashtabCache.tokenInfoById; } catch (err) { console.log( `Error getting tokenInfoById from cache on incoming tx`, err, ); } // parse tx for notification const parsedChronikTx = parseChronikTx( BCH, incomingTxDetails, wallet, tokenInfoById, ); /* If this is an incoming eToken tx and parseChronikTx was not able to get genesis info from cache, then get genesis info from API and add to cache */ if (parsedChronikTx.incoming) { if (parsedChronikTx.isEtokenTx) { let eTokenAmountReceived = parsedChronikTx.etokenAmount; if (parsedChronikTx.genesisInfo.success) { // Send this info to the notification function eTokenReceivedNotification( currency, parsedChronikTx.genesisInfo.tokenTicker, eTokenAmountReceived, parsedChronikTx.genesisInfo.tokenName, ); } else { // Get genesis info from API and add to cache try { // Get the tokenID const incomingTokenId = parsedChronikTx.slpMeta.tokenId; // chronik call to genesis tx to get this info const tokenGenesisInfo = await chronik.tx( incomingTokenId, ); const { genesisInfo } = tokenGenesisInfo.slpTxData; // Add this to cashtabCache let tokenInfoByIdUpdatedForThisToken = tokenInfoById; tokenInfoByIdUpdatedForThisToken[incomingTokenId] = genesisInfo; writeTokenInfoByIdToCache( tokenInfoByIdUpdatedForThisToken, ); // Update the tokenInfoById key in cashtabCache setCashtabCache({ ...cashtabCache, tokenInfoById: tokenInfoByIdUpdatedForThisToken, }); // Calculate eToken amount with decimals eTokenAmountReceived = new BigNumber( parsedChronikTx.etokenAmount, ).shiftedBy(-1 * genesisInfo.decimals); // Send this info to the notification function eTokenReceivedNotification( currency, genesisInfo.tokenTicker, eTokenAmountReceived, genesisInfo.tokenName, ); } catch (err) { console.log( `Error in getting and setting new token info for incoming eToken tx`, err, ); } } } else { xecReceivedNotificationWebsocket( parsedChronikTx.xecAmount, cashtabSettings, fiatPrice, ); } } }; // Chronik websockets const initializeWebsocket = async (wallet, fiatPrice) => { console.log( `Initializing websocket connection for wallet ${wallet.name}`, ); // Because wallet is set to `false` before it is loaded, do nothing if you find this case // Also return and wait for legacy migration if wallet is not migrated const hash160Array = getHashArrayFromWallet(wallet); if (!wallet || !hash160Array) { return setChronikWebsocket(null); } // Initialize if not in state let ws = chronikWebsocket; if (ws === null) { ws = chronik.ws({ onMessage: msg => { processChronikWsMsg(msg, wallet, fiatPrice); }, onReconnect: e => { // Fired before a reconnect attempt is made: console.log( 'Reconnecting websocket, disconnection cause: ', e, ); }, onConnect: e => { console.log(`Chronik websocket connected`, e); console.log( `Websocket connected, adjusting wallet refresh interval to ${ currency.websocketConnectedRefreshInterval / 1000 }s`, ); setWalletRefreshInterval( currency.websocketConnectedRefreshInterval, ); }, }); // Wait for websocket to be connected: await ws.waitForOpen(); } else { /* If the websocket connection is not null, initializeWebsocket was called because one of the websocket's dependencies changed Update the onMessage method to get the latest dependencies (wallet, fiatPrice) */ ws.onMessage = msg => { processChronikWsMsg(msg, wallet, fiatPrice); }; } // Check if current subscriptions match current wallet let activeSubscriptionsMatchActiveWallet = true; const previousWebsocketSubscriptions = ws._subs; // If there are no previous subscriptions, then activeSubscriptionsMatchActiveWallet is certainly false if (previousWebsocketSubscriptions.length === 0) { activeSubscriptionsMatchActiveWallet = false; } else { const subscribedHash160Array = previousWebsocketSubscriptions.map( function (subscription) { return subscription.scriptPayload; }, ); // Confirm that websocket is subscribed to every address in wallet hash160Array for (let i = 0; i < hash160Array.length; i += 1) { if (!subscribedHash160Array.includes(hash160Array[i])) { activeSubscriptionsMatchActiveWallet = false; } } } // If you are already subscribed to the right addresses, exit here // You get to this situation if fiatPrice changed but wallet.mnemonic did not if (activeSubscriptionsMatchActiveWallet) { // Put connected websocket in state return setChronikWebsocket(ws); } // Unsubscribe to any active subscriptions console.log( `previousWebsocketSubscriptions`, previousWebsocketSubscriptions, ); if (previousWebsocketSubscriptions.length > 0) { for (let i = 0; i < previousWebsocketSubscriptions.length; i += 1) { const unsubHash160 = previousWebsocketSubscriptions[i].scriptPayload; ws.unsubscribe('p2pkh', unsubHash160); console.log(`ws.unsubscribe('p2pkh', ${unsubHash160})`); } } // Subscribe to addresses of current wallet for (let i = 0; i < hash160Array.length; i += 1) { ws.subscribe('p2pkh', hash160Array[i]); console.log(`ws.subscribe('p2pkh', ${hash160Array[i]})`); } // Put connected websocket in state return setChronikWebsocket(ws); }; const handleUpdateWallet = async setWallet => { await loadWalletFromStorageOnStartup(setWallet); }; const loadCashtabSettings = async () => { // get settings object from localforage let localSettings; try { localSettings = await localforage.getItem('settings'); // If there is no keyvalue pair in localforage with key 'settings' if (localSettings === null) { // Create one with the default settings from Ticker.js localforage.setItem('settings', currency.defaultSettings); // Set state to default settings setCashtabSettings(currency.defaultSettings); return currency.defaultSettings; } } catch (err) { console.log(`Error getting cashtabSettings`, err); // TODO If they do not exist, write them // TODO add function to change them setCashtabSettings(currency.defaultSettings); return currency.defaultSettings; } // If you found an object in localforage at the settings key, make sure it's valid if (isValidCashtabSettings(localSettings)) { setCashtabSettings(localSettings); return localSettings; } // If a settings object is present but invalid, parse to find and add missing keys let modifiedLocalSettings = parseInvalidSettingsForMigration(localSettings); if (isValidCashtabSettings(modifiedLocalSettings)) { // modifiedLocalSettings placed in local storage localforage.setItem('settings', modifiedLocalSettings); setCashtabSettings(modifiedLocalSettings); // update missing key in local storage without overwriting existing valid settings return modifiedLocalSettings; } else { // if not valid, also set cashtabSettings to default setCashtabSettings(currency.defaultSettings); // Since this is returning default settings based on an error from reading storage, do not overwrite whatever is in storage return currency.defaultSettings; } }; const loadContactList = async () => { // get contactList object from localforage let localContactList; try { localContactList = await localforage.getItem('contactList'); // If there is no keyvalue pair in localforage with key 'contactList' if (localContactList === null) { // Use an array containing a single empty object localforage.setItem('contactList', [{}]); setContactList([{}]); return [{}]; } } catch (err) { console.log(`Error getting contactList`, err); setContactList([{}]); return [{}]; } // If you found an object in localforage at the contactList key, make sure it's valid if (isValidContactList(localContactList)) { setContactList(localContactList); return localContactList; } // if not valid, also set to default setContactList([{}]); return [{}]; }; const loadCashtabCache = async () => { // get cache object from localforage let localCashtabCache; try { localCashtabCache = await localforage.getItem('cashtabCache'); // If there is no keyvalue pair in localforage with key 'cashtabCache' if (localCashtabCache === null) { // Use the default localforage.setItem( 'cashtabCache', currency.defaultCashtabCache, ); setCashtabCache(currency.defaultCashtabCache); return currency.defaultCashtabCache; } } catch (err) { console.log(`Error getting cashtabCache`, err); setCashtabCache(currency.defaultCashtabCache); return currency.defaultCashtabCache; } // If you found an object in localforage at the cashtabCache key, make sure it's valid if (isValidCashtabCache(localCashtabCache)) { setCashtabCache(localCashtabCache); return localCashtabCache; } // if not valid, also set to default setCashtabCache(currency.defaultCashtabCache); return currency.defaultCashtabCache; }; // With different currency selections possible, need unique intervals for price checks // Must be able to end them and set new ones with new currencies const initializeFiatPriceApi = async selectedFiatCurrency => { // Update fiat price and confirm it is set to make sure ap keeps loading state until this is updated await fetchBchPrice(selectedFiatCurrency); // Set interval for updating the price with given currency const thisFiatInterval = setInterval(function () { fetchBchPrice(selectedFiatCurrency); }, 60000); // set interval in state setCheckFiatInterval(thisFiatInterval); }; const clearFiatPriceApi = fiatPriceApi => { // Clear fiat price check interval of previously selected currency clearInterval(fiatPriceApi); }; const changeCashtabSettings = async (key, newValue) => { // Set loading to true as you do not want to display the fiat price of the last currency // loading = true will lock the UI until the fiat price has updated if (key !== 'balanceVisible') { setLoading(true); } // Get settings from localforage let currentSettings; let newSettings; try { currentSettings = await localforage.getItem('settings'); } catch (err) { console.log(`Error in changeCashtabSettings`, err); // Set fiat price to null, which disables fiat sends throughout the app setFiatPrice(null); // Unlock the UI setLoading(false); return; } // Make sure function was called with valid params if (currency.settingsValidation[key].includes(newValue)) { // Update settings newSettings = currentSettings; newSettings[key] = newValue; } else { // Set fiat price to null, which disables fiat sends throughout the app setFiatPrice(null); // Unlock the UI setLoading(false); return; } // Set new settings in state so they are available in context throughout the app setCashtabSettings(newSettings); // If this settings change adjusted the fiat currency, update fiat price if (key === 'fiatCurrency') { clearFiatPriceApi(checkFiatInterval); initializeFiatPriceApi(newValue); } // Write new settings in localforage try { await localforage.setItem('settings', newSettings); } catch (err) { console.log( `Error writing newSettings object to localforage in changeCashtabSettings`, err, ); console.log(`newSettings`, newSettings); // do nothing. If this happens, the user will see default currency next time they load the app. } setLoading(false); }; // Parse for incoming XEC transactions // hasUpdated is set to true in the useInterval function, and re-sets to false during activateWallet // Do not show this notification if websocket connection is live; in this case the websocket will handle it if ( !isActiveWebsocket(chronikWebsocket) && previousBalances && balances && 'totalBalance' in previousBalances && 'totalBalance' in balances && new BigNumber(balances.totalBalance) .minus(previousBalances.totalBalance) .gt(0) && hasUpdated ) { xecReceivedNotification( balances, previousBalances, cashtabSettings, fiatPrice, ); } // Parse for incoming eToken transactions // Do not show this notification if websocket connection is live; in this case the websocket will handle it if ( !isActiveWebsocket(chronikWebsocket) && tokens && tokens[0] && tokens[0].balance && previousTokens && previousTokens[0] && previousTokens[0].balance && hasUpdated === true ) { // If tokens length is greater than previousTokens length, a new token has been received // Note, a user could receive a new token, AND more of existing tokens in between app updates // In this case, the app will only notify about the new token // TODO better handling for all possible cases to cover this // TODO handle with websockets for better response time, less complicated calc if (tokens.length > previousTokens.length) { // Find the new token const tokenIds = tokens.map(({ tokenId }) => tokenId); const previousTokenIds = previousTokens.map( ({ tokenId }) => tokenId, ); //console.log(`tokenIds`, tokenIds); //console.log(`previousTokenIds`, previousTokenIds); // An array with the new token Id const newTokenIdArr = tokenIds.filter( tokenId => !previousTokenIds.includes(tokenId), ); // It's possible that 2 new tokens were received // To do, handle this case const newTokenId = newTokenIdArr[0]; //console.log(newTokenId); // How much of this tokenId did you get? // would be at // Find where the newTokenId is const receivedTokenObjectIndex = tokens.findIndex( x => x.tokenId === newTokenId, ); //console.log(`receivedTokenObjectIndex`, receivedTokenObjectIndex); // Calculate amount received //console.log(`receivedTokenObject:`, tokens[receivedTokenObjectIndex]); const receivedSlpQty = tokens[receivedTokenObjectIndex].balance.toString(); const receivedSlpTicker = tokens[receivedTokenObjectIndex].info.tokenTicker; const receivedSlpName = tokens[receivedTokenObjectIndex].info.tokenName; //console.log(`receivedSlpQty`, receivedSlpQty); // Notification if you received SLP if (receivedSlpQty > 0) { eTokenReceivedNotification( currency, receivedSlpTicker, receivedSlpQty, receivedSlpName, ); } // } else { // If tokens[i].balance > previousTokens[i].balance, a new SLP tx of an existing token has been received // Note that tokens[i].balance is of type BigNumber for (let i = 0; i < tokens.length; i += 1) { if (tokens[i].balance.gt(previousTokens[i].balance)) { // Received this token // console.log(`previousTokenId`, previousTokens[i].tokenId); // console.log(`currentTokenId`, tokens[i].tokenId); if (previousTokens[i].tokenId !== tokens[i].tokenId) { console.log( `TokenIds do not match, breaking from SLP notifications`, ); // Then don't send the notification // Also don't 'continue' ; this means you have sent a token, just stop iterating through break; } const receivedSlpQty = tokens[i].balance.minus( previousTokens[i].balance, ); const receivedSlpTicker = tokens[i].info.tokenTicker; const receivedSlpName = tokens[i].info.tokenName; eTokenReceivedNotification( currency, receivedSlpTicker, receivedSlpQty, receivedSlpName, ); } } } } // Update wallet according to defined interval useInterval(async () => { const wallet = await getWallet(); update({ wallet, }).finally(() => { setLoading(false); if (!hasUpdated) { setHasUpdated(true); } }); }, walletRefreshInterval); const fetchBchPrice = async ( fiatCode = cashtabSettings ? cashtabSettings.fiatCurrency : 'usd', ) => { // Split this variable out in case coingecko changes const cryptoId = currency.coingeckoId; // Keep this in the code, because different URLs will have different outputs require different parsing const priceApiUrl = `https://api.coingecko.com/api/v3/simple/price?ids=${cryptoId}&vs_currencies=${fiatCode}&include_last_updated_at=true`; let bchPrice; let bchPriceJson; try { bchPrice = await fetch(priceApiUrl); //console.log(`bchPrice`, bchPrice); } catch (err) { console.log(`Error fetching BCH Price`); console.log(err); } try { bchPriceJson = await bchPrice.json(); //console.log(`bchPriceJson`, bchPriceJson); let bchPriceInFiat = bchPriceJson[cryptoId][fiatCode]; const validEcashPrice = typeof bchPriceInFiat === 'number'; if (validEcashPrice) { setFiatPrice(bchPriceInFiat); } else { // If API price looks fishy, do not allow app to send using fiat settings setFiatPrice(null); } } catch (err) { console.log(`Error parsing price API response to JSON`); console.log(err); } }; useEffect(async () => { handleUpdateWallet(setWallet); await loadContactList(); await loadCashtabCache(); const initialSettings = await loadCashtabSettings(); initializeFiatPriceApi(initialSettings.fiatCurrency); }, []); /* Run initializeWebsocket(wallet, fiatPrice) each time the wallet or fiatPrice changes Use wallet.mnemonic as the useEffect parameter here because we want to run initializeWebsocket(wallet, fiatPrice) when a new unique wallet is selected, not when the active wallet changes state */ useEffect(async () => { await initializeWebsocket(wallet, fiatPrice); }, [wallet.mnemonic, fiatPrice]); return { BCH, chronik, wallet, fiatPrice, loading, apiError, contactList, cashtabSettings, cashtabCache, changeCashtabSettings, getActiveWalletFromLocalForage, getWallet, getWalletDetails, getSavedWallets, migrateLegacyWallet, getContactListFromLocalForage, updateContactList, createWallet: async importMnemonic => { setLoading(true); const newWallet = await createWallet(importMnemonic); setWallet(newWallet); update({ wallet: newWallet, }).finally(() => setLoading(false)); }, activateWallet: async walletToActivate => { setLoading(true); // Make sure that the wallet update interval is not called on the former wallet before this function completes console.log( `Suspending wallet update interval while new wallet is activated`, ); setWalletRefreshInterval( currency.websocketDisconnectedRefreshInterval, ); const newWallet = await activateWallet(walletToActivate); console.log(`activateWallet gives newWallet ${newWallet.name}`); // Changing the wallet here will cause `initializeWebsocket` to fire which will update the websocket interval on a successful connection setWallet(newWallet); // Immediately call update on this wallet to populate it in the latest format // Use the instant interval of 10ms that the update function will cancel setWalletRefreshInterval(10); setLoading(false); }, addNewSavedWallet, renameSavedWallet, renameActiveWallet, deleteWallet, }; }; export default useWallet; diff --git a/web/cashtab/src/utils/__mocks__/mockStoredWallets.js b/web/cashtab/src/utils/__mocks__/mockStoredWallets.js index e7e0027d9..e5ec19a31 100644 --- a/web/cashtab/src/utils/__mocks__/mockStoredWallets.js +++ b/web/cashtab/src/utils/__mocks__/mockStoredWallets.js @@ -1,3358 +1,3774 @@ export const validStoredWallet = { mnemonic: 'Nope', name: 'TripDos', Path245: { cashAddress: 'bitcoincash:qq0mw6nah9huwaxt45qw3fegjpszkjlrqsvttwy36p', slpAddress: 'simpleledger:qq0mw6nah9huwaxt45qw3fegjpszkjlrqsqsq433yl', fundingWif: 'Nope', fundingAddress: 'simpleledger:qq0mw6nah9huwaxt45qw3fegjpszkjlrqsqsq433yl', legacyAddress: '13thfuvhCA1dGE7nVgyU61BZfoD8ApXJsg', }, Path145: { cashAddress: 'bitcoincash:qz5lf9pxde9neq3hzte8mmwts03sktl9nuz6m3dynu', slpAddress: 'simpleledger:qz5lf9pxde9neq3hzte8mmwts03sktl9nuwps2cydz', fundingWif: 'Nope', fundingAddress: 'simpleledger:qz5lf9pxde9neq3hzte8mmwts03sktl9nuwps2cydz', legacyAddress: '1GVeC3gB6V3EStcQbJiry5BJn4fRdHjKyc', }, Path1899: { cashAddress: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', slpAddress: 'simpleledger:qz2708636snqhsxu8wnlka78h6fdp77ar5syue64fa', fundingWif: 'Nope', fundingAddress: 'simpleledger:qz2708636snqhsxu8wnlka78h6fdp77ar5syue64fa', legacyAddress: '1Efd9z9GRVJK2r73nUpFmBnsKUmfXNm2y2', }, state: { balances: { totalBalanceInSatoshis: '1503017804', totalBalance: '15030178.04', }, tokens: [ { info: { height: 680782, tx_hash: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', tx_pos: 1, value: 546, txid: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', vout: 1, utxoType: 'token', transactionType: 'send', tokenId: 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', tokenTicker: 'ST', tokenName: 'ST', tokenDocumentUrl: 'developer.bitcoin.com', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', balance: { s: 1, e: 0, c: [1], }, hasBaton: false, }, { info: { height: 680784, tx_hash: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', tx_pos: 1, value: 546, txid: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', vout: 1, utxoType: 'token', transactionType: 'send', tokenId: 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', tokenTicker: 'CTP', tokenName: 'Cash Tab Points', tokenDocumentUrl: 'https://cashtabapp.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '1e-9', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', balance: { s: 1, e: -9, c: [100000], }, hasBaton: false, }, { info: { height: 681189, tx_hash: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', tx_pos: 2, value: 546, txid: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', tokenTicker: 'WDT', tokenName: 'Test Token With Exceptionally Long Name For CSS And Style Revisions', tokenDocumentUrl: 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', tokenDocumentHash: '����\\�IS\u001e9�����k+���\u0018���\u001b]�߷2��', decimals: 7, tokenType: 1, tokenQty: '523512277.7961432', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', balance: { s: 1, e: 8, c: [523512277, 79614320000000], }, hasBaton: false, }, { info: { height: 681190, tx_hash: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', tx_pos: 2, value: 546, txid: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', tokenTicker: '🍔', tokenName: 'Burger', tokenDocumentUrl: 'https://c4.wallpaperflare.com/wallpaper/58/564/863/giant-hamburger-wallpaper-preview.jpg', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', balance: { s: 1, e: 0, c: [1], }, hasBaton: false, }, { info: { height: 681191, tx_hash: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', tx_pos: 2, value: 546, txid: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', tokenTicker: 'NOCOVID', tokenName: 'Covid19 Lifetime Immunity', tokenDocumentUrl: 'https://www.who.int/emergencies/diseases/novel-coronavirus-2019/covid-19-vaccines', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '996797', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', balance: { s: 1, e: 5, c: [996797], }, hasBaton: false, }, { info: { height: 681191, tx_hash: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', tx_pos: 2, value: 546, txid: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', tokenTicker: 'TAP', tokenName: 'Thoughts and Prayers', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', balance: { s: 1, e: 0, c: [1], }, hasBaton: false, }, { info: { height: 681191, tx_hash: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', tx_pos: 2, value: 546, txid: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', tokenTicker: 'NAKAMOTO', tokenName: 'NAKAMOTO', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 8, tokenType: 1, tokenQty: '0.99999999', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', balance: { s: 1, e: -1, c: [99999999000000], }, hasBaton: false, }, { info: { height: 681191, tx_hash: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', tx_pos: 2, value: 546, txid: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', tokenTicker: 'HONK', tokenName: 'HONK HONK', tokenDocumentUrl: 'THE REAL HONK SLP TOKEN', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', balance: { s: 1, e: 0, c: [1], }, hasBaton: false, }, { info: { height: 681329, tx_hash: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', tx_pos: 2, value: 546, txid: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenTicker: 'XBIT', tokenName: 'eBits', tokenDocumentUrl: 'https://boomertakes.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '999997.999999994', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', balance: { s: 1, e: 5, c: [999997, 99999999400000], }, hasBaton: false, }, ], slpBalancesAndUtxos: { tokens: [ { info: { height: 680782, tx_hash: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', tx_pos: 1, value: 546, txid: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', vout: 1, utxoType: 'token', transactionType: 'send', tokenId: 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', tokenTicker: 'ST', tokenName: 'ST', tokenDocumentUrl: 'developer.bitcoin.com', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', balance: { s: 1, e: 0, c: [1], }, hasBaton: false, }, { info: { height: 680784, tx_hash: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', tx_pos: 1, value: 546, txid: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', vout: 1, utxoType: 'token', transactionType: 'send', tokenId: 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', tokenTicker: 'CTP', tokenName: 'Cash Tab Points', tokenDocumentUrl: 'https://cashtabapp.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '1e-9', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', balance: { s: 1, e: -9, c: [100000], }, hasBaton: false, }, { info: { height: 681189, tx_hash: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', tx_pos: 2, value: 546, txid: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', tokenTicker: 'WDT', tokenName: 'Test Token With Exceptionally Long Name For CSS And Style Revisions', tokenDocumentUrl: 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', tokenDocumentHash: '����\\�IS\u001e9�����k+���\u0018���\u001b]�߷2��', decimals: 7, tokenType: 1, tokenQty: '523512277.7961432', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', balance: { s: 1, e: 8, c: [523512277, 79614320000000], }, hasBaton: false, }, { info: { height: 681190, tx_hash: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', tx_pos: 2, value: 546, txid: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', tokenTicker: '🍔', tokenName: 'Burger', tokenDocumentUrl: 'https://c4.wallpaperflare.com/wallpaper/58/564/863/giant-hamburger-wallpaper-preview.jpg', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', balance: { s: 1, e: 0, c: [1], }, hasBaton: false, }, { info: { height: 681191, tx_hash: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', tx_pos: 2, value: 546, txid: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', tokenTicker: 'NOCOVID', tokenName: 'Covid19 Lifetime Immunity', tokenDocumentUrl: 'https://www.who.int/emergencies/diseases/novel-coronavirus-2019/covid-19-vaccines', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '996797', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', balance: { s: 1, e: 5, c: [996797], }, hasBaton: false, }, { info: { height: 681191, tx_hash: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', tx_pos: 2, value: 546, txid: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', tokenTicker: 'TAP', tokenName: 'Thoughts and Prayers', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', balance: { s: 1, e: 0, c: [1], }, hasBaton: false, }, { info: { height: 681191, tx_hash: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', tx_pos: 2, value: 546, txid: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', tokenTicker: 'NAKAMOTO', tokenName: 'NAKAMOTO', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 8, tokenType: 1, tokenQty: '0.99999999', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', balance: { s: 1, e: -1, c: [99999999000000], }, hasBaton: false, }, { info: { height: 681191, tx_hash: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', tx_pos: 2, value: 546, txid: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', tokenTicker: 'HONK', tokenName: 'HONK HONK', tokenDocumentUrl: 'THE REAL HONK SLP TOKEN', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', balance: { s: 1, e: 0, c: [1], }, hasBaton: false, }, { info: { height: 681329, tx_hash: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', tx_pos: 2, value: 546, txid: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenTicker: 'XBIT', tokenName: 'eBits', tokenDocumentUrl: 'https://boomertakes.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '999997.999999994', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', balance: { s: 1, e: 5, c: [999997, 99999999400000], }, hasBaton: false, }, ], nonSlpUtxos: [ { height: 682107, tx_hash: '8d4c90ecf069e3a1494339724ddbb8bf28e3b38315a009ca5c49237b3ae7687a', tx_pos: 1, value: 1503017804, txid: '8d4c90ecf069e3a1494339724ddbb8bf28e3b38315a009ca5c49237b3ae7687a', isValid: false, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', outpoint: { outIdx: 1, txid: '8d4c90ecf069e3a1494339724ddbb8bf28e3b38315a009ca5c49237b3ae7687a', }, }, ], slpUtxos: [ { height: 680782, tx_hash: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', tx_pos: 1, value: 546, txid: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', vout: 1, utxoType: 'token', transactionType: 'send', tokenTicker: 'ST', tokenName: 'ST', tokenDocumentUrl: 'developer.bitcoin.com', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', slpMeta: { tokenId: 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', }, }, { height: 680784, tx_hash: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', tx_pos: 1, value: 546, txid: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', vout: 1, utxoType: 'token', transactionType: 'send', slpMeta: { tokenId: 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', }, tokenTicker: 'CTP', tokenName: 'Cash Tab Points', tokenDocumentUrl: 'https://cashtabapp.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '1e-9', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681189, tx_hash: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', tx_pos: 2, value: 546, txid: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', vout: 2, utxoType: 'token', transactionType: 'send', slpMeta: { tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', }, tokenTicker: 'WDT', tokenName: 'Test Token With Exceptionally Long Name For CSS And Style Revisions', tokenDocumentUrl: 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', tokenDocumentHash: '����\\�IS\u001e9�����k+���\u0018���\u001b]�߷2��', decimals: 7, tokenType: 1, tokenQty: '523512277.7961432', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681190, tx_hash: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', tx_pos: 2, value: 546, txid: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', vout: 2, utxoType: 'token', transactionType: 'send', slpMeta: { tokenId: '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', }, tokenTicker: '🍔', tokenName: 'Burger', tokenDocumentUrl: 'https://c4.wallpaperflare.com/wallpaper/58/564/863/giant-hamburger-wallpaper-preview.jpg', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681191, tx_hash: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', tx_pos: 2, value: 546, txid: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', vout: 2, utxoType: 'token', transactionType: 'send', slpMeta: { tokenId: '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', }, tokenTicker: 'NOCOVID', tokenName: 'Covid19 Lifetime Immunity', tokenDocumentUrl: 'https://www.who.int/emergencies/diseases/novel-coronavirus-2019/covid-19-vaccines', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '996797', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681191, tx_hash: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', tx_pos: 2, value: 546, txid: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', vout: 2, utxoType: 'token', transactionType: 'send', slpMeta: { tokenId: 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', }, tokenTicker: 'TAP', tokenName: 'Thoughts and Prayers', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681191, tx_hash: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', tx_pos: 2, value: 546, txid: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', vout: 2, utxoType: 'token', transactionType: 'send', slpMeta: { tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', }, tokenTicker: 'NAKAMOTO', tokenName: 'NAKAMOTO', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 8, tokenType: 1, tokenQty: '0.99999999', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681191, tx_hash: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', tx_pos: 2, value: 546, txid: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', vout: 2, utxoType: 'token', transactionType: 'send', slpMeta: { tokenId: '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', }, tokenTicker: 'HONK', tokenName: 'HONK HONK', tokenDocumentUrl: 'THE REAL HONK SLP TOKEN', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681329, tx_hash: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', tx_pos: 2, value: 546, txid: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', vout: 2, utxoType: 'token', transactionType: 'send', slpMeta: { tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', }, tokenTicker: 'XBIT', tokenName: 'eBits', tokenDocumentUrl: 'https://boomertakes.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '999997.999999994', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, ], }, parsedTxHistory: [ { txid: '8d4c90ecf069e3a1494339724ddbb8bf28e3b38315a009ca5c49237b3ae7687a', confirmations: 644, height: 682107, blocktime: 1618439595, amountSent: 0.00002, amountReceived: 0, tokenTx: false, outgoingTx: true, destinationAddress: 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', }, { txid: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', confirmations: 1422, height: 681329, blocktime: 1617988189, amountSent: 0.00000546, amountReceived: 0, tokenTx: true, outgoingTx: true, destinationAddress: 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', tokenInfo: { qtySent: '1e-9', qtyReceived: '0', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenName: 'eBits', tokenTicker: 'XBIT', }, }, { txid: 'f27ff24c15b01c30d44218c6dc8706fd33cc7bc9b4b38399075f0f41d8e412af', confirmations: 1559, height: 681192, blocktime: 1617923457, amountSent: 0.00000546, amountReceived: 0, tokenTx: true, outgoingTx: true, destinationAddress: 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', tokenInfo: { qtySent: '5e-9', qtyReceived: '0', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenName: 'eBits', tokenTicker: 'XBIT', }, }, { txid: 'b7f8b23f5ce12842eb655239919b6142052a2fa2b2ce974a4baac36b0137f332', confirmations: 1559, height: 681192, blocktime: 1617923457, amountSent: 0.00000546, amountReceived: 0, tokenTx: true, outgoingTx: true, destinationAddress: 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', tokenInfo: { qtySent: '4e-9', qtyReceived: '0', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenName: 'eBits', tokenTicker: 'XBIT', }, }, { txid: '880baf5691c2b4c5a22ae4032e2004c0c54bfabf003468044a2e341846137136', confirmations: 1559, height: 681192, blocktime: 1617923457, amountSent: 0.00000546, amountReceived: 0, tokenTx: true, outgoingTx: true, destinationAddress: 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', tokenInfo: { qtySent: '3e-9', qtyReceived: '0', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenName: 'eBits', tokenTicker: 'XBIT', }, }, ], utxos: [ { utxos: [], address: 'bitcoincash:qq0mw6nah9huwaxt45qw3fegjpszkjlrqsvttwy36p', }, { utxos: [], address: 'bitcoincash:qz5lf9pxde9neq3hzte8mmwts03sktl9nuz6m3dynu', }, { utxos: [ { height: 680782, tx_hash: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', tx_pos: 1, value: 546, }, { height: 680784, tx_hash: '28f061fee068d3b9cb578141bac3d4d9ec4eccebec680464bf0aafaac414811f', tx_pos: 1, value: 546, }, { height: 680784, tx_hash: '5fa3ffccea55c968beb7d214c563c92336ce2bbccbb714ba819848a7f7060bdb', tx_pos: 1, value: 546, }, { height: 680784, tx_hash: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', tx_pos: 1, value: 546, }, { height: 681189, tx_hash: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', tx_pos: 2, value: 546, }, { height: 681190, tx_hash: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', tx_pos: 2, value: 546, }, { height: 681191, tx_hash: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', tx_pos: 2, value: 546, }, { height: 681191, tx_hash: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', tx_pos: 2, value: 546, }, { height: 681191, tx_hash: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', tx_pos: 2, value: 546, }, { height: 681191, tx_hash: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', tx_pos: 2, value: 546, }, { height: 681329, tx_hash: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', tx_pos: 2, value: 546, }, { height: 682107, tx_hash: '8d4c90ecf069e3a1494339724ddbb8bf28e3b38315a009ca5c49237b3ae7687a', tx_pos: 1, value: 1503017804, }, ], address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, ], }, }; -// Sample unmigrated (i.e. before diff adding required fields to wallet.state) wallet, with private key info removed -export const invalidStoredWallet = { - mnemonic: 'Remembered to take this out this time', +export const validStoredWalletAfter20221123Streamline = { + mnemonic: 'Nope', name: 'TripDos', Path245: { cashAddress: 'bitcoincash:qq0mw6nah9huwaxt45qw3fegjpszkjlrqsvttwy36p', slpAddress: 'simpleledger:qq0mw6nah9huwaxt45qw3fegjpszkjlrqsqsq433yl', - fundingWif: 'Remembered to take this out this time', + fundingWif: 'Nope', fundingAddress: 'simpleledger:qq0mw6nah9huwaxt45qw3fegjpszkjlrqsqsq433yl', legacyAddress: '13thfuvhCA1dGE7nVgyU61BZfoD8ApXJsg', }, Path145: { cashAddress: 'bitcoincash:qz5lf9pxde9neq3hzte8mmwts03sktl9nuz6m3dynu', slpAddress: 'simpleledger:qz5lf9pxde9neq3hzte8mmwts03sktl9nuwps2cydz', - fundingWif: 'Remembered to take this out this time', + fundingWif: 'Nope', fundingAddress: 'simpleledger:qz5lf9pxde9neq3hzte8mmwts03sktl9nuwps2cydz', legacyAddress: '1GVeC3gB6V3EStcQbJiry5BJn4fRdHjKyc', }, Path1899: { cashAddress: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', slpAddress: 'simpleledger:qz2708636snqhsxu8wnlka78h6fdp77ar5syue64fa', - fundingWif: 'Remembered to take this out this time', + fundingWif: 'Nope', fundingAddress: 'simpleledger:qz2708636snqhsxu8wnlka78h6fdp77ar5syue64fa', legacyAddress: '1Efd9z9GRVJK2r73nUpFmBnsKUmfXNm2y2', }, state: { balances: { - totalBalanceInSatoshis: 1503017804, - totalBalance: 15.03017804, + totalBalanceInSatoshis: '1503017804', + totalBalance: '15030178.04', }, tokens: [ { info: { height: 680782, tx_hash: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', tx_pos: 1, value: 546, txid: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', vout: 1, utxoType: 'token', transactionType: 'send', tokenId: 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', tokenTicker: 'ST', tokenName: 'ST', tokenDocumentUrl: 'developer.bitcoin.com', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', - balance: '1', + balance: { + s: 1, + e: 0, + c: [1], + }, hasBaton: false, }, { info: { height: 680784, tx_hash: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', tx_pos: 1, value: 546, txid: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', vout: 1, utxoType: 'token', transactionType: 'send', tokenId: 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', tokenTicker: 'CTP', tokenName: 'Cash Tab Points', tokenDocumentUrl: 'https://cashtabapp.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '1e-9', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', - balance: '1e-9', + balance: { + s: 1, + e: -9, + c: [100000], + }, hasBaton: false, }, { info: { height: 681189, tx_hash: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', tx_pos: 2, value: 546, txid: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', tokenTicker: 'WDT', tokenName: 'Test Token With Exceptionally Long Name For CSS And Style Revisions', tokenDocumentUrl: 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', tokenDocumentHash: '����\\�IS\u001e9�����k+���\u0018���\u001b]�߷2��', decimals: 7, tokenType: 1, tokenQty: '523512277.7961432', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', - balance: '523512277.7961432', + balance: { + s: 1, + e: 8, + c: [523512277, 79614320000000], + }, hasBaton: false, }, { info: { height: 681190, tx_hash: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', tx_pos: 2, value: 546, txid: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', tokenTicker: '🍔', tokenName: 'Burger', tokenDocumentUrl: 'https://c4.wallpaperflare.com/wallpaper/58/564/863/giant-hamburger-wallpaper-preview.jpg', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', - balance: '1', + balance: { + s: 1, + e: 0, + c: [1], + }, hasBaton: false, }, { info: { height: 681191, tx_hash: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', tx_pos: 2, value: 546, txid: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', tokenTicker: 'NOCOVID', tokenName: 'Covid19 Lifetime Immunity', tokenDocumentUrl: 'https://www.who.int/emergencies/diseases/novel-coronavirus-2019/covid-19-vaccines', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '996797', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', - balance: '996797', + balance: { + s: 1, + e: 5, + c: [996797], + }, hasBaton: false, }, { info: { height: 681191, tx_hash: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', tx_pos: 2, value: 546, txid: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', tokenTicker: 'TAP', tokenName: 'Thoughts and Prayers', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', - balance: '1', + balance: { + s: 1, + e: 0, + c: [1], + }, hasBaton: false, }, { info: { height: 681191, tx_hash: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', tx_pos: 2, value: 546, txid: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', tokenTicker: 'NAKAMOTO', tokenName: 'NAKAMOTO', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 8, tokenType: 1, tokenQty: '0.99999999', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', - balance: '0.99999999', + balance: { + s: 1, + e: -1, + c: [99999999000000], + }, hasBaton: false, }, { info: { height: 681191, tx_hash: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', tx_pos: 2, value: 546, txid: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', tokenTicker: 'HONK', tokenName: 'HONK HONK', tokenDocumentUrl: 'THE REAL HONK SLP TOKEN', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', - balance: '1', + balance: { + s: 1, + e: 0, + c: [1], + }, hasBaton: false, }, { info: { height: 681329, tx_hash: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', tx_pos: 2, value: 546, txid: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenTicker: 'XBIT', tokenName: 'eBits', tokenDocumentUrl: 'https://boomertakes.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '999997.999999994', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', - balance: '999997.999999994', + balance: { + s: 1, + e: 5, + c: [999997, 99999999400000], + }, hasBaton: false, }, ], + slpBalancesAndUtxos: { + nonSlpUtxos: [ + { + height: 682107, + tx_hash: + '8d4c90ecf069e3a1494339724ddbb8bf28e3b38315a009ca5c49237b3ae7687a', + tx_pos: 1, + value: 1503017804, + txid: '8d4c90ecf069e3a1494339724ddbb8bf28e3b38315a009ca5c49237b3ae7687a', + isValid: false, + address: + 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', + outpoint: { + outIdx: 1, + txid: '8d4c90ecf069e3a1494339724ddbb8bf28e3b38315a009ca5c49237b3ae7687a', + }, + }, + ], + slpUtxos: [ + { + height: 680782, + tx_hash: + '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', + tx_pos: 1, + value: 546, + txid: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', + vout: 1, + utxoType: 'token', + transactionType: 'send', + tokenTicker: 'ST', + tokenName: 'ST', + tokenDocumentUrl: 'developer.bitcoin.com', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '1', + isValid: true, + address: + 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', + slpMeta: { + tokenId: + 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', + }, + }, + { + height: 680784, + tx_hash: + 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', + tx_pos: 1, + value: 546, + txid: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', + vout: 1, + utxoType: 'token', + transactionType: 'send', + slpMeta: { + tokenId: + 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', + }, + tokenTicker: 'CTP', + tokenName: 'Cash Tab Points', + tokenDocumentUrl: 'https://cashtabapp.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '1e-9', + isValid: true, + address: + 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', + }, + { + height: 681189, + tx_hash: + 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', + tx_pos: 2, + value: 546, + txid: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', + vout: 2, + utxoType: 'token', + transactionType: 'send', + slpMeta: { + tokenId: + '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', + }, + tokenTicker: 'WDT', + tokenName: + 'Test Token With Exceptionally Long Name For CSS And Style Revisions', + tokenDocumentUrl: + 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', + tokenDocumentHash: + '����\\�IS\u001e9�����k+���\u0018���\u001b]�߷2��', + decimals: 7, + tokenType: 1, + tokenQty: '523512277.7961432', + isValid: true, + address: + 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', + }, + { + height: 681190, + tx_hash: + 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', + tx_pos: 2, + value: 546, + txid: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', + vout: 2, + utxoType: 'token', + transactionType: 'send', + slpMeta: { + tokenId: + '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', + }, + tokenTicker: '🍔', + tokenName: 'Burger', + tokenDocumentUrl: + 'https://c4.wallpaperflare.com/wallpaper/58/564/863/giant-hamburger-wallpaper-preview.jpg', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '1', + isValid: true, + address: + 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', + }, + { + height: 681191, + tx_hash: + '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', + tx_pos: 2, + value: 546, + txid: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', + vout: 2, + utxoType: 'token', + transactionType: 'send', + slpMeta: { + tokenId: + '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', + }, + tokenTicker: 'NOCOVID', + tokenName: 'Covid19 Lifetime Immunity', + tokenDocumentUrl: + 'https://www.who.int/emergencies/diseases/novel-coronavirus-2019/covid-19-vaccines', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '996797', + isValid: true, + address: + 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', + }, + { + height: 681191, + tx_hash: + 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', + tx_pos: 2, + value: 546, + txid: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', + vout: 2, + utxoType: 'token', + transactionType: 'send', + slpMeta: { + tokenId: + 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', + }, + tokenTicker: 'TAP', + tokenName: 'Thoughts and Prayers', + tokenDocumentUrl: '', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '1', + isValid: true, + address: + 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', + }, + { + height: 681191, + tx_hash: + 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', + tx_pos: 2, + value: 546, + txid: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', + vout: 2, + utxoType: 'token', + transactionType: 'send', + slpMeta: { + tokenId: + 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', + }, + tokenTicker: 'NAKAMOTO', + tokenName: 'NAKAMOTO', + tokenDocumentUrl: '', + tokenDocumentHash: '', + decimals: 8, + tokenType: 1, + tokenQty: '0.99999999', + isValid: true, + address: + 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', + }, + { + height: 681191, + tx_hash: + 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', + tx_pos: 2, + value: 546, + txid: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', + vout: 2, + utxoType: 'token', + transactionType: 'send', + slpMeta: { + tokenId: + '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', + }, + tokenTicker: 'HONK', + tokenName: 'HONK HONK', + tokenDocumentUrl: 'THE REAL HONK SLP TOKEN', + tokenDocumentHash: '', + decimals: 0, + tokenType: 1, + tokenQty: '1', + isValid: true, + address: + 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', + }, + { + height: 681329, + tx_hash: + '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', + tx_pos: 2, + value: 546, + txid: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', + vout: 2, + utxoType: 'token', + transactionType: 'send', + slpMeta: { + tokenId: + '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', + }, + tokenTicker: 'XBIT', + tokenName: 'eBits', + tokenDocumentUrl: 'https://boomertakes.com/', + tokenDocumentHash: '', + decimals: 9, + tokenType: 1, + tokenQty: '999997.999999994', + isValid: true, + address: + 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', + }, + ], + }, + parsedTxHistory: [ + { + txid: '8d4c90ecf069e3a1494339724ddbb8bf28e3b38315a009ca5c49237b3ae7687a', + confirmations: 644, + height: 682107, + blocktime: 1618439595, + amountSent: 0.00002, + amountReceived: 0, + tokenTx: false, + outgoingTx: true, + destinationAddress: + 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', + }, + { + txid: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', + confirmations: 1422, + height: 681329, + blocktime: 1617988189, + amountSent: 0.00000546, + amountReceived: 0, + tokenTx: true, + outgoingTx: true, + destinationAddress: + 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', + tokenInfo: { + qtySent: '1e-9', + qtyReceived: '0', + tokenId: + '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', + tokenName: 'eBits', + tokenTicker: 'XBIT', + }, + }, + { + txid: 'f27ff24c15b01c30d44218c6dc8706fd33cc7bc9b4b38399075f0f41d8e412af', + confirmations: 1559, + height: 681192, + blocktime: 1617923457, + amountSent: 0.00000546, + amountReceived: 0, + tokenTx: true, + outgoingTx: true, + destinationAddress: + 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', + tokenInfo: { + qtySent: '5e-9', + qtyReceived: '0', + tokenId: + '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', + tokenName: 'eBits', + tokenTicker: 'XBIT', + }, + }, + { + txid: 'b7f8b23f5ce12842eb655239919b6142052a2fa2b2ce974a4baac36b0137f332', + confirmations: 1559, + height: 681192, + blocktime: 1617923457, + amountSent: 0.00000546, + amountReceived: 0, + tokenTx: true, + outgoingTx: true, + destinationAddress: + 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', + tokenInfo: { + qtySent: '4e-9', + qtyReceived: '0', + tokenId: + '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', + tokenName: 'eBits', + tokenTicker: 'XBIT', + }, + }, + { + txid: '880baf5691c2b4c5a22ae4032e2004c0c54bfabf003468044a2e341846137136', + confirmations: 1559, + height: 681192, + blocktime: 1617923457, + amountSent: 0.00000546, + amountReceived: 0, + tokenTx: true, + outgoingTx: true, + destinationAddress: + 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', + tokenInfo: { + qtySent: '3e-9', + qtyReceived: '0', + tokenId: + '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', + tokenName: 'eBits', + tokenTicker: 'XBIT', + }, + }, + ], + }, +}; + +// Sample unmigrated (i.e. before diff adding required fields to wallet.state) wallet, with private key info removed +export const invalidStoredWallet = { + mnemonic: 'Remembered to take this out this time', + name: 'TripDos', + Path245: { + cashAddress: 'bitcoincash:qq0mw6nah9huwaxt45qw3fegjpszkjlrqsvttwy36p', + slpAddress: 'simpleledger:qq0mw6nah9huwaxt45qw3fegjpszkjlrqsqsq433yl', + fundingWif: 'Remembered to take this out this time', + fundingAddress: + 'simpleledger:qq0mw6nah9huwaxt45qw3fegjpszkjlrqsqsq433yl', + legacyAddress: '13thfuvhCA1dGE7nVgyU61BZfoD8ApXJsg', + }, + Path145: { + cashAddress: 'bitcoincash:qz5lf9pxde9neq3hzte8mmwts03sktl9nuz6m3dynu', + slpAddress: 'simpleledger:qz5lf9pxde9neq3hzte8mmwts03sktl9nuwps2cydz', + fundingWif: 'Remembered to take this out this time', + fundingAddress: + 'simpleledger:qz5lf9pxde9neq3hzte8mmwts03sktl9nuwps2cydz', + legacyAddress: '1GVeC3gB6V3EStcQbJiry5BJn4fRdHjKyc', + }, + Path1899: { + cashAddress: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', + slpAddress: 'simpleledger:qz2708636snqhsxu8wnlka78h6fdp77ar5syue64fa', + fundingWif: 'Remembered to take this out this time', + fundingAddress: + 'simpleledger:qz2708636snqhsxu8wnlka78h6fdp77ar5syue64fa', + legacyAddress: '1Efd9z9GRVJK2r73nUpFmBnsKUmfXNm2y2', + }, + state: { + balances: { + totalBalanceInSatoshis: 1503017804, + totalBalance: 15.03017804, + }, slpBalancesAndUtxos: { tokens: [ { info: { height: 680782, tx_hash: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', tx_pos: 1, value: 546, txid: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', vout: 1, utxoType: 'token', transactionType: 'send', tokenId: 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', tokenTicker: 'ST', tokenName: 'ST', tokenDocumentUrl: 'developer.bitcoin.com', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', balance: '1', hasBaton: false, }, { info: { height: 680784, tx_hash: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', tx_pos: 1, value: 546, txid: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', vout: 1, utxoType: 'token', transactionType: 'send', tokenId: 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', tokenTicker: 'CTP', tokenName: 'Cash Tab Points', tokenDocumentUrl: 'https://cashtabapp.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '1e-9', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', balance: '1e-9', hasBaton: false, }, { info: { height: 681189, tx_hash: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', tx_pos: 2, value: 546, txid: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', tokenTicker: 'WDT', tokenName: 'Test Token With Exceptionally Long Name For CSS And Style Revisions', tokenDocumentUrl: 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', tokenDocumentHash: '����\\�IS\u001e9�����k+���\u0018���\u001b]�߷2��', decimals: 7, tokenType: 1, tokenQty: '523512277.7961432', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', balance: '523512277.7961432', hasBaton: false, }, { info: { height: 681190, tx_hash: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', tx_pos: 2, value: 546, txid: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', tokenTicker: '🍔', tokenName: 'Burger', tokenDocumentUrl: 'https://c4.wallpaperflare.com/wallpaper/58/564/863/giant-hamburger-wallpaper-preview.jpg', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', balance: '1', hasBaton: false, }, { info: { height: 681191, tx_hash: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', tx_pos: 2, value: 546, txid: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', tokenTicker: 'NOCOVID', tokenName: 'Covid19 Lifetime Immunity', tokenDocumentUrl: 'https://www.who.int/emergencies/diseases/novel-coronavirus-2019/covid-19-vaccines', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '996797', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', balance: '996797', hasBaton: false, }, { info: { height: 681191, tx_hash: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', tx_pos: 2, value: 546, txid: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', tokenTicker: 'TAP', tokenName: 'Thoughts and Prayers', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', balance: '1', hasBaton: false, }, { info: { height: 681191, tx_hash: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', tx_pos: 2, value: 546, txid: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', tokenTicker: 'NAKAMOTO', tokenName: 'NAKAMOTO', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 8, tokenType: 1, tokenQty: '0.99999999', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', balance: '0.99999999', hasBaton: false, }, { info: { height: 681191, tx_hash: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', tx_pos: 2, value: 546, txid: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', tokenTicker: 'HONK', tokenName: 'HONK HONK', tokenDocumentUrl: 'THE REAL HONK SLP TOKEN', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', balance: '1', hasBaton: false, }, { info: { height: 681329, tx_hash: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', tx_pos: 2, value: 546, txid: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenTicker: 'XBIT', tokenName: 'eBits', tokenDocumentUrl: 'https://boomertakes.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '999997.999999994', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', balance: '999997.999999994', hasBaton: false, }, ], nonSlpUtxos: [ { height: 682107, tx_hash: '8d4c90ecf069e3a1494339724ddbb8bf28e3b38315a009ca5c49237b3ae7687a', tx_pos: 1, value: 1503017804, txid: '8d4c90ecf069e3a1494339724ddbb8bf28e3b38315a009ca5c49237b3ae7687a', vout: 1, isValid: false, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', wif: 'Remembered to take this out this time', }, ], slpUtxos: [ { height: 680782, tx_hash: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', tx_pos: 1, value: 546, txid: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', vout: 1, utxoType: 'token', transactionType: 'send', tokenId: 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', tokenTicker: 'ST', tokenName: 'ST', tokenDocumentUrl: 'developer.bitcoin.com', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 680784, tx_hash: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', tx_pos: 1, value: 546, txid: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', vout: 1, utxoType: 'token', transactionType: 'send', tokenId: 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', tokenTicker: 'CTP', tokenName: 'Cash Tab Points', tokenDocumentUrl: 'https://cashtabapp.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '1e-9', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681189, tx_hash: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', tx_pos: 2, value: 546, txid: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', tokenTicker: 'WDT', tokenName: 'Test Token With Exceptionally Long Name For CSS And Style Revisions', tokenDocumentUrl: 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', tokenDocumentHash: '����\\�IS\u001e9�����k+���\u0018���\u001b]�߷2��', decimals: 7, tokenType: 1, tokenQty: '523512277.7961432', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681190, tx_hash: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', tx_pos: 2, value: 546, txid: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', tokenTicker: '🍔', tokenName: 'Burger', tokenDocumentUrl: 'https://c4.wallpaperflare.com/wallpaper/58/564/863/giant-hamburger-wallpaper-preview.jpg', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681191, tx_hash: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', tx_pos: 2, value: 546, txid: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', tokenTicker: 'NOCOVID', tokenName: 'Covid19 Lifetime Immunity', tokenDocumentUrl: 'https://www.who.int/emergencies/diseases/novel-coronavirus-2019/covid-19-vaccines', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '996797', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681191, tx_hash: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', tx_pos: 2, value: 546, txid: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', tokenTicker: 'TAP', tokenName: 'Thoughts and Prayers', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681191, tx_hash: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', tx_pos: 2, value: 546, txid: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', tokenTicker: 'NAKAMOTO', tokenName: 'NAKAMOTO', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 8, tokenType: 1, tokenQty: '0.99999999', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681191, tx_hash: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', tx_pos: 2, value: 546, txid: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', tokenTicker: 'HONK', tokenName: 'HONK HONK', tokenDocumentUrl: 'THE REAL HONK SLP TOKEN', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681329, tx_hash: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', tx_pos: 2, value: 546, txid: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenTicker: 'XBIT', tokenName: 'eBits', tokenDocumentUrl: 'https://boomertakes.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '999997.999999994', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, ], }, parsedTxHistory: [ { txid: '8d4c90ecf069e3a1494339724ddbb8bf28e3b38315a009ca5c49237b3ae7687a', confirmations: 643, height: 682107, blocktime: 1618439595, amountSent: 0.00002, amountReceived: 0, tokenTx: false, outgoingTx: true, destinationAddress: 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', }, { txid: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', confirmations: 1421, height: 681329, blocktime: 1617988189, amountSent: 0.00000546, amountReceived: 0, tokenTx: true, outgoingTx: true, destinationAddress: 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', tokenInfo: { qtySent: '1e-9', qtyReceived: '0', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenName: 'eBits', tokenTicker: 'XBIT', }, }, { txid: 'f27ff24c15b01c30d44218c6dc8706fd33cc7bc9b4b38399075f0f41d8e412af', confirmations: 1558, height: 681192, blocktime: 1617923457, amountSent: 0.00000546, amountReceived: 0, tokenTx: true, outgoingTx: true, destinationAddress: 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', tokenInfo: { qtySent: '5e-9', qtyReceived: '0', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenName: 'eBits', tokenTicker: 'XBIT', }, }, { txid: 'b7f8b23f5ce12842eb655239919b6142052a2fa2b2ce974a4baac36b0137f332', confirmations: 1558, height: 681192, blocktime: 1617923457, amountSent: 0.00000546, amountReceived: 0, tokenTx: true, outgoingTx: true, destinationAddress: 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', tokenInfo: { qtySent: '4e-9', qtyReceived: '0', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenName: 'eBits', tokenTicker: 'XBIT', }, }, { txid: '880baf5691c2b4c5a22ae4032e2004c0c54bfabf003468044a2e341846137136', confirmations: 1558, height: 681192, blocktime: 1617923457, amountSent: 0.00000546, amountReceived: 0, tokenTx: true, outgoingTx: true, destinationAddress: 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', tokenInfo: { qtySent: '3e-9', qtyReceived: '0', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenName: 'eBits', tokenTicker: 'XBIT', }, }, ], }, }; // A previously valid wallet export const invalidpreChronikStoredWallet = { mnemonic: 'Nope', name: 'TripDos', Path245: { cashAddress: 'bitcoincash:qq0mw6nah9huwaxt45qw3fegjpszkjlrqsvttwy36p', slpAddress: 'simpleledger:qq0mw6nah9huwaxt45qw3fegjpszkjlrqsqsq433yl', fundingWif: 'Nope', fundingAddress: 'simpleledger:qq0mw6nah9huwaxt45qw3fegjpszkjlrqsqsq433yl', legacyAddress: '13thfuvhCA1dGE7nVgyU61BZfoD8ApXJsg', }, Path145: { cashAddress: 'bitcoincash:qz5lf9pxde9neq3hzte8mmwts03sktl9nuz6m3dynu', slpAddress: 'simpleledger:qz5lf9pxde9neq3hzte8mmwts03sktl9nuwps2cydz', fundingWif: 'Nope', fundingAddress: 'simpleledger:qz5lf9pxde9neq3hzte8mmwts03sktl9nuwps2cydz', legacyAddress: '1GVeC3gB6V3EStcQbJiry5BJn4fRdHjKyc', }, Path1899: { cashAddress: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', slpAddress: 'simpleledger:qz2708636snqhsxu8wnlka78h6fdp77ar5syue64fa', fundingWif: 'Nope', fundingAddress: 'simpleledger:qz2708636snqhsxu8wnlka78h6fdp77ar5syue64fa', legacyAddress: '1Efd9z9GRVJK2r73nUpFmBnsKUmfXNm2y2', }, state: { balances: { totalBalanceInSatoshis: '1503017804', totalBalance: '15030178.04', }, tokens: [ { info: { height: 680782, tx_hash: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', tx_pos: 1, value: 546, txid: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', vout: 1, utxoType: 'token', transactionType: 'send', tokenId: 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', tokenTicker: 'ST', tokenName: 'ST', tokenDocumentUrl: 'developer.bitcoin.com', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', balance: { s: 1, e: 0, c: [1], }, hasBaton: false, }, { info: { height: 680784, tx_hash: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', tx_pos: 1, value: 546, txid: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', vout: 1, utxoType: 'token', transactionType: 'send', tokenId: 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', tokenTicker: 'CTP', tokenName: 'Cash Tab Points', tokenDocumentUrl: 'https://cashtabapp.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '1e-9', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', balance: { s: 1, e: -9, c: [100000], }, hasBaton: false, }, { info: { height: 681189, tx_hash: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', tx_pos: 2, value: 546, txid: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', tokenTicker: 'WDT', tokenName: 'Test Token With Exceptionally Long Name For CSS And Style Revisions', tokenDocumentUrl: 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', tokenDocumentHash: '����\\�IS\u001e9�����k+���\u0018���\u001b]�߷2��', decimals: 7, tokenType: 1, tokenQty: '523512277.7961432', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', balance: { s: 1, e: 8, c: [523512277, 79614320000000], }, hasBaton: false, }, { info: { height: 681190, tx_hash: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', tx_pos: 2, value: 546, txid: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', tokenTicker: '🍔', tokenName: 'Burger', tokenDocumentUrl: 'https://c4.wallpaperflare.com/wallpaper/58/564/863/giant-hamburger-wallpaper-preview.jpg', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', balance: { s: 1, e: 0, c: [1], }, hasBaton: false, }, { info: { height: 681191, tx_hash: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', tx_pos: 2, value: 546, txid: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', tokenTicker: 'NOCOVID', tokenName: 'Covid19 Lifetime Immunity', tokenDocumentUrl: 'https://www.who.int/emergencies/diseases/novel-coronavirus-2019/covid-19-vaccines', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '996797', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', balance: { s: 1, e: 5, c: [996797], }, hasBaton: false, }, { info: { height: 681191, tx_hash: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', tx_pos: 2, value: 546, txid: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', tokenTicker: 'TAP', tokenName: 'Thoughts and Prayers', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', balance: { s: 1, e: 0, c: [1], }, hasBaton: false, }, { info: { height: 681191, tx_hash: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', tx_pos: 2, value: 546, txid: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', tokenTicker: 'NAKAMOTO', tokenName: 'NAKAMOTO', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 8, tokenType: 1, tokenQty: '0.99999999', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', balance: { s: 1, e: -1, c: [99999999000000], }, hasBaton: false, }, { info: { height: 681191, tx_hash: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', tx_pos: 2, value: 546, txid: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', tokenTicker: 'HONK', tokenName: 'HONK HONK', tokenDocumentUrl: 'THE REAL HONK SLP TOKEN', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', balance: { s: 1, e: 0, c: [1], }, hasBaton: false, }, { info: { height: 681329, tx_hash: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', tx_pos: 2, value: 546, txid: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenTicker: 'XBIT', tokenName: 'eBits', tokenDocumentUrl: 'https://boomertakes.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '999997.999999994', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', balance: { s: 1, e: 5, c: [999997, 99999999400000], }, hasBaton: false, }, ], slpBalancesAndUtxos: { tokens: [ { info: { height: 680782, tx_hash: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', tx_pos: 1, value: 546, txid: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', vout: 1, utxoType: 'token', transactionType: 'send', tokenId: 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', tokenTicker: 'ST', tokenName: 'ST', tokenDocumentUrl: 'developer.bitcoin.com', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', balance: { s: 1, e: 0, c: [1], }, hasBaton: false, }, { info: { height: 680784, tx_hash: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', tx_pos: 1, value: 546, txid: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', vout: 1, utxoType: 'token', transactionType: 'send', tokenId: 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', tokenTicker: 'CTP', tokenName: 'Cash Tab Points', tokenDocumentUrl: 'https://cashtabapp.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '1e-9', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', balance: { s: 1, e: -9, c: [100000], }, hasBaton: false, }, { info: { height: 681189, tx_hash: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', tx_pos: 2, value: 546, txid: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', tokenTicker: 'WDT', tokenName: 'Test Token With Exceptionally Long Name For CSS And Style Revisions', tokenDocumentUrl: 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', tokenDocumentHash: '����\\�IS\u001e9�����k+���\u0018���\u001b]�߷2��', decimals: 7, tokenType: 1, tokenQty: '523512277.7961432', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', balance: { s: 1, e: 8, c: [523512277, 79614320000000], }, hasBaton: false, }, { info: { height: 681190, tx_hash: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', tx_pos: 2, value: 546, txid: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', tokenTicker: '🍔', tokenName: 'Burger', tokenDocumentUrl: 'https://c4.wallpaperflare.com/wallpaper/58/564/863/giant-hamburger-wallpaper-preview.jpg', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', balance: { s: 1, e: 0, c: [1], }, hasBaton: false, }, { info: { height: 681191, tx_hash: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', tx_pos: 2, value: 546, txid: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', tokenTicker: 'NOCOVID', tokenName: 'Covid19 Lifetime Immunity', tokenDocumentUrl: 'https://www.who.int/emergencies/diseases/novel-coronavirus-2019/covid-19-vaccines', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '996797', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', balance: { s: 1, e: 5, c: [996797], }, hasBaton: false, }, { info: { height: 681191, tx_hash: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', tx_pos: 2, value: 546, txid: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', tokenTicker: 'TAP', tokenName: 'Thoughts and Prayers', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', balance: { s: 1, e: 0, c: [1], }, hasBaton: false, }, { info: { height: 681191, tx_hash: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', tx_pos: 2, value: 546, txid: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', tokenTicker: 'NAKAMOTO', tokenName: 'NAKAMOTO', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 8, tokenType: 1, tokenQty: '0.99999999', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', balance: { s: 1, e: -1, c: [99999999000000], }, hasBaton: false, }, { info: { height: 681191, tx_hash: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', tx_pos: 2, value: 546, txid: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', tokenTicker: 'HONK', tokenName: 'HONK HONK', tokenDocumentUrl: 'THE REAL HONK SLP TOKEN', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', balance: { s: 1, e: 0, c: [1], }, hasBaton: false, }, { info: { height: 681329, tx_hash: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', tx_pos: 2, value: 546, txid: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenTicker: 'XBIT', tokenName: 'eBits', tokenDocumentUrl: 'https://boomertakes.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '999997.999999994', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', balance: { s: 1, e: 5, c: [999997, 99999999400000], }, hasBaton: false, }, ], nonSlpUtxos: [ { height: 682107, tx_hash: '8d4c90ecf069e3a1494339724ddbb8bf28e3b38315a009ca5c49237b3ae7687a', tx_pos: 1, value: 1503017804, txid: '8d4c90ecf069e3a1494339724ddbb8bf28e3b38315a009ca5c49237b3ae7687a', isValid: false, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', outpoint: { outIdx: 1, txid: '8d4c90ecf069e3a1494339724ddbb8bf28e3b38315a009ca5c49237b3ae7687a', }, }, ], slpUtxos: [ { height: 680782, tx_hash: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', tx_pos: 1, value: 546, txid: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', vout: 1, utxoType: 'token', transactionType: 'send', tokenTicker: 'ST', tokenName: 'ST', tokenDocumentUrl: 'developer.bitcoin.com', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', slpMeta: { tokenId: 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', }, }, { height: 680784, tx_hash: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', tx_pos: 1, value: 546, txid: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', vout: 1, utxoType: 'token', transactionType: 'send', slpMeta: { tokenId: 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', }, tokenTicker: 'CTP', tokenName: 'Cash Tab Points', tokenDocumentUrl: 'https://cashtabapp.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '1e-9', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681189, tx_hash: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', tx_pos: 2, value: 546, txid: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', vout: 2, utxoType: 'token', transactionType: 'send', slpMeta: { tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', }, tokenTicker: 'WDT', tokenName: 'Test Token With Exceptionally Long Name For CSS And Style Revisions', tokenDocumentUrl: 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', tokenDocumentHash: '����\\�IS\u001e9�����k+���\u0018���\u001b]�߷2��', decimals: 7, tokenType: 1, tokenQty: '523512277.7961432', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681190, tx_hash: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', tx_pos: 2, value: 546, txid: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', vout: 2, utxoType: 'token', transactionType: 'send', slpMeta: { tokenId: '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', }, tokenTicker: '🍔', tokenName: 'Burger', tokenDocumentUrl: 'https://c4.wallpaperflare.com/wallpaper/58/564/863/giant-hamburger-wallpaper-preview.jpg', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681191, tx_hash: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', tx_pos: 2, value: 546, txid: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', vout: 2, utxoType: 'token', transactionType: 'send', slpMeta: { tokenId: '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', }, tokenTicker: 'NOCOVID', tokenName: 'Covid19 Lifetime Immunity', tokenDocumentUrl: 'https://www.who.int/emergencies/diseases/novel-coronavirus-2019/covid-19-vaccines', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '996797', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681191, tx_hash: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', tx_pos: 2, value: 546, txid: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', vout: 2, utxoType: 'token', transactionType: 'send', slpMeta: { tokenId: 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', }, tokenTicker: 'TAP', tokenName: 'Thoughts and Prayers', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681191, tx_hash: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', tx_pos: 2, value: 546, txid: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', vout: 2, utxoType: 'token', transactionType: 'send', slpMeta: { tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', }, tokenTicker: 'NAKAMOTO', tokenName: 'NAKAMOTO', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 8, tokenType: 1, tokenQty: '0.99999999', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681191, tx_hash: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', tx_pos: 2, value: 546, txid: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', vout: 2, utxoType: 'token', transactionType: 'send', slpMeta: { tokenId: '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', }, tokenTicker: 'HONK', tokenName: 'HONK HONK', tokenDocumentUrl: 'THE REAL HONK SLP TOKEN', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681329, tx_hash: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', tx_pos: 2, value: 546, txid: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', vout: 2, utxoType: 'token', transactionType: 'send', slpMeta: { tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', }, tokenTicker: 'XBIT', tokenName: 'eBits', tokenDocumentUrl: 'https://boomertakes.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '999997.999999994', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, ], }, parsedTxHistory: [ { txid: '8d4c90ecf069e3a1494339724ddbb8bf28e3b38315a009ca5c49237b3ae7687a', confirmations: 644, height: 682107, blocktime: 1618439595, amountSent: 0.00002, amountReceived: 0, tokenTx: false, outgoingTx: true, destinationAddress: 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', }, { txid: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', confirmations: 1422, height: 681329, blocktime: 1617988189, amountSent: 0.00000546, amountReceived: 0, tokenTx: true, outgoingTx: true, destinationAddress: 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', tokenInfo: { qtySent: '1e-9', qtyReceived: '0', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenName: 'eBits', tokenTicker: 'XBIT', }, }, { txid: 'f27ff24c15b01c30d44218c6dc8706fd33cc7bc9b4b38399075f0f41d8e412af', confirmations: 1559, height: 681192, blocktime: 1617923457, amountSent: 0.00000546, amountReceived: 0, tokenTx: true, outgoingTx: true, destinationAddress: 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', tokenInfo: { qtySent: '5e-9', qtyReceived: '0', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenName: 'eBits', tokenTicker: 'XBIT', }, }, { txid: 'b7f8b23f5ce12842eb655239919b6142052a2fa2b2ce974a4baac36b0137f332', confirmations: 1559, height: 681192, blocktime: 1617923457, amountSent: 0.00000546, amountReceived: 0, tokenTx: true, outgoingTx: true, destinationAddress: 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', tokenInfo: { qtySent: '4e-9', qtyReceived: '0', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenName: 'eBits', tokenTicker: 'XBIT', }, }, { txid: '880baf5691c2b4c5a22ae4032e2004c0c54bfabf003468044a2e341846137136', confirmations: 1559, height: 681192, blocktime: 1617923457, amountSent: 0.00000546, amountReceived: 0, tokenTx: true, outgoingTx: true, destinationAddress: 'bitcoincash:qqartrrq3npyzpcqswq2hcslstzu38mq8gvgtuqfpf', tokenInfo: { qtySent: '3e-9', qtyReceived: '0', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenName: 'eBits', tokenTicker: 'XBIT', }, }, ], utxos: [ { utxos: [], address: 'bitcoincash:qq0mw6nah9huwaxt45qw3fegjpszkjlrqsvttwy36p', }, { utxos: [], address: 'bitcoincash:qz5lf9pxde9neq3hzte8mmwts03sktl9nuz6m3dynu', }, { utxos: [ { height: 680782, tx_hash: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', tx_pos: 1, value: 546, }, { height: 680784, tx_hash: '28f061fee068d3b9cb578141bac3d4d9ec4eccebec680464bf0aafaac414811f', tx_pos: 1, value: 546, }, { height: 680784, tx_hash: '5fa3ffccea55c968beb7d214c563c92336ce2bbccbb714ba819848a7f7060bdb', tx_pos: 1, value: 546, }, { height: 680784, tx_hash: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', tx_pos: 1, value: 546, }, { height: 681189, tx_hash: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', tx_pos: 2, value: 546, }, { height: 681190, tx_hash: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', tx_pos: 2, value: 546, }, { height: 681191, tx_hash: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', tx_pos: 2, value: 546, }, { height: 681191, tx_hash: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', tx_pos: 2, value: 546, }, { height: 681191, tx_hash: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', tx_pos: 2, value: 546, }, { height: 681191, tx_hash: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', tx_pos: 2, value: 546, }, { height: 681329, tx_hash: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', tx_pos: 2, value: 546, }, { height: 682107, tx_hash: '8d4c90ecf069e3a1494339724ddbb8bf28e3b38315a009ca5c49237b3ae7687a', tx_pos: 1, value: 1503017804, }, ], address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, ], hydratedUtxoDetails: { slpUtxos: [ { utxos: [ { height: 680782, tx_hash: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', tx_pos: 1, value: 546, txid: '525457276f1b6984170c9b35a8312d4988fce495723eabadd2afcdb3b872b2f1', vout: 1, utxoType: 'token', transactionType: 'send', tokenId: 'bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd', tokenTicker: 'ST', tokenName: 'ST', tokenDocumentUrl: 'developer.bitcoin.com', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 680784, tx_hash: '28f061fee068d3b9cb578141bac3d4d9ec4eccebec680464bf0aafaac414811f', tx_pos: 1, value: 546, txid: '28f061fee068d3b9cb578141bac3d4d9ec4eccebec680464bf0aafaac414811f', vout: 1, utxoType: 'token', transactionType: 'send', tokenId: 'bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba', tokenTicker: 'TBS', tokenName: 'TestBits', tokenDocumentUrl: 'https://thecryptoguy.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '9897999885.21030105', isValid: false, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 680784, tx_hash: '5fa3ffccea55c968beb7d214c563c92336ce2bbccbb714ba819848a7f7060bdb', tx_pos: 1, value: 546, txid: '5fa3ffccea55c968beb7d214c563c92336ce2bbccbb714ba819848a7f7060bdb', vout: 1, utxoType: 'token', transactionType: 'send', tokenId: 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', tokenTicker: 'CTP', tokenName: 'Cash Tab Points', tokenDocumentUrl: 'https://cashtabapp.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '308.87654321', isValid: false, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 680784, tx_hash: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', tx_pos: 1, value: 546, txid: 'daa98a872b7d88fefd2257b006db001ef82a601f3943b92e0c753076598a7b75', vout: 1, utxoType: 'token', transactionType: 'send', tokenId: 'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1', tokenTicker: 'CTP', tokenName: 'Cash Tab Points', tokenDocumentUrl: 'https://cashtabapp.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '1e-9', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681189, tx_hash: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', tx_pos: 2, value: 546, txid: 'f38ccfa615e38f0c871f4eb35db420157808014f1f5743f1522529253c0c4c56', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '7443f7c831cdf2b2b04d5f0465ed0bcf348582675b0e4f17906438c232c22f3d', tokenTicker: 'WDT', tokenName: 'Test Token With Exceptionally Long Name For CSS And Style Revisions', tokenDocumentUrl: 'https://www.ImpossiblyLongWebsiteDidYouThinkWebDevWouldBeFun.org', tokenDocumentHash: '����\\�IS\u001e9�����k+���\u0018���\u001b]�߷2��', decimals: 7, tokenType: 1, tokenQty: '523512277.7961432', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681190, tx_hash: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', tx_pos: 2, value: 546, txid: 'e9dca9aa954131a0004325fff11dfddcd6e5843c468116cf4d38cb264032cdc0', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '1f6a65e7a4bde92c0a012de2bcf4007034504a765377cdf08a3ee01d1eaa6901', tokenTicker: '🍔', tokenName: 'Burger', tokenDocumentUrl: 'https://c4.wallpaperflare.com/wallpaper/58/564/863/giant-hamburger-wallpaper-preview.jpg', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681191, tx_hash: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', tx_pos: 2, value: 546, txid: '091c9f32deb2f4f3733673803f51acf050b65d8042d1561824c6cd22d14bb43b', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', tokenTicker: 'NOCOVID', tokenName: 'Covid19 Lifetime Immunity', tokenDocumentUrl: 'https://www.who.int/emergencies/diseases/novel-coronavirus-2019/covid-19-vaccines', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '996797', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681191, tx_hash: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', tx_pos: 2, value: 546, txid: 'b35c502f388cdfbdd6841b7a73e973149b3c8deca76295a3e4665939e0562796', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d', tokenTicker: 'TAP', tokenName: 'Thoughts and Prayers', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681191, tx_hash: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', tx_pos: 2, value: 546, txid: 'c70408fca1a5bf48f338f7ef031e586293be6948a5bff1fbbdd4eb923ef11e59', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', tokenTicker: 'NAKAMOTO', tokenName: 'NAKAMOTO', tokenDocumentUrl: '', tokenDocumentHash: '', decimals: 8, tokenType: 1, tokenQty: '0.99999999', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681191, tx_hash: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', tx_pos: 2, value: 546, txid: 'e1097932e5a607c100dc73fa18169be2e501e1782c7c94500742974d6353476c', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '7f8889682d57369ed0e32336f8b7e0ffec625a35cca183f4e81fde4e71a538a1', tokenTicker: 'HONK', tokenName: 'HONK HONK', tokenDocumentUrl: 'THE REAL HONK SLP TOKEN', tokenDocumentHash: '', decimals: 0, tokenType: 1, tokenQty: '1', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 681329, tx_hash: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', tx_pos: 2, value: 546, txid: '08e9a7b9537e60f630eba0f339be6b97e9d8061d5fc0c4d3247226fc86574ce9', vout: 2, utxoType: 'token', transactionType: 'send', tokenId: '1101bd5d7b6bbc3176fb2b93d08e76ab532b04ff731d71502249e3cb9b6fcb1a', tokenTicker: 'XBIT', tokenName: 'eBits', tokenDocumentUrl: 'https://boomertakes.com/', tokenDocumentHash: '', decimals: 9, tokenType: 1, tokenQty: '999997.999999994', isValid: true, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, { height: 682107, tx_hash: '8d4c90ecf069e3a1494339724ddbb8bf28e3b38315a009ca5c49237b3ae7687a', tx_pos: 1, value: 1503017804, txid: '8d4c90ecf069e3a1494339724ddbb8bf28e3b38315a009ca5c49237b3ae7687a', vout: 1, isValid: false, address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', wif: 'Nope', }, ], address: 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', }, ], }, }, }; diff --git a/web/cashtab/src/utils/__tests__/cashMethods.test.js b/web/cashtab/src/utils/__tests__/cashMethods.test.js index 260650f26..7ff743626 100644 --- a/web/cashtab/src/utils/__tests__/cashMethods.test.js +++ b/web/cashtab/src/utils/__tests__/cashMethods.test.js @@ -1,1567 +1,1573 @@ import BigNumber from 'bignumber.js'; import { fromSatoshisToXec, flattenContactList, loadStoredWallet, isValidStoredWallet, fromLegacyDecimals, convertToEcashPrefix, isLegacyMigrationRequired, convertEtokenToEcashAddr, parseOpReturn, convertEcashtoEtokenAddr, getHashArrayFromWallet, isActiveWebsocket, parseXecSendValue, getChangeAddressFromInputUtxos, generateOpReturnScript, generateTxInput, generateTxOutput, generateTokenTxInput, signAndBuildTx, fromXecToSatoshis, getWalletBalanceFromUtxos, signUtxosByAddress, generateTokenTxOutput, getCashtabByteCount, calcFee, toHash160, generateGenesisOpReturn, generateSendOpReturn, generateBurnOpReturn, getECPairFromWIF, hash160ToAddress, } from 'utils/cashMethods'; import { currency } from 'components/Common/Ticker'; import { validAddressArrayInput } from '../__mocks__/mockAddressArray'; import { mockGenesisOpReturnScript, mockSendOpReturnScript, mockSendOpReturnTokenUtxos, mockBurnOpReturnScript, mockBurnOpReturnTokenUtxos, } from '../__mocks__/mockOpReturnScript'; import { cachedUtxos, utxosLoadedFromCache, } from '../__mocks__/mockCachedUtxos'; import { validStoredWallet, invalidStoredWallet, invalidpreChronikStoredWallet, + validStoredWalletAfter20221123Streamline, } from '../__mocks__/mockStoredWallets'; import { missingPath1899Wallet, missingPublicKeyInPath1899Wallet, missingPublicKeyInPath145Wallet, missingPublicKeyInPath245Wallet, notLegacyWallet, missingHash160, } from '../__mocks__/mockLegacyWalletsUtils'; import { shortCashtabMessageInputHex, longCashtabMessageInputHex, shortExternalMessageInputHex, longExternalMessageInputHex, shortSegmentedExternalMessageInputHex, longSegmentedExternalMessageInputHex, mixedSegmentedExternalMessageInputHex, mockParsedShortCashtabMessageArray, mockParsedLongCashtabMessageArray, mockParsedShortExternalMessageArray, mockParsedLongExternalMessageArray, mockParsedShortSegmentedExternalMessageArray, mockParsedLongSegmentedExternalMessageArray, mockParsedMixedSegmentedExternalMessageArray, eTokenInputHex, mockParsedETokenOutputArray, mockAirdropHexOutput, mockParsedAirdropMessageArray, } from '../__mocks__/mockOpReturnParsedArray'; import mockLegacyWallets from 'hooks/__mocks__/mockLegacyWallets'; import BCHJS from '@psf/bch-js'; import sendBCHMock from '../../hooks/__mocks__/sendBCH'; import { activeWebsocketAlpha, disconnectedWebsocketAlpha, unsubscribedWebsocket, } from '../__mocks__/chronikWs'; import mockNonSlpUtxos from '../../hooks/__mocks__/mockNonSlpUtxos'; import mockSlpUtxos from '../../hooks/__mocks__/mockSlpUtxos'; import { mockOneToOneSendXecTxBuilderObj, mockOneToManySendXecTxBuilderObj, mockCreateTokenOutputsTxBuilderObj, mockSendTokenOutputsTxBuilderObj, mockBurnTokenOutputsTxBuilderObj, mockCreateTokenTxBuilderObj, mockSendTokenTxBuilderObj, mockBurnTokenTxBuilderObj, } from '../__mocks__/mockTxBuilderObj'; import { mockSingleInputUtxo, mockMultipleInputUtxos, mockSingleOutput, mockMultipleOutputs, } from '../__mocks__/mockTxBuilderData'; import createTokenMock from '../../hooks/__mocks__/createToken'; import TransactionBuilder from 'utils/txBuilder'; import { mockWif, mockStringifiedECPair } from '../__mocks__/mockECPair'; it(`generateSendOpReturn() returns correct script object for valid tokenUtxo and send quantity`, () => { const BCH = new BCHJS(); const tokensToSend = 50; const sendOpReturnScriptObj = generateSendOpReturn( mockSendOpReturnTokenUtxos, tokensToSend, ); const legacySendOpReturnScriptObj = BCH.SLP.TokenType1.generateSendOpReturn( mockSendOpReturnTokenUtxos, tokensToSend.toString(), ); expect(JSON.stringify(sendOpReturnScriptObj.script)).toStrictEqual( JSON.stringify(mockSendOpReturnScript), ); expect(JSON.stringify(sendOpReturnScriptObj.script)).toStrictEqual( JSON.stringify(legacySendOpReturnScriptObj.script), ); }); it(`generateSendOpReturnScript() throws error on invalid input`, () => { const mockSendOpReturnTokenUtxos = null; const tokensToSend = 50; let errorThrown; try { generateSendOpReturn(mockSendOpReturnTokenUtxos, tokensToSend); } catch (err) { errorThrown = err.message; } expect(errorThrown).toStrictEqual('Invalid send token parameter'); }); it(`generateBurnOpReturn() returns correct script for valid tokenUtxo and burn quantity`, () => { const BCH = new BCHJS(); const tokensToBurn = 7000; const burnOpReturnScript = generateBurnOpReturn( mockBurnOpReturnTokenUtxos, tokensToBurn, ); const legacyBurnOpReturnScript = BCH.SLP.TokenType1.generateBurnOpReturn( mockBurnOpReturnTokenUtxos, tokensToBurn, ); expect(JSON.stringify(burnOpReturnScript)).toStrictEqual( JSON.stringify(mockBurnOpReturnScript), ); expect(JSON.stringify(burnOpReturnScript)).toStrictEqual( JSON.stringify(legacyBurnOpReturnScript), ); }); it(`generateBurnOpReturn() throws error on invalid input`, () => { const tokensToBurn = 7000; let errorThrown; try { generateBurnOpReturn(null, tokensToBurn); } catch (err) { errorThrown = err.message; } expect(errorThrown).toStrictEqual('Invalid burn token parameter'); }); it(`generateGenesisOpReturn() returns correct script for a valid configObj`, () => { const BCH = new BCHJS(); const configObj = { name: 'ethantest', ticker: 'ETN', documentUrl: 'https://cashtab.com/', decimals: '3', initialQty: '5000', documentHash: '', mintBatonVout: null, }; const genesisOpReturnScript = generateGenesisOpReturn(configObj); const legacyGenesisOpReturnScript = BCH.SLP.TokenType1.generateGenesisOpReturn(configObj); expect(JSON.stringify(genesisOpReturnScript)).toStrictEqual( JSON.stringify(mockGenesisOpReturnScript), ); expect(JSON.stringify(genesisOpReturnScript)).toStrictEqual( JSON.stringify(legacyGenesisOpReturnScript), ); }); it(`generateGenesisOpReturn() throws error on invalid configObj`, () => { const configObj = null; let errorThrown; try { generateGenesisOpReturn(configObj); } catch (err) { errorThrown = err.message; } expect(errorThrown).toStrictEqual('Invalid token configuration'); }); it(`signUtxosByAddress() successfully returns a txBuilder object for a one to one XEC tx`, () => { const isOneToMany = false; const { destinationAddress, wallet, utxos } = sendBCHMock; let txBuilder = new TransactionBuilder(); const satoshisToSendInput = new BigNumber(2184); const feeInSatsPerByte = currency.defaultFee; // mock tx input const inputObj = generateTxInput( isOneToMany, utxos, txBuilder, null, satoshisToSendInput, feeInSatsPerByte, ); // mock tx output const totalInputUtxoValue = mockOneToOneSendXecTxBuilderObj.transaction.inputs[0].value; const singleSendValue = new BigNumber( fromSatoshisToXec( mockOneToOneSendXecTxBuilderObj.transaction.tx.outs[0].value, ), ); const satoshisToSendOutput = fromXecToSatoshis( new BigNumber(singleSendValue), ); const txFee = new BigNumber(totalInputUtxoValue).minus( new BigNumber(satoshisToSendOutput), ); const changeAddress = wallet.Path1899.cashAddress; const outputObj = generateTxOutput( isOneToMany, singleSendValue, satoshisToSendOutput, totalInputUtxoValue, destinationAddress, null, changeAddress, txFee, inputObj.txBuilder, ); const txBuilderResponse = signUtxosByAddress( mockSingleInputUtxo, wallet, outputObj, ); expect(txBuilderResponse.toString()).toStrictEqual( mockOneToOneSendXecTxBuilderObj.toString(), ); }); it(`signUtxosByAddress() successfully returns a txBuilder object for a one to many XEC tx`, () => { const isOneToMany = true; const { wallet, utxos } = sendBCHMock; let txBuilder = new TransactionBuilder(); let destinationAddressAndValueArray = [ 'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,3000', 'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,3000', 'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,3000', ]; const satoshisToSendInput = new BigNumber(900000); const feeInSatsPerByte = currency.defaultFee; // mock tx input const inputObj = generateTxInput( isOneToMany, utxos, txBuilder, destinationAddressAndValueArray, satoshisToSendInput, feeInSatsPerByte, ); // mock tx output const totalInputUtxoValue = mockOneToManySendXecTxBuilderObj.transaction.inputs[0].value + mockOneToManySendXecTxBuilderObj.transaction.inputs[1].value + mockOneToManySendXecTxBuilderObj.transaction.inputs[2].value; const singleSendValue = null; const satoshisToSendOutput = new BigNumber( mockOneToManySendXecTxBuilderObj.transaction.tx.outs[0].value + mockOneToManySendXecTxBuilderObj.transaction.tx.outs[1].value + mockOneToManySendXecTxBuilderObj.transaction.tx.outs[2].value, ); const txFee = new BigNumber(totalInputUtxoValue) .minus(satoshisToSendOutput) .minus( new BigNumber( mockOneToManySendXecTxBuilderObj.transaction.tx.outs[3].value, ), ); // change value destinationAddressAndValueArray = validAddressArrayInput; const changeAddress = wallet.Path1899.cashAddress; const outputObj = generateTxOutput( isOneToMany, singleSendValue, satoshisToSendOutput, totalInputUtxoValue, null, destinationAddressAndValueArray, changeAddress, txFee, inputObj.txBuilder, ); const txBuilderResponse = signUtxosByAddress( mockSingleInputUtxo, wallet, outputObj, ); expect(txBuilderResponse.toString()).toStrictEqual( mockOneToManySendXecTxBuilderObj.toString(), ); }); it(`getChangeAddressFromInputUtxos() returns a correct change address from a valid inputUtxo`, () => { const { wallet } = sendBCHMock; const inputUtxo = [ { height: 669639, tx_hash: '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58', tx_pos: 0, value: 1000, txid: '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58', vout: 0, isValid: false, address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', }, ]; const changeAddress = getChangeAddressFromInputUtxos(inputUtxo, wallet); expect(changeAddress).toStrictEqual(inputUtxo[0].address); }); it(`getChangeAddressFromInputUtxos() throws error upon a malformed input utxo`, () => { const { wallet } = sendBCHMock; const invalidInputUtxo = [ { height: 669639, tx_hash: '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58', tx_pos: 0, value: 1000, txid: '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58', vout: 0, isValid: false, wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', }, ]; let thrownError; try { getChangeAddressFromInputUtxos(invalidInputUtxo, wallet); } catch (err) { thrownError = err; } expect(thrownError.message).toStrictEqual('Invalid input utxo'); }); it(`getChangeAddressFromInputUtxos() throws error upon a valid input utxo with invalid address param`, () => { const { wallet } = sendBCHMock; const invalidInputUtxo = [ { height: 669639, tx_hash: '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58', tx_pos: 0, value: 1000, address: 'bitcoincash:1qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', // invalid cash address txid: '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58', vout: 0, isValid: false, wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', }, ]; let thrownError; try { getChangeAddressFromInputUtxos(invalidInputUtxo, wallet); } catch (err) { thrownError = err; } expect(thrownError.message).toStrictEqual('Invalid input utxo'); }); it(`getChangeAddressFromInputUtxos() throws an error upon a null inputUtxos param`, () => { const { wallet } = sendBCHMock; const inputUtxo = null; let thrownError; try { getChangeAddressFromInputUtxos(inputUtxo, wallet); } catch (err) { thrownError = err; } expect(thrownError.message).toStrictEqual( 'Invalid getChangeAddressFromWallet input parameter', ); }); it(`parseXecSendValue() correctly parses the value for a valid one to one send XEC transaction`, () => { expect(parseXecSendValue(false, '550', null)).toStrictEqual( new BigNumber(550), ); }); it(`parseXecSendValue() correctly parses the value for a valid one to many send XEC transaction`, () => { const destinationAddressAndValueArray = [ 'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,6', 'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,6', 'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,6', ]; expect( parseXecSendValue(true, null, destinationAddressAndValueArray), ).toStrictEqual(new BigNumber(18)); }); it(`parseXecSendValue() correctly throws error when singleSendValue is invalid for a one to one send XEC transaction`, () => { let errorThrown; try { parseXecSendValue(false, null, 550); } catch (err) { errorThrown = err; } expect(errorThrown.message).toStrictEqual('Invalid singleSendValue'); }); it(`parseXecSendValue() correctly throws error when destinationAddressAndValueArray is invalid for a one to many send XEC transaction`, () => { let errorThrown; try { parseXecSendValue(true, null, null); } catch (err) { errorThrown = err; } expect(errorThrown.message).toStrictEqual( 'Invalid destinationAddressAndValueArray', ); }); it(`parseXecSendValue() correctly throws error when the total value for a one to one send XEC transaction is below dust`, () => { let errorThrown; try { parseXecSendValue(false, '4.5', null); } catch (err) { errorThrown = err; } expect(errorThrown.message).toStrictEqual('dust'); }); it(`parseXecSendValue() correctly throws error when the total value for a one to many send XEC transaction is below dust`, () => { const destinationAddressAndValueArray = [ 'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,2', 'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,2', 'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,1', ]; let errorThrown; try { parseXecSendValue(true, null, destinationAddressAndValueArray); } catch (err) { errorThrown = err; } expect(errorThrown.message).toStrictEqual('dust'); }); it('generateOpReturnScript() correctly generates an encrypted message script', () => { const optionalOpReturnMsg = 'testing generateOpReturnScript()'; const encryptionFlag = true; const airdropFlag = false; const airdropTokenId = null; const mockEncryptedEj = '04688f9907fe3c7c0b78a73c4ab4f75e15e7e2b79641add519617086126fe6f6b1405a14eed48e90c9c8c0fc77f0f36984a78173e76ce51f0a44af94b59e9da703c9ff82758cfdb9cc46437d662423400fb731d3bfc1df0599279356ca261213fbb40d398c041e1bac966afed1b404581ab1bcfcde1fa039d53b7c7b70e8edf26d64bea9fbeed24cc80909796e6af5863707fa021f2a2ebaa2fe894904702be19d'; const encodedScript = generateOpReturnScript( optionalOpReturnMsg, encryptionFlag, airdropFlag, airdropTokenId, mockEncryptedEj, ); expect(encodedScript.toString('hex')).toBe( '6a04657461624d420130343638386639393037666533633763306237386137336334616234663735653135653765326237393634316164643531393631373038363132366665366636623134303561313465656434386539306339633863306663373766306633363938346137383137336537366365353166306134346166393462353965396461373033633966663832373538636664623963633436343337643636323432333430306662373331643362666331646630353939323739333536636132363132313366626234306433393863303431653162616339363661666564316234303435383161623162636663646531666130333964353362376337623730653865646632366436346265613966626565643234636338303930393739366536616635383633373037666130323166326132656261613266653839343930343730326265313964', ); }); it('generateOpReturnScript() correctly generates an un-encrypted non-airdrop message script', () => { const optionalOpReturnMsg = 'testing generateOpReturnScript()'; const encryptionFlag = false; const airdropFlag = false; const encodedScript = generateOpReturnScript( optionalOpReturnMsg, encryptionFlag, airdropFlag, ); expect(encodedScript.toString('hex')).toBe( '6a04007461622074657374696e672067656e65726174654f7052657475726e5363726970742829', ); }); it('generateOpReturnScript() correctly generates an un-encrypted airdrop message script', () => { const optionalOpReturnMsg = 'testing generateOpReturnScript()'; const encryptionFlag = false; const airdropFlag = true; const airdropTokenId = '1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e'; const encodedScript = generateOpReturnScript( optionalOpReturnMsg, encryptionFlag, airdropFlag, airdropTokenId, ); expect(encodedScript.toString('hex')).toBe( '6a0464726f70201c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e04007461622074657374696e672067656e65726174654f7052657475726e5363726970742829', ); }); it('generateOpReturnScript() correctly generates an un-encrypted airdrop with no message script', () => { const optionalOpReturnMsg = null; const encryptionFlag = false; const airdropFlag = true; const airdropTokenId = '1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e'; const encodedScript = generateOpReturnScript( optionalOpReturnMsg, encryptionFlag, airdropFlag, airdropTokenId, ); expect(encodedScript.toString('hex')).toBe( '6a0464726f70201c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e0400746162', ); }); it('generateOpReturnScript() correctly throws an error on an invalid encryption input', () => { const optionalOpReturnMsg = null; const encryptionFlag = true; const airdropFlag = false; const airdropTokenId = null; const mockEncryptedEj = null; // invalid given encryptionFlag is true let thrownError; try { generateOpReturnScript( optionalOpReturnMsg, encryptionFlag, airdropFlag, airdropTokenId, mockEncryptedEj, ); } catch (err) { thrownError = err; } expect(thrownError.message).toStrictEqual('Invalid OP RETURN script input'); }); it('generateOpReturnScript() correctly throws an error on an invalid airdrop input', () => { const optionalOpReturnMsg = null; const encryptionFlag = false; const airdropFlag = true; const airdropTokenId = null; // invalid given airdropFlag is true let thrownError; try { generateOpReturnScript( optionalOpReturnMsg, encryptionFlag, airdropFlag, airdropTokenId, ); } catch (err) { thrownError = err; } expect(thrownError.message).toStrictEqual('Invalid OP RETURN script input'); }); it(`generateTokenTxInput() returns a valid object for a valid create token tx`, async () => { let txBuilder = new TransactionBuilder(); const tokenId = '1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e'; const tokenInputObj = generateTokenTxInput( 'GENESIS', mockNonSlpUtxos, null, // no slpUtxos used for genesis tx tokenId, null, // no token send/burn amount for genesis tx currency.defaultFee, txBuilder, ); expect(tokenInputObj.inputXecUtxos).toStrictEqual( [mockNonSlpUtxos[0]].concat([mockNonSlpUtxos[1]]), ); expect(tokenInputObj.txBuilder.toString()).toStrictEqual( mockCreateTokenTxBuilderObj.toString(), ); expect(tokenInputObj.remainderXecValue).toStrictEqual( new BigNumber(699702), // remainder = tokenInputObj.inputXecUtxos - currency.etokenSats - txFee ); }); it(`generateTokenTxInput() returns a valid object for a valid send token tx`, async () => { let txBuilder = new TransactionBuilder(); const tokenId = mockSlpUtxos[0].tokenId; const tokenInputObj = generateTokenTxInput( 'SEND', mockNonSlpUtxos, mockSlpUtxos, tokenId, new BigNumber(500), // sending 500 of these tokens currency.defaultFee, txBuilder, ); expect(tokenInputObj.inputTokenUtxos).toStrictEqual( [mockSlpUtxos[0]].concat([mockSlpUtxos[1]]), // mockSlpUtxos[0] 400 + mockSlpUtxos[1] 6500 ); expect(tokenInputObj.remainderTokenValue).toStrictEqual( new BigNumber(6400), // token change is mockSlpUtxos[0] 400 + mockSlpUtxos[1] 6500 - [tokenAmount] 500 ); expect(tokenInputObj.txBuilder.toString()).toStrictEqual( mockSendTokenTxBuilderObj.toString(), ); }); it(`generateTokenTxInput() returns a valid object for a valid burn token tx`, async () => { let txBuilder = new TransactionBuilder(); const tokenId = mockSlpUtxos[0].tokenId; const tokenInputObj = generateTokenTxInput( 'BURN', mockNonSlpUtxos, mockSlpUtxos, tokenId, new BigNumber(500), // burning 500 of these tokens currency.defaultFee, txBuilder, ); expect(tokenInputObj.inputTokenUtxos).toStrictEqual( [mockSlpUtxos[0]].concat([mockSlpUtxos[1]]), // mockSlpUtxos[0] 400 + mockSlpUtxos[1] 6500 ); expect(tokenInputObj.remainderTokenValue).toStrictEqual( new BigNumber(6400), // token change is mockSlpUtxos[0] 400 + mockSlpUtxos[1] 6500 - [tokenAmount] 500 ); expect(tokenInputObj.txBuilder.toString()).toStrictEqual( mockBurnTokenTxBuilderObj.toString(), ); }); it(`generateTokenTxOutput() returns a valid object for a valid create token tx`, async () => { let txBuilder = new TransactionBuilder(); const { configObj, wallet } = createTokenMock; const tokenSenderCashAddress = wallet.Path1899.cashAddress; const tokenOutputObj = generateTokenTxOutput( txBuilder, 'GENESIS', tokenSenderCashAddress, null, // optional, for SEND or BURN amount new BigNumber(500), // remainder XEC value configObj, ); expect(tokenOutputObj.toString()).toStrictEqual( mockCreateTokenOutputsTxBuilderObj.toString(), ); }); it(`generateTokenTxOutput() returns a valid object for a valid send token tx`, async () => { let txBuilder = new TransactionBuilder(); const { wallet } = createTokenMock; const tokenSenderCashAddress = wallet.Path1899.cashAddress; const tokenRecipientTokenAddress = wallet.Path1899.slpAddress; const tokenOutputObj = generateTokenTxOutput( txBuilder, 'SEND', tokenSenderCashAddress, mockSlpUtxos, new BigNumber(500), // remainder XEC value null, // only for genesis tx tokenRecipientTokenAddress, // recipient token address new BigNumber(50), ); expect(tokenOutputObj.toString()).toStrictEqual( mockSendTokenOutputsTxBuilderObj.toString(), ); }); it(`generateTokenTxOutput() returns a valid object for a valid burn token tx`, async () => { let txBuilder = new TransactionBuilder(); const { wallet } = createTokenMock; const tokenSenderCashAddress = wallet.Path1899.cashAddress; const tokenOutputObj = generateTokenTxOutput( txBuilder, 'BURN', tokenSenderCashAddress, mockSlpUtxos, new BigNumber(500), // remainder XEC value null, // only for genesis tx null, // no token recipients for burn tx new BigNumber(50), ); expect(tokenOutputObj.toString()).toStrictEqual( mockBurnTokenOutputsTxBuilderObj.toString(), ); }); it(`generateTxInput() returns an input object for a valid one to one XEC tx`, async () => { const isOneToMany = false; const utxos = mockNonSlpUtxos; let txBuilder = new TransactionBuilder(); const destinationAddressAndValueArray = null; const satoshisToSend = new BigNumber(2184); const feeInSatsPerByte = currency.defaultFee; const inputObj = generateTxInput( isOneToMany, utxos, txBuilder, destinationAddressAndValueArray, satoshisToSend, feeInSatsPerByte, ); expect(inputObj.txBuilder).not.toStrictEqual(null); expect(inputObj.totalInputUtxoValue).toStrictEqual(new BigNumber(701000)); expect(inputObj.txFee).toStrictEqual(752); expect(inputObj.inputUtxos.length).not.toStrictEqual(0); }); it(`generateTxInput() returns an input object for a valid one to many XEC tx`, async () => { const isOneToMany = true; const utxos = mockNonSlpUtxos; let txBuilder = new TransactionBuilder(); const destinationAddressAndValueArray = [ 'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,3000', 'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,3000', 'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,3000', ]; const satoshisToSend = new BigNumber(900000); const feeInSatsPerByte = currency.defaultFee; const inputObj = generateTxInput( isOneToMany, utxos, txBuilder, destinationAddressAndValueArray, satoshisToSend, feeInSatsPerByte, ); expect(inputObj.txBuilder).not.toStrictEqual(null); expect(inputObj.totalInputUtxoValue).toStrictEqual(new BigNumber(1401000)); expect(inputObj.txFee).toStrictEqual(1186); expect(inputObj.inputUtxos.length).not.toStrictEqual(0); }); it(`generateTxInput() throws error for a one to many XEC tx with invalid destinationAddressAndValueArray input`, async () => { const isOneToMany = true; const utxos = mockNonSlpUtxos; let txBuilder = new TransactionBuilder(); const destinationAddressAndValueArray = null; // invalid since isOneToMany is true const satoshisToSend = new BigNumber(900000); const feeInSatsPerByte = currency.defaultFee; let thrownError; try { generateTxInput( isOneToMany, utxos, txBuilder, destinationAddressAndValueArray, satoshisToSend, feeInSatsPerByte, ); } catch (err) { thrownError = err; } expect(thrownError.message).toStrictEqual('Invalid tx input parameter'); }); it(`generateTxInput() throws error for a one to many XEC tx with invalid utxos input`, async () => { const isOneToMany = true; const utxos = null; let txBuilder = new TransactionBuilder(); const destinationAddressAndValueArray = [ 'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,3000', 'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,3000', 'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,3000', ]; const satoshisToSend = new BigNumber(900000); const feeInSatsPerByte = currency.defaultFee; let thrownError; try { generateTxInput( isOneToMany, utxos, txBuilder, destinationAddressAndValueArray, satoshisToSend, feeInSatsPerByte, ); } catch (err) { thrownError = err; } expect(thrownError.message).toStrictEqual('Invalid tx input parameter'); }); it(`generateTxOutput() returns a txBuilder instance for a valid one to one XEC tx`, () => { // txbuilder output params const { destinationAddress, wallet } = sendBCHMock; const isOneToMany = false; const singleSendValue = fromSatoshisToXec( mockOneToOneSendXecTxBuilderObj.transaction.tx.outs[0].value, ); const totalInputUtxoValue = mockOneToOneSendXecTxBuilderObj.transaction.inputs[0].value; const satoshisToSend = fromXecToSatoshis(new BigNumber(singleSendValue)); // for unit test purposes, calculate fee by subtracting satoshisToSend from totalInputUtxoValue // no change output to be subtracted in this tx const txFee = new BigNumber(totalInputUtxoValue).minus( new BigNumber(satoshisToSend), ); const destinationAddressAndValueArray = null; let txBuilder = new TransactionBuilder(); const changeAddress = wallet.Path1899.cashAddress; const outputObj = generateTxOutput( isOneToMany, singleSendValue, satoshisToSend, totalInputUtxoValue, destinationAddress, destinationAddressAndValueArray, changeAddress, txFee, txBuilder, ); expect(outputObj.toString()).toStrictEqual( mockOneToOneSendXecTxBuilderObj.toString(), ); }); it(`generateTxOutput() returns a txBuilder instance for a valid one to many XEC tx`, () => { // txbuilder output params const { destinationAddress, wallet } = sendBCHMock; const isOneToMany = true; const singleSendValue = null; const totalInputUtxoValue = mockOneToManySendXecTxBuilderObj.transaction.inputs[0].value + mockOneToManySendXecTxBuilderObj.transaction.inputs[1].value + mockOneToManySendXecTxBuilderObj.transaction.inputs[2].value; const satoshisToSend = new BigNumber( mockOneToManySendXecTxBuilderObj.transaction.tx.outs[0].value + mockOneToManySendXecTxBuilderObj.transaction.tx.outs[1].value + mockOneToManySendXecTxBuilderObj.transaction.tx.outs[2].value, ); // for unit test purposes, calculate fee by subtracting satoshisToSend and change amount from totalInputUtxoValue const txFee = new BigNumber(totalInputUtxoValue) .minus(satoshisToSend) .minus( new BigNumber( mockOneToManySendXecTxBuilderObj.transaction.tx.outs[3].value, ), ); // change value const destinationAddressAndValueArray = validAddressArrayInput; let txBuilder = new TransactionBuilder(); const changeAddress = wallet.Path1899.cashAddress; const outputObj = generateTxOutput( isOneToMany, singleSendValue, satoshisToSend, totalInputUtxoValue, destinationAddress, destinationAddressAndValueArray, changeAddress, txFee, txBuilder, ); expect(outputObj.toString()).toStrictEqual( mockOneToManySendXecTxBuilderObj.toString(), ); }); it(`generateTxOutput() throws an error on invalid input params for a one to one XEC tx`, () => { // txbuilder output params const { wallet } = sendBCHMock; const isOneToMany = false; const singleSendValue = null; // invalid due to singleSendValue being mandatory when isOneToMany is false const totalInputUtxoValue = mockOneToOneSendXecTxBuilderObj.transaction.inputs[0].value; const satoshisToSend = fromXecToSatoshis(new BigNumber(singleSendValue)); // for unit test purposes, calculate fee by subtracting satoshisToSend from totalInputUtxoValue // no change output to be subtracted in this tx const txFee = new BigNumber(totalInputUtxoValue).minus(satoshisToSend); const destinationAddressAndValueArray = null; let txBuilder = new TransactionBuilder(); const changeAddress = wallet.Path1899.cashAddress; let thrownError; try { generateTxOutput( isOneToMany, singleSendValue, satoshisToSend, totalInputUtxoValue, null, destinationAddressAndValueArray, changeAddress, txFee, txBuilder, ); } catch (err) { thrownError = err; } expect(thrownError.message).toStrictEqual('Invalid tx input parameter'); }); it(`generateTxOutput() throws an error on invalid input params for a one to many XEC tx`, () => { // txbuilder output params const { wallet } = sendBCHMock; const isOneToMany = true; const singleSendValue = null; const totalInputUtxoValue = mockOneToManySendXecTxBuilderObj.transaction.inputs[0].value + mockOneToManySendXecTxBuilderObj.transaction.inputs[1].value + mockOneToManySendXecTxBuilderObj.transaction.inputs[2].value; const satoshisToSend = new BigNumber( mockOneToManySendXecTxBuilderObj.transaction.tx.outs[0].value + mockOneToManySendXecTxBuilderObj.transaction.tx.outs[1].value + mockOneToManySendXecTxBuilderObj.transaction.tx.outs[2].value, ); // for unit test purposes, calculate fee by subtracting satoshisToSend and change amount from totalInputUtxoValue const txFee = new BigNumber(totalInputUtxoValue) .minus(satoshisToSend) .minus( new BigNumber( mockOneToManySendXecTxBuilderObj.transaction.tx.outs[3].value, ), ); // change value const destinationAddressAndValueArray = null; // invalid as this is mandatory when isOneToMany is true let txBuilder = new TransactionBuilder(); const changeAddress = wallet.Path1899.cashAddress; let thrownError; try { generateTxOutput( isOneToMany, singleSendValue, satoshisToSend, totalInputUtxoValue, null, destinationAddressAndValueArray, changeAddress, txFee, txBuilder, ); } catch (err) { thrownError = err; } expect(thrownError.message).toStrictEqual('Invalid tx input parameter'); }); it(`signAndBuildTx() successfully returns a raw tx hex for a tx with a single input and a single output`, () => { // txbuilder output params let txBuilder = new TransactionBuilder(); const { wallet } = sendBCHMock; // add inputs to txBuilder txBuilder.addInput( mockSingleInputUtxo[0].txid, mockSingleInputUtxo[0].vout, ); // add outputs to txBuilder const outputAddressAndValue = mockSingleOutput.split(','); txBuilder.addOutput( outputAddressAndValue[0], // address parseInt(fromXecToSatoshis(new BigNumber(outputAddressAndValue[1]))), // value ); const rawTxHex = signAndBuildTx(mockSingleInputUtxo, txBuilder, wallet); expect(rawTxHex).toStrictEqual( '0200000001582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006b483045022100b4ee5268cb64c4f097e739df7c6934d1df7e75a4f217d5824db18ae2e12554b102204faf039738181aae80c064b928b3d8079a82cdb080ce9a2d5453939a588f4372412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff0158020000000000001976a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac00000000', ); }); it(`signAndBuildTx() successfully returns a raw tx hex for a tx with a single input and multiple outputs`, () => { // txbuilder output params let txBuilder = new TransactionBuilder(); const { wallet } = sendBCHMock; // add inputs to txBuilder txBuilder.addInput( mockSingleInputUtxo[0].txid, mockSingleInputUtxo[0].vout, ); // add outputs to txBuilder for (let i = 0; i < mockMultipleOutputs.length; i++) { const outputAddressAndValue = mockMultipleOutputs[i].split(','); txBuilder.addOutput( outputAddressAndValue[0], // address parseInt( fromXecToSatoshis(new BigNumber(outputAddressAndValue[1])), ), // value ); } const rawTxHex = signAndBuildTx(mockSingleInputUtxo, txBuilder, wallet); expect(rawTxHex).toStrictEqual( '0200000001582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006b483045022100df29734c4fb348b0e8b613ce522c10c5ac14cb3ecd32843dc7fcf004d60f1b8a022023c4ae02b38c7272e29f344902ae2afa4db1ec37d582a31c16650a0abc4f480c412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff0326020000000000001976a914f627e51001a51a1a92d8927808701373cf29267f88ac26020000000000001976a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac26020000000000001976a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac00000000', ); }); it(`signAndBuildTx() successfully returns a raw tx hex for a tx with multiple inputs and a single output`, () => { // txbuilder output params let txBuilder = new TransactionBuilder(); const { wallet } = sendBCHMock; // add inputs to txBuilder for (let i = 0; i < mockMultipleInputUtxos.length; i++) { txBuilder.addInput( mockMultipleInputUtxos[i].txid, mockMultipleInputUtxos[i].vout, ); } // add outputs to txBuilder const outputAddressAndValue = mockSingleOutput.split(','); txBuilder.addOutput( outputAddressAndValue[0], // address parseInt(fromXecToSatoshis(new BigNumber(outputAddressAndValue[1]))), // value ); const rawTxHex = signAndBuildTx(mockMultipleInputUtxos, txBuilder, wallet); expect(rawTxHex).toStrictEqual( '0200000003582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006a4730440220541366dd5ea25d65d3044dbde16fc6118ab1aee07c7d0d4c25c9e8aa299f040402203ed2f540948197d4c6a4ae963ad187d145a9fb339e311317b03c6172732e267b412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff7313e804af08113dfa290515390a8ec3ac01448118f2eb556ee168a96ee6acdd000000006b483045022100c1d02c5023f83b87a4f2dd26a7306ed9be9d53ab972bd935b440e45eb54a304302200b99aa2f1a728b3bb1dcbff80742c5fcab991bb74e80fa231255a31d58a6ff7d412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff960dd2f0c47e8a3cf1486b046d879f45a047da3b51aedfb5594138ac857214f1000000006b483045022100bd24d11d7070988848cb4aa2b10748aa0aeb79dc8af39c1f22dc1034b3121e5f02201491026e5f8f6eb566eb17cb195e3da3ff0d9cf01bdd34c944964d33a8d3b1ad412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff0158020000000000001976a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac00000000', ); }); it(`signAndBuildTx() successfully returns a raw tx hex for a tx with multiple inputs and multiple outputs`, () => { // txbuilder output params let txBuilder = new TransactionBuilder(); const { wallet } = sendBCHMock; // add inputs to txBuilder for (let i = 0; i < mockMultipleInputUtxos.length; i++) { txBuilder.addInput( mockMultipleInputUtxos[i].txid, mockMultipleInputUtxos[i].vout, ); } // add outputs to txBuilder for (let i = 0; i < mockMultipleOutputs.length; i++) { const outputAddressAndValue = mockMultipleOutputs[i].split(','); txBuilder.addOutput( outputAddressAndValue[0], // address parseInt( fromXecToSatoshis(new BigNumber(outputAddressAndValue[1])), ), // value ); } const rawTxHex = signAndBuildTx(mockMultipleInputUtxos, txBuilder, wallet); expect(rawTxHex).toStrictEqual( '0200000003582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006a47304402203de4e6a512a6bec1d378b6444008484e1be5a0c621dc4b201d67addefffe864602202daf82e76b7594fe1ab54a49380c6b1226ab65551ae6ab9164216b66266f34a1412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff7313e804af08113dfa290515390a8ec3ac01448118f2eb556ee168a96ee6acdd000000006a473044022029f5fcbc9356beb9eae6b9ff9a479e8c8331b95406b6be456fccf9d90f148ea1022028f4e7fa7234f9429535360c8f5dad303e2c5044431615997861b10f26fa8a88412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff960dd2f0c47e8a3cf1486b046d879f45a047da3b51aedfb5594138ac857214f1000000006a473044022049a67738d99006b3523cff818f3626104cf5106bd463be70d22ad179a8cb403b022025829baf67f964202ea77ea7462a5447e32415e7293cdee382ea7ae9374364e8412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff0326020000000000001976a914f627e51001a51a1a92d8927808701373cf29267f88ac26020000000000001976a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac26020000000000001976a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac00000000', ); }); it(`signAndBuildTx() throws error on an empty inputUtxo param`, () => { // txbuilder output params let txBuilder = new TransactionBuilder(); const { wallet } = sendBCHMock; let thrownError; try { signAndBuildTx([], txBuilder, wallet); } catch (err) { thrownError = err; } expect(thrownError.message).toStrictEqual('Invalid buildTx parameter'); }); it(`signAndBuildTx() throws error on a null inputUtxo param`, () => { // txbuilder output params let txBuilder = new TransactionBuilder(); const inputUtxo = null; // invalid input param const { wallet } = sendBCHMock; let thrownError; try { signAndBuildTx(inputUtxo, txBuilder, wallet); } catch (err) { thrownError = err; } expect(thrownError.message).toStrictEqual('Invalid buildTx parameter'); }); describe('Correctly executes cash utility functions', () => { it(`Correctly converts smallest base unit to smallest decimal for cashDecimals = 2`, () => { expect(fromSatoshisToXec(1, 2)).toStrictEqual(new BigNumber(0.01)); }); it(`Correctly converts largest base unit to smallest decimal for cashDecimals = 2`, () => { expect(fromSatoshisToXec(1000000012345678, 2)).toStrictEqual( new BigNumber(10000000123456.78), ); }); it(`Correctly converts smallest base unit to smallest decimal for cashDecimals = 8`, () => { expect(fromSatoshisToXec(1, 8)).toStrictEqual( new BigNumber(0.00000001), ); }); it(`Correctly converts largest base unit to smallest decimal for cashDecimals = 8`, () => { expect(fromSatoshisToXec(1000000012345678, 8)).toStrictEqual( new BigNumber(10000000.12345678), ); }); it(`Accepts a cachedWalletState that has not preserved BigNumber object types, and returns the same wallet state with BigNumber type re-instituted`, () => { expect(loadStoredWallet(cachedUtxos)).toStrictEqual( utxosLoadedFromCache, ); }); it(`Correctly determines a wallet's balance from its set of non-eToken utxos (nonSlpUtxos)`, () => { expect( getWalletBalanceFromUtxos( validStoredWallet.state.slpBalancesAndUtxos.nonSlpUtxos, ), ).toStrictEqual(validStoredWallet.state.balances); }); it(`Correctly determines a wallet's zero balance from its empty set of non-eToken utxos (nonSlpUtxos)`, () => { expect( getWalletBalanceFromUtxos( utxosLoadedFromCache.slpBalancesAndUtxos.nonSlpUtxos, ), ).toStrictEqual(utxosLoadedFromCache.balances); }); - it(`Recognizes a stored wallet as valid if it has all required fields`, () => { + it(`Recognizes a stored wallet as valid if it has all required fields prior to 20221123 updated format`, () => { expect(isValidStoredWallet(validStoredWallet)).toBe(true); }); + it(`Recognizes a stored wallet as valid if it has all required fields in 20221123 updated format`, () => { + expect( + isValidStoredWallet(validStoredWalletAfter20221123Streamline), + ).toBe(true); + }); it(`Recognizes a stored wallet as invalid if it is missing required fields`, () => { expect(isValidStoredWallet(invalidStoredWallet)).toBe(false); }); it(`Recognizes a stored wallet as invalid if it includes hydratedUtxoDetails in the state field`, () => { expect(isValidStoredWallet(invalidpreChronikStoredWallet)).toBe(false); }); it(`Converts a legacy BCH amount to an XEC amount`, () => { expect(fromLegacyDecimals(0.00000546, 2)).toStrictEqual(5.46); }); it(`Leaves a legacy BCH amount unchanged if currency.cashDecimals is 8`, () => { expect(fromLegacyDecimals(0.00000546, 8)).toStrictEqual(0.00000546); }); it(`convertToEcashPrefix converts a bitcoincash: prefixed address to an ecash: prefixed address`, () => { expect( convertToEcashPrefix( 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', ), ).toBe('ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035'); }); it(`convertToEcashPrefix returns an ecash: prefix address unchanged`, () => { expect( convertToEcashPrefix( 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', ), ).toBe('ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035'); }); it(`Recognizes a wallet with missing Path1889 is a Legacy Wallet and requires migration`, () => { expect(isLegacyMigrationRequired(missingPath1899Wallet)).toBe(true); }); it(`Recognizes a wallet with missing PublicKey in Path1889 is a Legacy Wallet and requires migration`, () => { expect( isLegacyMigrationRequired(missingPublicKeyInPath1899Wallet), ).toBe(true); }); it(`Recognizes a wallet with missing PublicKey in Path145 is a Legacy Wallet and requires migration`, () => { expect(isLegacyMigrationRequired(missingPublicKeyInPath145Wallet)).toBe( true, ); }); it(`Recognizes a wallet with missing PublicKey in Path245 is a Legacy Wallet and requires migration`, () => { expect(isLegacyMigrationRequired(missingPublicKeyInPath245Wallet)).toBe( true, ); }); it(`Recognizes a wallet with missing Hash160 values is a Legacy Wallet and requires migration`, () => { expect(isLegacyMigrationRequired(missingHash160)).toBe(true); }); it(`Recognizes a latest, current wallet that does not require migration`, () => { expect(isLegacyMigrationRequired(notLegacyWallet)).toBe(false); }); test('toHash160() converts a valid bitcoincash: prefix address to a hash160', () => { const result = toHash160( 'bitcoincash:qq9h6d0a5q65fgywv4ry64x04ep906mdku7ymranw3', ); expect(result).toStrictEqual( '0b7d35fda03544a08e65464d54cfae4257eb6db7', ); }); test('toHash160 throws error if input address is an invalid bitcoincash: address', () => { const address = 'bitcoincash:qqd3qnINVALIDDDDDDDDDza25m'; let errorThrown; try { toHash160(address); } catch (err) { errorThrown = err.message; } expect(errorThrown).toStrictEqual('Invalid address: ' + address + '.'); }); test('toHash160() converts a valid ecash: prefix address to a hash160', () => { const result = toHash160( 'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx', ); expect(result).toStrictEqual( '0b7d35fda03544a08e65464d54cfae4257eb6db7', ); }); test('toHash160 throws error if input address is an invalid ecash address', () => { const address = 'ecash:qqd3qn4zINVALIDDDDDtfza25m'; let errorThrown; try { toHash160(address); } catch (err) { errorThrown = err.message; } expect(errorThrown).toStrictEqual('Invalid address: ' + address + '.'); }); test('toHash160() converts a valid etoken: address to a hash160', () => { const result = toHash160( 'etoken:qq9h6d0a5q65fgywv4ry64x04ep906mdkufhx2swv3', ); expect(result).toStrictEqual( '0b7d35fda03544a08e65464d54cfae4257eb6db7', ); }); test('toHash160 throws error if input address is an invalid etoken: address', () => { const address = 'etoken:qq9h6d0a5INVALIDDDDDDx2swv3'; let errorThrown; try { toHash160(address); } catch (err) { errorThrown = err.message; } expect(errorThrown).toStrictEqual('Invalid address: ' + address + '.'); }); test('toHash160() converts a valid simpleledger: address to a hash160', () => { const result = toHash160( 'simpleledger:qq9h6d0a5q65fgywv4ry64x04ep906mdkujlscgns0', ); expect(result).toStrictEqual( '0b7d35fda03544a08e65464d54cfae4257eb6db7', ); }); test('toHash160 throws error if input address is an invalid simpleledger: address', () => { const address = 'simpleledger:qq9h6d0a5qINVALIDDDjlscgns0'; let errorThrown; try { toHash160(address); } catch (err) { errorThrown = err.message; } expect(errorThrown).toStrictEqual('Invalid address: ' + address + '.'); }); test('parseOpReturn() successfully parses a short cashtab message', async () => { const result = parseOpReturn(shortCashtabMessageInputHex); expect(result).toStrictEqual(mockParsedShortCashtabMessageArray); }); test('parseOpReturn() successfully parses a long cashtab message where an additional PUSHDATA1 is present', async () => { const result = parseOpReturn(longCashtabMessageInputHex); expect(result).toStrictEqual(mockParsedLongCashtabMessageArray); }); test('parseOpReturn() successfully parses a short external message', async () => { const result = parseOpReturn(shortExternalMessageInputHex); expect(result).toStrictEqual(mockParsedShortExternalMessageArray); }); test('parseOpReturn() successfully parses a long external message where an additional PUSHDATA1 is present', async () => { const result = parseOpReturn(longExternalMessageInputHex); expect(result).toStrictEqual(mockParsedLongExternalMessageArray); }); test('parseOpReturn() successfully parses an external message that is segmented into separate short parts', async () => { const result = parseOpReturn(shortSegmentedExternalMessageInputHex); expect(result).toStrictEqual( mockParsedShortSegmentedExternalMessageArray, ); }); test('parseOpReturn() successfully parses an external message that is segmented into separate long parts', async () => { const result = parseOpReturn(longSegmentedExternalMessageInputHex); expect(result).toStrictEqual( mockParsedLongSegmentedExternalMessageArray, ); }); test('parseOpReturn() successfully parses an external message that is segmented into separate long and short parts', async () => { const result = parseOpReturn(mixedSegmentedExternalMessageInputHex); expect(result).toStrictEqual( mockParsedMixedSegmentedExternalMessageArray, ); }); test('parseOpReturn() successfully parses an eToken output', async () => { const result = parseOpReturn(eTokenInputHex); expect(result).toStrictEqual(mockParsedETokenOutputArray); }); test('parseOpReturn() successfully parses an airdrop transaction', async () => { const result = parseOpReturn(mockAirdropHexOutput); // verify the hex output is parsed correctly expect(result).toStrictEqual(mockParsedAirdropMessageArray); // verify airdrop hex prefix is contained in the array returned from parseOpReturn() expect( result.find( element => element === currency.opReturn.appPrefixesHex.airdrop, ), ).toStrictEqual(currency.opReturn.appPrefixesHex.airdrop); }); test('convertEtokenToEcashAddr successfully converts a valid eToken address to eCash', async () => { const result = convertEtokenToEcashAddr( 'etoken:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs', ); expect(result).toStrictEqual( 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8', ); }); test('convertEtokenToEcashAddr successfully converts prefixless eToken address as input', async () => { const result = convertEtokenToEcashAddr( 'qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs', ); expect(result).toStrictEqual( 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8', ); }); test('convertEtokenToEcashAddr throws error with an invalid eToken address as input', async () => { const result = convertEtokenToEcashAddr('etoken:qpj9gcldpffcs'); expect(result).toStrictEqual( new Error( 'cashMethods.convertToEcashAddr() error: etoken:qpj9gcldpffcs is not a valid etoken address', ), ); }); test('convertEtokenToEcashAddr throws error with an ecash address as input', async () => { const result = convertEtokenToEcashAddr( 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8', ); expect(result).toStrictEqual( new Error( 'cashMethods.convertToEcashAddr() error: ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8 is not a valid etoken address', ), ); }); test('convertEtokenToEcashAddr throws error with null input', async () => { const result = convertEtokenToEcashAddr(null); expect(result).toStrictEqual( new Error( 'cashMethods.convertToEcashAddr() error: No etoken address provided', ), ); }); test('convertEtokenToEcashAddr throws error with empty string input', async () => { const result = convertEtokenToEcashAddr(''); expect(result).toStrictEqual( new Error( 'cashMethods.convertToEcashAddr() error: No etoken address provided', ), ); }); test('convertEcashtoEtokenAddr successfully converts a valid ecash address into an etoken address', async () => { const eCashAddress = 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8'; const eTokenAddress = 'etoken:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs'; const result = convertEcashtoEtokenAddr(eCashAddress); expect(result).toStrictEqual(eTokenAddress); }); test('convertEcashtoEtokenAddr successfully converts a valid prefix-less ecash address into an etoken address', async () => { const eCashAddress = 'qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8'; const eTokenAddress = 'etoken:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs'; const result = convertEcashtoEtokenAddr(eCashAddress); expect(result).toStrictEqual(eTokenAddress); }); test('convertEcashtoEtokenAddr throws error with invalid ecash address input', async () => { const eCashAddress = 'ecash:qpaNOTVALIDADDRESSwu8'; const result = convertEcashtoEtokenAddr(eCashAddress); expect(result).toStrictEqual( new Error(eCashAddress + ' is not a valid ecash address'), ); }); test('convertEcashtoEtokenAddr throws error with a valid etoken address input', async () => { const eTokenAddress = 'etoken:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs'; const result = convertEcashtoEtokenAddr(eTokenAddress); expect(result).toStrictEqual( new Error(eTokenAddress + ' is not a valid ecash address'), ); }); test('convertEcashtoEtokenAddr throws error with a valid bitcoincash address input', async () => { const bchAddress = 'bitcoincash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9g0vsgy56s'; const result = convertEcashtoEtokenAddr(bchAddress); expect(result).toStrictEqual( new Error(bchAddress + ' is not a valid ecash address'), ); }); test('convertEcashtoEtokenPrefix throws error with null ecash address input', async () => { const eCashAddress = null; const result = convertEcashtoEtokenAddr(eCashAddress); expect(result).toStrictEqual( new Error(eCashAddress + ' is not a valid ecash address'), ); }); it(`flattenContactList flattens contactList array by returning an array of addresses`, () => { expect( flattenContactList([ { address: 'ecash:qpdkc5p7f25hwkxsr69m3evlj4h7wqq9xcgmjc8sxr', name: 'Alpha', }, { address: 'ecash:qpq235n3l3u6ampc8slapapnatwfy446auuv64ylt2', name: 'Beta', }, { address: 'ecash:qz50e58nkeg2ej2f34z6mhwylp6ven8emy8pp52r82', name: 'Gamma', }, ]), ).toStrictEqual([ 'ecash:qpdkc5p7f25hwkxsr69m3evlj4h7wqq9xcgmjc8sxr', 'ecash:qpq235n3l3u6ampc8slapapnatwfy446auuv64ylt2', 'ecash:qz50e58nkeg2ej2f34z6mhwylp6ven8emy8pp52r82', ]); }); it(`flattenContactList flattens contactList array of length 1 by returning an array of 1 address`, () => { expect( flattenContactList([ { address: 'ecash:qpdkc5p7f25hwkxsr69m3evlj4h7wqq9xcgmjc8sxr', name: 'Alpha', }, ]), ).toStrictEqual(['ecash:qpdkc5p7f25hwkxsr69m3evlj4h7wqq9xcgmjc8sxr']); }); it(`flattenContactList returns an empty array for invalid input`, () => { expect(flattenContactList(false)).toStrictEqual([]); }); it(`getHashArrayFromWallet returns false for a legacy wallet`, () => { expect( getHashArrayFromWallet(mockLegacyWallets.legacyAlphaMainnet), ).toBe(false); }); it(`Successfully extracts a hash160 array from a migrated wallet object`, () => { expect( getHashArrayFromWallet( mockLegacyWallets.migratedLegacyAlphaMainnet, ), ).toStrictEqual([ '960c9ed561f1699f0c49974d50b3bb7cdc118625', '2be0e0c999e7e77a443ea726f82c441912fca92b', 'ba8257db65f40359989c7b894c5e88ed7b6344f6', ]); }); it(`isActiveWebsocket returns true for an active chronik websocket connection`, () => { expect(isActiveWebsocket(activeWebsocketAlpha)).toBe(true); }); it(`isActiveWebsocket returns false for a disconnected chronik websocket connection`, () => { expect(isActiveWebsocket(disconnectedWebsocketAlpha)).toBe(false); }); it(`isActiveWebsocket returns false for a null chronik websocket connection`, () => { expect(isActiveWebsocket(null)).toBe(false); }); it(`isActiveWebsocket returns false for an active websocket connection with no subscriptions`, () => { expect(isActiveWebsocket(unsubscribedWebsocket)).toBe(false); }); it(`getCashtabByteCount for 2 inputs, 2 outputs returns the same value as BCH.BitcoinCash.getByteCount( { P2PKH: utxos.length }, { P2PKH: p2pkhOutputNumber }, );`, () => { const BCH = new BCHJS(); // 374 expect(getCashtabByteCount(2, 2)).toBe( BCH.BitcoinCash.getByteCount({ P2PKH: 2 }, { P2PKH: 2 }), ); }); it(`getCashtabByteCount for 1 input, 2 outputs returns the same value as BCH.BitcoinCash.getByteCount( { P2PKH: utxos.length }, { P2PKH: p2pkhOutputNumber }, );`, () => { const BCH = new BCHJS(); expect(getCashtabByteCount(1, 2)).toBe( BCH.BitcoinCash.getByteCount({ P2PKH: 1 }, { P2PKH: 2 }), ); }); it(`getCashtabByteCount for 173 input, 1 outputs returns the same value as BCH.BitcoinCash.getByteCount( { P2PKH: utxos.length }, { P2PKH: p2pkhOutputNumber }, );`, () => { const BCH = new BCHJS(); expect(getCashtabByteCount(173, 1)).toBe( BCH.BitcoinCash.getByteCount({ P2PKH: 173 }, { P2PKH: 1 }), ); }); it(`getCashtabByteCount for 1 input, 2000 outputs returns the same value as BCH.BitcoinCash.getByteCount( { P2PKH: utxos.length }, { P2PKH: p2pkhOutputNumber }, );`, () => { const BCH = new BCHJS(); expect(getCashtabByteCount(1, 2000)).toBe( BCH.BitcoinCash.getByteCount({ P2PKH: 1 }, { P2PKH: 2000 }), ); }); it('calculates fee correctly for 2 P2PKH outputs', () => { const utxosMock = [{}, {}]; expect(calcFee(utxosMock, 2, 1.01)).toBe(378); }); it(`Gets correct EC Pair from WIF`, () => { expect(JSON.stringify(getECPairFromWIF(mockWif))).toBe( mockStringifiedECPair, ); }); it(`Converts a hash160 to an ecash address`, () => { expect( hash160ToAddress('76458db0ed96fe9863fc1ccec9fa2cfab884b0f6'), ).toBe('ecash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj'); }); }); diff --git a/web/cashtab/src/utils/cashMethods.js b/web/cashtab/src/utils/cashMethods.js index 7aa4e9bcd..5a1a983fe 100644 --- a/web/cashtab/src/utils/cashMethods.js +++ b/web/cashtab/src/utils/cashMethods.js @@ -1,1159 +1,1158 @@ import { currency } from 'components/Common/Ticker'; import { isValidXecAddress, isValidEtokenAddress, isValidContactList, isValidBchAddress, } from 'utils/validation'; import BigNumber from 'bignumber.js'; import cashaddr from 'ecashaddrjs'; import bs58 from 'bs58'; import * as slpMdm from 'slp-mdm'; import Bitcoin from '@psf/bitcoincashjs-lib'; import coininfo from 'utils/coininfo'; // function is based on BCH-JS' generateBurnOpReturn() however it's been trimmed down for Cashtab use // Reference: https://github.com/Permissionless-Software-Foundation/bch-js/blob/62e56c832b35731880fe448269818b853c76dd80/src/slp/tokentype1.js#L217 export const generateBurnOpReturn = (tokenUtxos, burnQty) => { try { if (!tokenUtxos || !burnQty) { throw new Error('Invalid burn token parameter'); } // sendToken component already prevents burning of a value greater than the token utxo total held by the wallet const tokenId = tokenUtxos[0].tokenId; const decimals = tokenUtxos[0].decimals; // account for token decimals const finalBurnTokenQty = new BigNumber(burnQty).times(10 ** decimals); // Calculate the total amount of tokens owned by the wallet. const totalTokens = tokenUtxos.reduce( (tot, txo) => tot.plus(new BigNumber(txo.tokenQty).times(10 ** decimals)), new BigNumber(0), ); // calculate the token change const tokenChange = totalTokens.minus(finalBurnTokenQty); const tokenChangeStr = tokenChange.toString(); // Generate the burn OP_RETURN as a Buffer // No need for separate .send() calls for change and non-change burns as // nil change values do not generate token outputs as the full balance is burnt const script = slpMdm.TokenType1.send(tokenId, [ new slpMdm.BN(tokenChangeStr), ]); return script; } catch (err) { console.log('Error in generateBurnOpReturn(): ' + err); throw err; } }; // Function originally based on BCH-JS' generateSendOpReturn function however trimmed down for Cashtab // Reference: https://github.com/Permissionless-Software-Foundation/bch-js/blob/62e56c832b35731880fe448269818b853c76dd80/src/slp/tokentype1.js#L95 export const generateSendOpReturn = (tokenUtxos, sendQty) => { try { if (!tokenUtxos || !sendQty) { throw new Error('Invalid send token parameter'); } const tokenId = tokenUtxos[0].tokenId; const decimals = tokenUtxos[0].decimals; // account for token decimals const finalSendTokenQty = new BigNumber(sendQty).times(10 ** decimals); const finalSendTokenQtyStr = finalSendTokenQty.toString(); // Calculate the total amount of tokens owned by the wallet. const totalTokens = tokenUtxos.reduce( (tot, txo) => tot.plus(new BigNumber(txo.tokenQty).times(10 ** decimals)), new BigNumber(0), ); // calculate token change const tokenChange = totalTokens.minus(finalSendTokenQty); const tokenChangeStr = tokenChange.toString(); // When token change output is required let script, outputs; if (tokenChange > 0) { outputs = 2; // Generate the OP_RETURN as a Buffer. script = slpMdm.TokenType1.send(tokenId, [ new slpMdm.BN(finalSendTokenQtyStr), new slpMdm.BN(tokenChangeStr), ]); } else { // no token change needed outputs = 1; // Generate the OP_RETURN as a Buffer. script = slpMdm.TokenType1.send(tokenId, [ new slpMdm.BN(finalSendTokenQtyStr), ]); } return { script, outputs }; } catch (err) { console.log('Error in generateSendOpReturn(): ' + err); throw err; } }; // function is based on BCH-JS' generateGenesisOpReturn() however it's been trimmed down for Cashtab use // Reference: https://github.com/Permissionless-Software-Foundation/bch-js/blob/62e56c832b35731880fe448269818b853c76dd80/src/slp/tokentype1.js#L286 export const generateGenesisOpReturn = configObj => { try { if (!configObj) { throw new Error('Invalid token configuration'); } // adjust initial quantity for token decimals const initialQty = new BigNumber(configObj.initialQty) .times(10 ** configObj.decimals) .toString(); const script = slpMdm.TokenType1.genesis( configObj.ticker, configObj.name, configObj.documentUrl, configObj.documentHash, configObj.decimals, configObj.mintBatonVout, new slpMdm.BN(initialQty), ); return script; } catch (err) { console.log('Error in generateGenesisOpReturn(): ' + err); throw err; } }; export const getUtxoWif = (utxo, wallet) => { if (!wallet) { throw new Error('Invalid wallet parameter'); } const accounts = [wallet.Path245, wallet.Path145, wallet.Path1899]; const wif = accounts .filter(acc => acc.cashAddress === utxo.address) .pop().fundingWif; return wif; }; // Reference https://github.com/Permissionless-Software-Foundation/bch-js/blob/master/src/ecpair.js#L24 // Modified for mainnet only export const getECPairFromWIF = wif => { let xec = coininfo.bitcoincash.main; const xecBitcoinJSLib = xec.toBitcoinJS(); return Bitcoin.ECPair.fromWIF(wif, xecBitcoinJSLib); }; export const signUtxosByAddress = (inputUtxos, wallet, txBuilder) => { for (let i = 0; i < inputUtxos.length; i++) { const utxo = inputUtxos[i]; const accounts = [wallet.Path245, wallet.Path145, wallet.Path1899]; const wif = accounts .filter(acc => acc.cashAddress === utxo.address) .pop().fundingWif; const utxoECPair = getECPairFromWIF(wif); txBuilder.sign( i, utxoECPair, undefined, txBuilder.hashTypes.SIGHASH_ALL, parseInt(utxo.value), ); } return txBuilder; }; export const getCashtabByteCount = (p2pkhInputCount, p2pkhOutputCount) => { // Simplifying bch-js function for P2PKH txs only, as this is all Cashtab supports for now // https://github.com/Permissionless-Software-Foundation/bch-js/blob/master/src/bitcoincash.js#L408 /* const types = { inputs: { 'P2PKH': 148 * 4, }, outputs: { P2PKH: 34 * 4, }, }; */ const inputCount = new BigNumber(p2pkhInputCount); const outputCount = new BigNumber(p2pkhOutputCount); const inputWeight = new BigNumber(148 * 4); const outputWeight = new BigNumber(34 * 4); const nonSegwitWeightConstant = new BigNumber(10 * 4); let totalWeight = new BigNumber(0); totalWeight = totalWeight .plus(inputCount.times(inputWeight)) .plus(outputCount.times(outputWeight)) .plus(nonSegwitWeightConstant); const byteCount = totalWeight.div(4).integerValue(BigNumber.ROUND_CEIL); return Number(byteCount); }; export const calcFee = ( utxos, p2pkhOutputNumber = 2, satoshisPerByte = currency.defaultFee, ) => { const byteCount = getCashtabByteCount(utxos.length, p2pkhOutputNumber); const txFee = Math.ceil(satoshisPerByte * byteCount); return txFee; }; export const generateTokenTxOutput = ( txBuilder, tokenAction, legacyCashOriginAddress, tokenUtxosBeingSpent = [], // optional - send or burn tx only remainderXecValue = new BigNumber(0), // optional - only if > dust tokenConfigObj = {}, // optional - genesis only tokenRecipientAddress = false, // optional - send tx only tokenAmount = false, // optional - send or burn amount for send/burn tx only ) => { try { if (!tokenAction || !legacyCashOriginAddress || !txBuilder) { throw new Error('Invalid token tx output parameter'); } let script, opReturnObj, destinationAddress; switch (tokenAction) { case 'GENESIS': script = generateGenesisOpReturn(tokenConfigObj); destinationAddress = legacyCashOriginAddress; break; case 'SEND': opReturnObj = generateSendOpReturn( tokenUtxosBeingSpent, tokenAmount.toString(), ); script = opReturnObj.script; destinationAddress = tokenRecipientAddress; break; case 'BURN': script = generateBurnOpReturn( tokenUtxosBeingSpent, tokenAmount, ); destinationAddress = legacyCashOriginAddress; break; default: throw new Error('Invalid token transaction type'); } // OP_RETURN needs to be the first output in the transaction. txBuilder.addOutput(script, 0); // add XEC dust output as fee for genesis, send or burn token output txBuilder.addOutput(destinationAddress, parseInt(currency.etokenSats)); // Return any token change back to the sender for send and burn txs if ( tokenAction !== 'GENESIS' || (opReturnObj && opReturnObj.outputs > 1) ) { // add XEC dust output as fee txBuilder.addOutput( tokenUtxosBeingSpent[0].address, // etoken address parseInt(currency.etokenSats), ); } // Send xec change to own address if (remainderXecValue.gte(new BigNumber(currency.dustSats))) { txBuilder.addOutput( legacyCashOriginAddress, parseInt(remainderXecValue), ); } } catch (err) { console.log(`generateTokenTxOutput() error: ` + err); throw err; } return txBuilder; }; export const generateTxInput = ( isOneToMany, utxos, txBuilder, destinationAddressAndValueArray, satoshisToSend, feeInSatsPerByte, ) => { let txInputObj = {}; const inputUtxos = []; let txFee = 0; let totalInputUtxoValue = new BigNumber(0); try { if ( (isOneToMany && !destinationAddressAndValueArray) || !utxos || !txBuilder || !satoshisToSend || !feeInSatsPerByte ) { throw new Error('Invalid tx input parameter'); } // 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]; totalInputUtxoValue = totalInputUtxoValue.plus(utxo.value); const vout = utxo.outpoint.outIdx; const txid = utxo.outpoint.txid; // add input with txid and index of vout txBuilder.addInput(txid, vout); inputUtxos.push(utxo); txFee = calcFee(inputUtxos, txOutputs, feeInSatsPerByte); if (totalInputUtxoValue.minus(satoshisToSend).minus(txFee).gte(0)) { break; } } } catch (err) { console.log(`generateTxInput() error: ` + err); throw err; } txInputObj.txBuilder = txBuilder; txInputObj.totalInputUtxoValue = totalInputUtxoValue; txInputObj.inputUtxos = inputUtxos; txInputObj.txFee = txFee; return txInputObj; }; export const generateTokenTxInput = ( tokenAction, // GENESIS, SEND or BURN totalXecUtxos, totalTokenUtxos, tokenId, tokenAmount, // optional - only for sending or burning feeInSatsPerByte, txBuilder, ) => { let totalXecInputUtxoValue = new BigNumber(0); let remainderXecValue = new BigNumber(0); let remainderTokenValue = new BigNumber(0); let totalXecInputUtxos = []; let txFee = 0; let tokenUtxosBeingSpent = []; try { if ( !tokenAction || !totalXecUtxos || (tokenAction !== 'GENESIS' && !tokenId) || !feeInSatsPerByte || !txBuilder ) { throw new Error('Invalid token tx input parameter'); } // collate XEC UTXOs for this token tx const txOutputs = tokenAction === 'GENESIS' ? 2 // one for genesis OP_RETURN output and one for change : 4; // for SEND/BURN token txs see T2645 on why this is not dynamically generated for (let i = 0; i < totalXecUtxos.length; i++) { const thisXecUtxo = totalXecUtxos[i]; totalXecInputUtxoValue = totalXecInputUtxoValue.plus( new BigNumber(thisXecUtxo.value), ); const vout = thisXecUtxo.outpoint.outIdx; const txid = thisXecUtxo.outpoint.txid; // add input with txid and index of vout txBuilder.addInput(txid, vout); totalXecInputUtxos.push(thisXecUtxo); txFee = calcFee(totalXecInputUtxos, txOutputs, feeInSatsPerByte); remainderXecValue = tokenAction === 'GENESIS' ? totalXecInputUtxoValue .minus(new BigNumber(currency.etokenSats)) .minus(new BigNumber(txFee)) : totalXecInputUtxoValue .minus(new BigNumber(currency.etokenSats * 2)) // one for token send/burn output, one for token change .minus(new BigNumber(txFee)); if (remainderXecValue.gte(0)) { break; } } if (remainderXecValue.lt(0)) { throw new Error(`Insufficient funds`); } let filteredTokenInputUtxos = []; let finalTokenAmountSpent = new BigNumber(0); let tokenAmountBeingSpent = new BigNumber(tokenAmount); if (tokenAction === 'SEND' || tokenAction === 'BURN') { // filter for token UTXOs matching the token being sent/burnt filteredTokenInputUtxos = totalTokenUtxos.filter(utxo => { if ( utxo && // UTXO is associated with a token. utxo.slpMeta.tokenId === tokenId && // UTXO matches the token ID. !utxo.slpToken.isMintBaton // UTXO is not a minting baton. ) { return true; } return false; }); if (filteredTokenInputUtxos.length === 0) { throw new Error( 'No token UTXOs for the specified token could be found.', ); } // collate token UTXOs to cover the token amount being sent/burnt for (let i = 0; i < filteredTokenInputUtxos.length; i++) { finalTokenAmountSpent = finalTokenAmountSpent.plus( new BigNumber(filteredTokenInputUtxos[i].tokenQty), ); txBuilder.addInput( filteredTokenInputUtxos[i].outpoint.txid, filteredTokenInputUtxos[i].outpoint.outIdx, ); tokenUtxosBeingSpent.push(filteredTokenInputUtxos[i]); if (tokenAmountBeingSpent.lte(finalTokenAmountSpent)) { break; } } // calculate token change remainderTokenValue = finalTokenAmountSpent.minus( new BigNumber(tokenAmount), ); if (remainderTokenValue.lt(0)) { throw new Error( 'Insufficient token UTXOs for the specified token amount.', ); } } } catch (err) { console.log(`generateTokenTxInput() error: ` + err); throw err; } return { txBuilder: txBuilder, inputXecUtxos: totalXecInputUtxos, inputTokenUtxos: tokenUtxosBeingSpent, remainderXecValue: remainderXecValue, remainderTokenValue: remainderTokenValue, }; }; export const getChangeAddressFromInputUtxos = (inputUtxos, wallet) => { if (!inputUtxos || !wallet) { throw new Error('Invalid getChangeAddressFromWallet input parameter'); } // Assume change address is input address of utxo at index 0 let changeAddress; // Validate address try { changeAddress = inputUtxos[0].address; if (!isValidBchAddress(changeAddress)) { throw new Error('Invalid change address'); } } catch (err) { throw new Error('Invalid input utxo'); } return changeAddress; }; /* * Parse the total value of a send XEC tx and checks whether it is more than dust * One to many: isOneToMany is true, singleSendValue is null * One to one: isOneToMany is false, destinationAddressAndValueArray is null * Returns the aggregate send value in BigNumber format */ export const parseXecSendValue = ( isOneToMany, singleSendValue, destinationAddressAndValueArray, ) => { let value = new BigNumber(0); try { 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 // each array row is: 'eCash address, send value' value = BigNumber.sum( value, new BigNumber( destinationAddressAndValueArray[i].split(',')[1], ), ); } } else { // this is a one to one XEC transaction then check singleSendValue // note: one to many transactions won't be sending a singleSendValue param if (!singleSendValue) { throw new Error('Invalid singleSendValue'); } value = new BigNumber(singleSendValue); } // If user is attempting to send an aggregate value that is less than minimum accepted by the backend if ( value.lt( new BigNumber(fromSatoshisToXec(currency.dustSats).toString()), ) ) { // Throw the same error given by the backend attempting to broadcast such a tx throw new Error('dust'); } } catch (err) { console.log('Error in parseXecSendValue: ' + err); throw err; } return value; }; export const encodeOpReturnScript = scriptChunks => { // reference https://github.com/Permissionless-Software-Foundation/bch-js/blob/master/src/script.js#L153 const arr = []; scriptChunks.forEach(chunk => { arr.push(chunk); }); return Bitcoin.script.compile(arr); }; /* * Generates an OP_RETURN script to reflect the various send XEC permutations * involving messaging, encryption, eToken IDs and airdrop flags. * * Returns the final encoded script object */ export const generateOpReturnScript = ( optionalOpReturnMsg, encryptionFlag, airdropFlag, airdropTokenId, encryptedEj, ) => { // encrypted mesage is mandatory when encryptionFlag is true // airdrop token id is mandatory when airdropFlag is true if ((encryptionFlag && !encryptedEj) || (airdropFlag && !airdropTokenId)) { throw new Error('Invalid OP RETURN script input'); } // Note: script.push(Buffer.from(currency.opReturn.opReturnPrefixHex, 'hex')); actually evaluates to '016a' // instead of keeping the hex string intact. This behavour is specific to the initial script array element. // To get around this, the bch-js approach of directly using the opReturn prefix in decimal form for the initial entry is used here. let script = [currency.opReturn.opReturnPrefixDec]; // initialize script with the OP_RETURN op code (6a) in decimal form (106) try { if (encryptionFlag) { // if the user has opted to encrypt this message // add the encrypted cashtab messaging prefix and encrypted msg to script script.push( Buffer.from( currency.opReturn.appPrefixesHex.cashtabEncrypted, 'hex', ), // 65746162 ); // add the encrypted message to script script.push(Buffer.from(encryptedEj)); } else { // this is an un-encrypted message if (airdropFlag) { // if this was routed from the airdrop component // add the airdrop prefix to script script.push( Buffer.from( currency.opReturn.appPrefixesHex.airdrop, 'hex', ), // drop ); // add the airdrop token ID to script script.push(Buffer.from(airdropTokenId, 'hex')); } // add the cashtab prefix to script script.push( Buffer.from(currency.opReturn.appPrefixesHex.cashtab, 'hex'), // 00746162 ); // add the un-encrypted message to script if supplied if (optionalOpReturnMsg) { script.push(Buffer.from(optionalOpReturnMsg)); } } } catch (err) { console.log('Error in generateOpReturnScript(): ' + err); throw err; } return encodeOpReturnScript(script); }; export const generateTxOutput = ( isOneToMany, singleSendValue, satoshisToSend, totalInputUtxoValue, destinationAddress, destinationAddressAndValueArray, changeAddress, txFee, txBuilder, ) => { try { if ( (isOneToMany && !destinationAddressAndValueArray) || (!isOneToMany && !destinationAddress && !singleSendValue) || !changeAddress || !satoshisToSend || !totalInputUtxoValue || !txFee || !txBuilder ) { throw new Error('Invalid tx input parameter'); } // amount to send back to the remainder address. const remainder = new BigNumber(totalInputUtxoValue) .minus(satoshisToSend) .minus(txFee); if (remainder.lt(0)) { throw new Error(`Insufficient funds`); } 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], ); txBuilder.addOutput( outputAddress, parseInt(fromXecToSatoshis(outputValue)), ); } } else { // for one to one mode, add output w/ single address and amount to send txBuilder.addOutput( destinationAddress, parseInt(fromXecToSatoshis(singleSendValue)), ); } // if a remainder exists, return to change address as the final output if (remainder.gte(new BigNumber(currency.dustSats))) { txBuilder.addOutput(changeAddress, parseInt(remainder)); } } catch (err) { console.log('Error in generateTxOutput(): ' + err); throw err; } return txBuilder; }; export const signAndBuildTx = (inputUtxos, txBuilder, wallet) => { if ( !inputUtxos || inputUtxos.length === 0 || !txBuilder || !wallet || // txBuilder.transaction.tx.ins is empty until the inputUtxos are signed txBuilder.transaction.tx.outs.length === 0 ) { throw new Error('Invalid buildTx parameter'); } // Sign each XEC UTXO being consumed and refresh transactionBuilder txBuilder = signUtxosByAddress(inputUtxos, wallet, txBuilder); let hex; try { // build tx const tx = txBuilder.build(); // output rawhex hex = tx.toHex(); } catch (err) { throw new Error('Transaction build failed'); } return hex; }; export function parseOpReturn(hexStr) { if ( !hexStr || typeof hexStr !== 'string' || hexStr.substring(0, 2) !== currency.opReturn.opReturnPrefixHex ) { return false; } hexStr = hexStr.slice(2); // remove the first byte i.e. 6a /* * @Return: resultArray is structured as follows: * resultArray[0] is the transaction type i.e. eToken prefix, cashtab prefix, external message itself if unrecognized prefix * resultArray[1] is the actual cashtab message or the 2nd part of an external message * resultArray[2 - n] are the additional messages for future protcols */ let resultArray = []; let message = ''; let hexStrLength = hexStr.length; for (let i = 0; hexStrLength !== 0; i++) { // part 1: check the preceding byte value for the subsequent message let byteValue = hexStr.substring(0, 2); let msgByteSize = 0; if (byteValue === currency.opReturn.opPushDataOne) { // if this byte is 4c then the next byte is the message byte size - retrieve the message byte size only msgByteSize = parseInt(hexStr.substring(2, 4), 16); // hex base 16 to decimal base 10 hexStr = hexStr.slice(4); // strip the 4c + message byte size info } else { // take the byte as the message byte size msgByteSize = parseInt(hexStr.substring(0, 2), 16); // hex base 16 to decimal base 10 hexStr = hexStr.slice(2); // strip the message byte size info } // part 2: parse the subsequent message based on bytesize const msgCharLength = 2 * msgByteSize; message = hexStr.substring(0, msgCharLength); if (i === 0 && message === currency.opReturn.appPrefixesHex.eToken) { // add the extracted eToken prefix to array then exit loop resultArray[i] = currency.opReturn.appPrefixesHex.eToken; break; } else if ( i === 0 && message === currency.opReturn.appPrefixesHex.cashtab ) { // add the extracted Cashtab prefix to array resultArray[i] = currency.opReturn.appPrefixesHex.cashtab; } else if ( i === 0 && message === currency.opReturn.appPrefixesHex.cashtabEncrypted ) { // add the Cashtab encryption prefix to array resultArray[i] = currency.opReturn.appPrefixesHex.cashtabEncrypted; } else if ( i === 0 && message === currency.opReturn.appPrefixesHex.airdrop ) { // add the airdrop prefix to array resultArray[i] = currency.opReturn.appPrefixesHex.airdrop; } else { // this is either an external message or a subsequent cashtab message loop to extract the message resultArray[i] = message; } // strip out the parsed message hexStr = hexStr.slice(msgCharLength); hexStrLength = hexStr.length; } return resultArray; } export const fromLegacyDecimals = ( amount, cashDecimals = currency.cashDecimals, ) => { // Input 0.00000546 BCH // Output 5.46 XEC or 0.00000546 BCH, depending on currency.cashDecimals const amountBig = new BigNumber(amount); const conversionFactor = new BigNumber(10 ** (8 - cashDecimals)); const amountSmallestDenomination = amountBig .times(conversionFactor) .toNumber(); return amountSmallestDenomination; }; export const fromSatoshisToXec = ( amount, cashDecimals = currency.cashDecimals, ) => { const amountBig = new BigNumber(amount); const multiplier = new BigNumber(10 ** (-1 * cashDecimals)); const amountInBaseUnits = amountBig.times(multiplier); return amountInBaseUnits; }; export const fromXecToSatoshis = ( sendAmount, cashDecimals = currency.cashDecimals, ) => { // Replace the BCH.toSatoshi method with an equivalent function that works for arbitrary decimal places // Example, for an 8 decimal place currency like Bitcoin // Input: a BigNumber of the amount of Bitcoin to be sent // Output: a BigNumber of the amount of satoshis to be sent, or false if input is invalid // Validate // Input should be a BigNumber with no more decimal places than cashDecimals const isValidSendAmount = BigNumber.isBigNumber(sendAmount) && sendAmount.dp() <= cashDecimals; if (!isValidSendAmount) { return false; } const conversionFactor = new BigNumber(10 ** cashDecimals); const sendAmountSmallestDenomination = sendAmount.times(conversionFactor); return sendAmountSmallestDenomination; }; export const flattenContactList = contactList => { /* Converts contactList from array of objects of type {address: , name: } to array of addresses only If contact list is invalid, returns and empty array */ if (!isValidContactList(contactList)) { return []; } let flattenedContactList = []; for (let i = 0; i < contactList.length; i += 1) { const thisAddress = contactList[i].address; flattenedContactList.push(thisAddress); } return flattenedContactList; }; export const loadStoredWallet = walletStateFromStorage => { // Accept cached tokens array that does not save BigNumber type of BigNumbers // Return array with BigNumbers converted // See BigNumber.js api for how to create a BigNumber object from an object // https://mikemcl.github.io/bignumber.js/ const liveWalletState = walletStateFromStorage; const { slpBalancesAndUtxos, tokens } = liveWalletState; for (let i = 0; i < tokens.length; i += 1) { const thisTokenBalance = tokens[i].balance; thisTokenBalance._isBigNumber = true; tokens[i].balance = new BigNumber(thisTokenBalance); } // Also confirm balance is correct // Necessary step in case currency.decimals changed since last startup const balancesRebased = getWalletBalanceFromUtxos( slpBalancesAndUtxos.nonSlpUtxos, ); liveWalletState.balances = balancesRebased; return liveWalletState; }; export const getWalletBalanceFromUtxos = nonSlpUtxos => { const totalBalanceInSatoshis = nonSlpUtxos.reduce( (previousBalance, utxo) => previousBalance.plus(new BigNumber(utxo.value)), new BigNumber(0), ); return { totalBalanceInSatoshis: totalBalanceInSatoshis.toString(), totalBalance: fromSatoshisToXec(totalBalanceInSatoshis).toString(), }; }; export const isValidStoredWallet = walletStateFromStorage => { return ( typeof walletStateFromStorage === 'object' && 'state' in walletStateFromStorage && typeof walletStateFromStorage.state === 'object' && 'balances' in walletStateFromStorage.state && - 'utxos' in walletStateFromStorage.state && !('hydratedUtxoDetails' in walletStateFromStorage.state) && 'slpBalancesAndUtxos' in walletStateFromStorage.state && 'tokens' in walletStateFromStorage.state ); }; export const getWalletState = wallet => { if (!wallet || !wallet.state) { return { balances: { totalBalance: 0, totalBalanceInSatoshis: 0 }, hydratedUtxoDetails: {}, tokens: [], slpBalancesAndUtxos: {}, parsedTxHistory: [], utxos: [], }; } return wallet.state; }; export function convertEtokenToEcashAddr(eTokenAddress) { if (!eTokenAddress) { return new Error( `cashMethods.convertToEcashAddr() error: No etoken address provided`, ); } // Confirm input is a valid eToken address const isValidInput = isValidEtokenAddress(eTokenAddress); if (!isValidInput) { return new Error( `cashMethods.convertToEcashAddr() error: ${eTokenAddress} is not a valid etoken address`, ); } // Check for etoken: prefix const isPrefixedEtokenAddress = eTokenAddress.slice(0, 7) === 'etoken:'; // If no prefix, assume it is checksummed for an etoken: prefix const testedEtokenAddr = isPrefixedEtokenAddress ? eTokenAddress : `etoken:${eTokenAddress}`; let ecashAddress; try { const { type, hash } = cashaddr.decode(testedEtokenAddr); ecashAddress = cashaddr.encode('ecash', type, hash); } catch (err) { return err; } return ecashAddress; } export function convertToEcashPrefix(bitcoincashPrefixedAddress) { // Prefix-less addresses may be valid, but the cashaddr.decode function used below // will throw an error without a prefix. Hence, must ensure prefix to use that function. const hasPrefix = bitcoincashPrefixedAddress.includes(':'); if (hasPrefix) { // Is it bitcoincash: or simpleledger: const { type, hash, prefix } = cashaddr.decode( bitcoincashPrefixedAddress, ); let newPrefix; if (prefix === 'bitcoincash') { newPrefix = 'ecash'; } else if (prefix === 'simpleledger') { newPrefix = 'etoken'; } else { return bitcoincashPrefixedAddress; } const convertedAddress = cashaddr.encode(newPrefix, type, hash); return convertedAddress; } else { return bitcoincashPrefixedAddress; } } export function convertEcashtoEtokenAddr(eCashAddress) { const isValidInput = isValidXecAddress(eCashAddress); if (!isValidInput) { return new Error(`${eCashAddress} is not a valid ecash address`); } // Check for ecash: prefix const isPrefixedEcashAddress = eCashAddress.slice(0, 6) === 'ecash:'; // If no prefix, assume it is checksummed for an ecash: prefix const testedEcashAddr = isPrefixedEcashAddress ? eCashAddress : `ecash:${eCashAddress}`; let eTokenAddress; try { const { type, hash } = cashaddr.decode(testedEcashAddr); eTokenAddress = cashaddr.encode('etoken', type, hash); } catch (err) { return new Error('eCash to eToken address conversion error'); } return eTokenAddress; } // converts ecash, etoken, bitcoincash and simpleledger addresses to hash160 export function toHash160(addr) { try { // decode address hash const { hash } = cashaddr.decode(addr); // encode the address hash to legacy format (bitcoin) const legacyAdress = bs58.encode(hash); // convert legacy to hash160 const addrHash160 = Buffer.from(bs58.decode(legacyAdress)).toString( 'hex', ); return addrHash160; } catch (err) { console.log('Error converting address to hash160'); throw err; } } /* Converts a serialized buffer containing encrypted data into an object * that can be interpreted by the ecies-lite library. * * For reference on the parsing logic in this function refer to the link below on the segment of * ecies-lite's encryption function where the encKey, macKey, iv and cipher are sliced and concatenated * https://github.com/tibetty/ecies-lite/blob/8fd97e80b443422269d0223ead55802378521679/index.js#L46-L55 * * A similar PSF implmentation can also be found at: * https://github.com/Permissionless-Software-Foundation/bch-encrypt-lib/blob/master/lib/encryption.js * * For more detailed overview on the ecies encryption scheme, see https://cryptobook.nakov.com/asymmetric-key-ciphers/ecies-public-key-encryption */ export const convertToEncryptStruct = encryptionBuffer => { // based on ecies-lite's encryption logic, the encryption buffer is concatenated as follows: // [ epk + iv + ct + mac ] whereby: // - The first 32 or 64 chars of the encryptionBuffer is the epk // - Both iv and ct params are 16 chars each, hence their combined substring is 32 chars from the end of the epk string // - within this combined iv/ct substring, the first 16 chars is the iv param, and ct param being the later half // - The mac param is appended to the end of the encryption buffer // validate input buffer if (!encryptionBuffer) { throw new Error( 'cashmethods.convertToEncryptStruct() error: input must be a buffer', ); } try { // variable tracking the starting char position for string extraction purposes let startOfBuf = 0; // *** epk param extraction *** // The first char of the encryptionBuffer indicates the type of the public key // If the first char is 4, then the public key is 64 chars // If the first char is 3 or 2, then the public key is 32 chars // Otherwise this is not a valid encryption buffer compatible with the ecies-lite library let publicKey; switch (encryptionBuffer[0]) { case 4: publicKey = encryptionBuffer.slice(0, 65); // extract first 64 chars as public key break; case 3: case 2: publicKey = encryptionBuffer.slice(0, 33); // extract first 32 chars as public key break; default: throw new Error(`Invalid type: ${encryptionBuffer[0]}`); } // *** iv and ct param extraction *** startOfBuf += publicKey.length; // sets the starting char position to the end of the public key (epk) in order to extract subsequent iv and ct substrings const encryptionTagLength = 32; // the length of the encryption tag (i.e. mac param) computed from each block of ciphertext, and is used to verify no one has tampered with the encrypted data const ivCtSubstring = encryptionBuffer.slice( startOfBuf, encryptionBuffer.length - encryptionTagLength, ); // extract the substring containing both iv and ct params, which is after the public key but before the mac param i.e. the 'encryption tag' const ivbufParam = ivCtSubstring.slice(0, 16); // extract the first 16 chars of substring as the iv param const ctbufParam = ivCtSubstring.slice(16); // extract the last 16 chars as substring the ct param // *** mac param extraction *** const macParam = encryptionBuffer.slice( encryptionBuffer.length - encryptionTagLength, encryptionBuffer.length, ); // extract the mac param appended to the end of the buffer return { iv: ivbufParam, epk: publicKey, ct: ctbufParam, mac: macParam, }; } catch (err) { console.error(`useBCH.convertToEncryptStruct() error: `, err); throw err; } }; export const isLegacyMigrationRequired = wallet => { // If the wallet does not have Path1899, // Or each Path1899, Path145, Path245 does not have a public key // Then it requires migration if ( !wallet.Path1899 || !wallet.Path1899.publicKey || !wallet.Path1899.hash160 || !wallet.Path145.publicKey || !wallet.Path145.hash160 || !wallet.Path245.publicKey || !wallet.Path245.hash160 ) { return true; } return false; }; export const getHashArrayFromWallet = wallet => { // If the wallet has wallet.Path1899.hash160, it's migrated and will have all of them // Return false for an umigrated wallet const hash160Array = wallet && wallet.Path1899 && 'hash160' in wallet.Path1899 ? [ wallet.Path245.hash160, wallet.Path145.hash160, wallet.Path1899.hash160, ] : false; return hash160Array; }; export const isActiveWebsocket = ws => { // Return true if websocket is connected and subscribed // Otherwise return false return ( ws !== null && ws && '_ws' in ws && 'readyState' in ws._ws && ws._ws.readyState === 1 && '_subs' in ws && ws._subs.length > 0 ); }; export const hash160ToAddress = hash160 => { const buffer = Buffer.from(hash160, 'hex'); // Because ecashaddrjs only accepts Uint8Array as input type, convert const hash160ArrayBuffer = new ArrayBuffer(buffer.length); const hash160Uint8Array = new Uint8Array(hash160ArrayBuffer); for (let i = 0; i < hash160Uint8Array.length; i += 1) { hash160Uint8Array[i] = buffer[i]; } // Encode ecash: address const ecashAddr = cashaddr.encode('ecash', 'P2PKH', hash160Uint8Array); return ecashAddr; };