diff --git a/cashtab/src/components/Common/Ticker.js b/cashtab/src/components/Common/Ticker.js index 0b3563d2d..664e084da 100644 --- a/cashtab/src/components/Common/Ticker.js +++ b/cashtab/src/components/Common/Ticker.js @@ -1,1740 +1,1738 @@ import mainLogo from 'assets/logo_primary.png'; import tokenLogo from 'assets/logo_secondary.png'; import BigNumber from 'bignumber.js'; export const currency = { name: 'eCash', ticker: 'XEC', logo: mainLogo, legacyPrefix: 'bitcoincash', // temporary parameters for pre-prod testing of alias feature aliasSettings: { aliasEnabled: false, // change to multisig prior to landing in prod aliasPaymentAddress: 'ecash:qp3c268rd5946l2f5m5es4x25f7ewu4sjvpy52pqa8', // nominal pricing tiers for testing only - to be used as 'aliasRegistrationFeeInSats.oneByte' aliasRegistrationFeeInSats: { oneByte: 558, twoByte: 557, threeByte: 556, fourByte: 555, fiveByte: 554, sixByte: 553, sevenByte: 552, eightByte: 551, minFee: 550, // dust }, aliasServerBaseUrl: 'https://aliasdev.etokens.cash', aliasMaxLength: 21, // max byte length, refer to the Alias spec at https://reviews.bitcoinabc.org/D12972 reservedAliases: [ 'avalanche', 'electrum', 'electrumabc', 'bitcoin', 'bitcoinabc', 'ecash', 'ecashofficial', 'xec', 'abc', 'cashtab', 'ecashtab', 'cashtabwallet', 'xecwallet', 'gnc', 'etoken', 'token', 'cashfusion', 'coinbase', 'binance', 'ethereum', 'helpdesk', 'admin', 'support', 'official', ], }, coingeckoId: 'ecash', defaultFee: 2.01, dustSats: 550, etokenSats: 546, cashDecimals: 2, blockExplorerUrl: 'https://explorer.e.cash', blockExplorerUrlTestnet: 'https://texplorer.bitcoinabc.org', pdfReceiptUrl: 'https://blockchair.com/ecash/transaction', tokenName: 'eToken', tokenTicker: 'eToken', tokenIconSubmitApi: 'https://icons.etokens.cash/new', tokenLogo: tokenLogo, tokenIconsUrl: 'https://etoken-icons.s3.us-west-2.amazonaws.com', tokenDbUrl: 'https://tokendb.kingbch.com', txHistoryCount: 10, txHistoryPageSize: 25, xecApiBatchSize: 20, - websocketDisconnectedRefreshInterval: 5000, // 1000 = 1s - websocketConnectedRefreshInterval: 10000, defaultCashtabCache: { tokenInfoById: {}, aliasCache: { aliases: [], cachedAliasCount: 0, }, }, defaultSettings: { fiatCurrency: 'usd', sendModal: false, autoCameraOn: true, hideMessagesFromUnknownSenders: false, balanceVisible: true, }, notificationDurationShort: 3, notificationDurationLong: 5, localStorageMaxCharacters: 24, newTokenDefaultUrl: 'https://cashtab.com/', opReturn: { opReturnPrefixHex: '6a', opReturnPrefixDec: '106', opReturnAppPrefixLengthHex: '04', opPushDataOne: '4c', appPrefixesHex: { eToken: '534c5000', cashtab: '00746162', cashtabEncrypted: '65746162', airdrop: '64726f70', aliasRegistration: '2e786563', }, /* encryptedMsgByteLimit context: As per `convertToEncryptStruct()` in cashMethods.js which breaks down the ecies-lite library's encryption logic, the encrypted OP_RETURN message that follows pushdata1 (4c) and pushdata (d1) prefixes is 209 bytes, based on a 127 byte message supplied via the frontend. These 209 bytes can be broken down into the following: - ivbufParam: 16 bytes - publicKey: 33 bytes - ctbufParam: 128 bytes - macParam: 32 bytes These byte sizes can be verified via debug logs in `convertToEncryptStruct`. The `ctbufParam` is the cipher text buffer, which is the encrypted message content. The other params (ivbuf, pubkey, mac) are all there to validate that the encryption has not been tampered with and facilitate the decryption. Based on testing, adding one more character to the message input (127+ bytes in cashtab frontend) will translate to an encryption output message (ivbuf + pubkey + ctbuf + mac) that is larger than 215 bytes (`unencryptedMsgByteLimit`). Therefore this encrypted bytesize limit is not derived as a constant delta from `unencryptedMsgByteLimit` like the airdrop message limit. */ encryptedMsgByteLimit: 127, /* The max payload per spec is 220 bytes (or 223 bytes including +1 for OP_RETURN and +2 for pushdata opcodes) Within this 223 bytes, transaction building will take up 8 bytes, hence unencryptedMsgByteLimit is set to 215 bytes i.e. 6a 04 [prefix byte] [prefix byte] [prefix byte] [prefix byte] 4c [next byte is pushdata byte] [pushdata byte] (d7 for 215 on a max-size Cashtab msg) */ unencryptedMsgByteLimit: 215, }, settingsValidation: { fiatCurrency: [ 'usd', 'idr', 'krw', 'cny', 'zar', 'vnd', 'cad', 'nok', 'eur', 'gbp', 'jpy', 'try', 'rub', 'inr', 'brl', 'php', 'ils', 'clp', 'twd', 'hkd', 'bhd', 'sar', 'aud', 'nzd', 'chf', 'ngn', ], sendModal: [true, false], autoCameraOn: [true, false], hideMessagesFromUnknownSenders: [true, false], balanceVisible: [true, false], }, fiatCurrencies: { usd: { name: 'US Dollar', symbol: '$', slug: 'usd' }, aud: { name: 'Australian Dollar', symbol: '$', slug: 'aud' }, bhd: { name: 'Bahraini Dinar', symbol: 'BD', slug: 'bhd' }, brl: { name: 'Brazilian Real', symbol: 'R$', slug: 'brl' }, gbp: { name: 'British Pound', symbol: '£', slug: 'gbp' }, cad: { name: 'Canadian Dollar', symbol: '$', slug: 'cad' }, clp: { name: 'Chilean Peso', symbol: '$', slug: 'clp' }, cny: { name: 'Chinese Yuan', symbol: '元', slug: 'cny' }, eur: { name: 'Euro', symbol: '€', slug: 'eur' }, hkd: { name: 'Hong Kong Dollar', symbol: 'HK$', slug: 'hkd' }, inr: { name: 'Indian Rupee', symbol: '₹', slug: 'inr' }, idr: { name: 'Indonesian Rupiah', symbol: 'Rp', slug: 'idr' }, ils: { name: 'Israeli Shekel', symbol: '₪', slug: 'ils' }, jpy: { name: 'Japanese Yen', symbol: '¥', slug: 'jpy' }, krw: { name: 'Korean Won', symbol: '₩', slug: 'krw' }, ngn: { name: 'Nigerian Naira', symbol: '₦', slug: 'ngn' }, nzd: { name: 'New Zealand Dollar', symbol: '$', slug: 'nzd' }, nok: { name: 'Norwegian Krone', symbol: 'kr', slug: 'nok' }, php: { name: 'Philippine Peso', symbol: '₱', slug: 'php' }, rub: { name: 'Russian Ruble', symbol: 'р.', slug: 'rub' }, twd: { name: 'New Taiwan Dollar', symbol: 'NT$', slug: 'twd' }, sar: { name: 'Saudi Riyal', symbol: 'SAR', slug: 'sar' }, zar: { name: 'South African Rand', symbol: 'R', slug: 'zar' }, chf: { name: 'Swiss Franc', symbol: 'Fr.', slug: 'chf' }, try: { name: 'Turkish Lira', symbol: '₺', slug: 'try' }, vnd: { name: 'Vietnamese đồng', symbol: 'đ', slug: 'vnd' }, }, coingeckoTop500Tickers: [ 'btc', 'eth', 'usdt', 'bnb', 'usdc', 'xrp', 'busd', 'ada', 'sol', 'doge', 'dot', 'matic', 'steth', 'shib', 'dai', 'trx', 'wbtc', 'avax', 'uni', 'okb', 'leo', 'ltc', 'atom', 'link', 'etc', 'ftt', 'xlm', 'cro', 'qnt', 'xmr', 'near', 'algo', 'bch', 'vet', 'lunc', 'flow', 'fil', 'hbar', 'ape', 'egld', 'wbt', 'frax', 'icp', 'apt', 'aave', 'xtz', 'xcn', 'ht', 'tkx', 'sand', 'mana', 'ldo', 'eos', 'theta', 'chz', 'cusdc', 'kcs', 'bsv', 'usdp', 'axs', 'tusd', 'mkr', 'evmos', 'btt', 'usdd', 'xec', 'miota', 'ethw', 'klay', 'zec', 'cdai', 'gt', 'cake', 'grt', 'btse', 'neo', 'osmo', 'ceth', 'hnt', 'snx', 'nexo', 'ftm', 'ar', 'xrd', 'paxg', 'cspr', 'crv', 'kava', 'bit', 'ens', 'twt', 'dash', 'rune', 'zil', 'rpl', 'xdc', 'enj', 'bat', 'xaut', 'stx', 'fxs', 'cel', 'mina', 'dcr', 'dfi', 'amp', 'luna', 'rvn', 'ustc', 'cusdt', 'cvx', '1inch', 'mex', 'comp', 'omi', 'hot', 'xem', 'gusd', 'celo', 'waves', 'imx', 'btg', 'lrc', 'ksm', 'rose', 'nxm', 'tfuel', 'gmx', 'gno', 'ohm', 'gmt', 'qtum', 'sushi', 'safemoon', 'okt', 'srm', 'iost', 'glm', 'jst', 'hbtc', 'gala', 'kda', 'cvxcrv', 'iotx', 'rsr', 'yfi', 'poly', 'elg', 'lpt', 'ankr', 'bal', 'omg', 'bone', 'msol', 'syn', 'zrx', 'mc', 'one', 'reth', 'eurt', 'dydx', 'mim', 'dag', 'flux', 'icx', 'glmr', 'woo', 'juno', 'rbn', 'ont', 'babydoge', 'op', 'husd', 'brise', 'lusd', 'nft', 'ewt', 'nu', 'chsb', 'ln', 'xcm', '10set', 'alusd', 'waxp', 'hive', 'vvs', 'azero', 'xch', 'deso', 'zen', 'mdx', 'sc', 'scrt', 'sxp', 'rndr', 'audio', 'uma', 'sgb', 'inj', 'dao', 'sfm', 'cet', 'lsk', 'pla', 'skl', 'dgb', 'elon', 'astr', 'slp', 'metis', 'fx', 'eurs', 'pundix', 'mxc', 'kiro', 'erg', 'gfarm2', 'stsol', 'vgx', 'ckb', 'reef', 'usdn', 'ren', 'lyxe', 'api3', 'coti', 'looks', 'dexe', 'snt', 'pltc', 'uqc', 'kub', 'ceek', 'usdx', 'scho', 'prom', 'tribe', 'veri', 'pyr', 'xsushi', 'win', 'med', 'mpl', 'pokt', 'boba', 'cvc', 'xno', 'sys', 'gns', 'steem', 'orbs', 'ardr', 'mbox', 'mvl', 'tryb', 'ever', 'wrx', 'kuji', 'nmr', 'pha', 'mx', 'ult', 'seth2', 'rad', 'bsw', 'aca', 'people', 'spell', 'knc', 'bico', 'alice', 'hxro', 'eul', 'multi', 'plt', 'c98', 'raca', 'exrd', 'celr', 'req', 'plex', 'powr', 'bnt', 'ctsi', 'ilv', 'ctc', 'temple', 'tel', 'krd', 'cfx', 'uos', 'dka', 'nrv', 'chr', 'xprt', 'dent', 'fun', 'asd', 'arrr', 'savax', 'magic', 'rlc', 'bdx', 'emaid', 'xyo', 'euroc', 'quack', 'strax', 'wcfg', 'cfg', 'aethc', 'tsuka', 'btc.b', 'qkc', 'renbtc', 'stpt', 'canto', 'vtho', 'gal', 'mlk', 'fidu', 'ton', 'trac', 'dock', 'ctk', 'ocean', 'ray', 'stg', 'shr', 'ogn', 'mnw', 'pcx', 'sx', 'susd', 'joe', 'nest', 'kilt', 'aurora', 'h2o', 'keep', 'flexusd', 'mtrg', 'xvs', 'bfc', 'ant', 'ccd', 'ssv', 'floki', 'sfund', 'cdt', 'mtl', 'ark', 'sure', 'xmon', 'tlm', 'btcst', 'stmx', 'itamcube', 'koin', 'lqty', 'rev', 'utk', 'storj', 'movr', 'elf', 'xido', 'hez', 'core', 'efi', 'aergo', 'stars', 'zz', 'wozx', 'fei', 'ach', 'fet', 'sun', 'agix', 'gog', 'sofi', 'ride', 'nkn', 'mtd', 'ads', 'bor', 'sweat', 'oxt', 'iq', 'badger', 'dodo', 'fold', 'xsgd', 'rly', 'xvg', 'rep', 'dawn', 'gbex', 'ygg', 'strk', 'beta', 'mimatic', 'alpha', 'manc', 'phb', 'koda', 'dpx', 'dero', 'ghst', 'cusd', 'meta', 'ufo', 'etn', 'divi', 'saitama', 'mask', 'dusk', 'santos', 'akt', 'nxs', 'bond', 'rif', 'mintme', 'yfii', 'wnxm', 'bitci', 'mft', 'ibeur', 'tlos', 'dc', 'ata', 'alpaca', 'fer', 'xcad', 'blid', 'band', 'wmt', 'gods', 'egc', 'axn', 'bake', 'pols', 'auction', 'kishu', 'ama', 'tt', 'usdk', 'flm', 'ageur', 'zig', 'poop', 'dome', 'vega', 'sfp', 'cbat', 'vra', 'ampl', 'mln', 'dola', 'ousd', 'kp3r', 'cocos', 'hydra', 'pond', 'tru', 'dei', 'ern', 'idex', 'nebl', 'time', 'loomold', 'qrdo', 'lcx', 'avinoc', 'perp', 'hunt', 'cweb', 'b2m', 'clv', 'klv', 'xki', 'lto', 'ava', 'ron', 'hc', 'opt2', 'astrafer', 'kunci', 'itheum', 'leash', 'tomo', 'cqt', 'volt', 'orn', 'regen', 'super', 'gxc', 'wan', 'forth', 'sb', ], coingeckoTop500Names: [ 'Bitcoin', 'Ethereum', 'Tether', 'BNB', 'USD Coin', 'XRP', 'Binance USD', 'Cardano', 'Solana', 'Dogecoin', 'Polkadot', 'Polygon', 'Lido Staked Ether', 'Shiba Inu', 'Dai', 'TRON', 'Wrapped Bitcoin', 'Avalanche', 'Uniswap', 'OKB', 'LEO Token', 'Litecoin', 'Cosmos Hub', 'Chainlink', 'Ethereum Classic', 'FTX', 'Stellar', 'Cronos', 'Quant', 'Monero', 'NEAR Protocol', 'Algorand', 'Bitcoin Cash', 'VeChain', 'Terra Luna Classic', 'Flow', 'Filecoin', 'Hedera', 'ApeCoin', 'Elrond', 'WhiteBIT Token', 'Frax', 'Internet Computer', 'Aptos', 'Aave', 'Tezos', 'Chain', 'Huobi', 'Tokenize Xchange', 'The Sandbox', 'Decentraland', 'Lido DAO', 'EOS', 'Theta Network', 'Chiliz', 'cUSDC', 'KuCoin', 'Bitcoin SV', 'Pax Dollar', 'Axie Infinity', 'TrueUSD', 'Maker', 'Evmos', 'BitTorrent', 'USDD', 'eCash', 'Klaytn', 'IOTA', 'EthereumPoW', 'Zcash', 'cDAI', 'PancakeSwap', 'Gate', 'The Graph', 'NEO', 'BTSE Token', 'cETH', 'Osmosis', 'Helium', 'Synthetix Network', 'NEXO', 'Fantom', 'Arweave', 'Radix', 'Casper Network', 'PAX Gold', 'Curve DAO', 'Kava', 'BitDAO', 'Ethereum Name Service', 'Dash', 'Trust Wallet', 'Zilliqa', 'THORChain', 'Rocket Pool', 'XDC Network', 'Enjin Coin', 'Basic Attention', 'Tether Gold', 'Stacks', 'Frax Share', 'Celsius Network', 'Mina Protocol', 'Decred', 'DeFiChain', 'Amp', 'Terra', 'Ravencoin', 'cUSDT', 'TerraClassicUSD', 'Convex Finance', '1inch', 'Maiar DEX', 'Compound', 'ECOMI', 'Holo', 'NEM', 'Gemini Dollar', 'Celo', 'Waves', 'Bitcoin Gold', 'Immutable X', 'Loopring', 'Kusama', 'Oasis Network', 'Theta Fuel', 'Nexus Mutual', 'GMX', 'Gnosis', 'Olympus', 'STEPN', 'Qtum', 'Sushi', 'SafeMoon [OLD]', 'OKC', 'Serum', 'IOST', 'Golem', 'JUST', 'Gala', 'Huobi BTC', 'Kadena', 'IoTeX', 'Convex CRV', 'Reserve Rights', 'yearn.finance', 'Polymath', 'Ankr', 'Escoin', 'Livepeer', 'Balancer', 'Bone ShibaSwap', 'OMG Network', 'Marinade staked SOL', 'Merit Circle', '0x', 'Harmony', 'Synapse', 'Rocket Pool ETH', 'Euro Tether', 'dYdX', 'Magic Internet Money', 'Flux', 'ICON', 'Constellation', 'Moonbeam', 'WOO Network', 'JUNO', 'Optimism', 'Ontology', 'Ribbon Finance', 'Bitgert', 'HUSD', 'Baby Doge Coin', 'Energy Web', 'APENFT', 'SwissBorg', 'Liquity USD', 'NuCypher', 'Coinmetro', 'Tenset', 'LINK', 'Alchemix USD', 'WAX', 'Hive', 'VVS Finance', 'Decentralized Social', 'Aleph Zero', 'Chia', 'Horizen', 'Mdex', 'Siacoin', 'Secret', 'SXP', 'Render', 'UMA', 'Audius', 'Injective', 'DAO Maker', 'Songbird', 'SafeMoon', 'CoinEx', 'Lisk', 'PlayDapp', 'SKALE', 'DigiByte', 'Dogelon Mars', 'Astar', 'Smooth Love Potion', 'Metis', 'Function X', 'Pundi X', 'STASIS EURO', 'MXC', 'Kirobo', 'Ergo', 'Gains Farm', 'Lido Staked SOL', 'Voyager VGX', 'Nervos Network', 'Reef', 'Neutrino USD', 'REN', 'LUKSO', 'API3', 'COTI', 'LooksRare', 'DeXe', 'Status', 'PlatonCoin', 'Uquid Coin', 'Bitkub Coin', 'CEEK Smart VR', 'USDX', 'Scholarship Coin', 'Tribe', 'Prom', 'Veritaseum', 'xSUSHI', 'Vulcan Forged', 'WINkLink', 'Medibloc', 'Maple', 'Pocket Network', 'Civic', 'Nano', 'Boba Network', 'Syscoin', 'Gains Network', 'Velas', 'Steem', 'Orbs', 'Ardor', 'Mobox', 'MVL', 'BiLira', 'Everscale', 'WazirX', 'Kujira', 'Numeraire', 'Phala Network', 'MX', 'Shardus', 'sETH2', 'Radicle', 'Biswap', 'Acala', 'ConstitutionDAO', 'Spell', 'Kyber Network Crystal', 'Biconomy', 'My Neighbor Alice', 'Hxro', 'Euler', 'Multichain', 'Poollotto.finance', 'Coin98', 'Radio Caca', 'e-Radix', 'Celer Network', 'Request', 'PLEX', 'Power Ledger', 'Bancor Network', 'Cartesi', 'Illuvium', 'Creditcoin', 'Telcoin', 'TempleDAO', 'Krypton DAO', 'Conflux', 'Ultra', 'dKargo', 'Nerve Finance', 'Chromia', 'Persistence', 'Dent', 'FUN', 'AscendEx', 'Pirate Chain', 'BENQI Liquid Staked AVAX', 'iExec RLC', 'Magic', 'Beldex', 'MaidSafeCoin', 'XYO Network', 'Euro Coin', 'Rich Quack', 'Stratis', 'Wrapped Centrifuge', 'Centrifuge', 'Ankr Reward-Bearing Staked ETH', 'Bitcoin Avalanche Bridged (BTC.b)', 'QuarkChain', 'renBTC', 'STP', 'CANTO', 'VeThor', 'Galxe', 'MiL.k Alliance', 'Fidu', 'Dejitaru Tsuka', 'Tokamak Network', 'OriginTrail', 'Dock', 'Shentu', 'Ocean Protocol', 'Raydium', 'Stargate Finance', 'Share', 'Origin Protocol', 'Morpheus Network', 'ChainX', 'SX Network', 'sUSD', 'JOE', 'Nest Protocol', 'KILT Protocol', 'Aurora', 'H2O Dao', 'Keep Network', 'flexUSD', 'Meter Governance', 'Venus', 'Bifrost', 'Aragon', 'Concordium', 'SSV Network', 'FLOKI', 'Seedify.fund', 'Blox', 'Metal', 'Ark', 'inSure DeFi', 'XMON', 'Alien Worlds', 'BTC Standard Hashrate Token', 'StormX', 'CUBE', 'Koinos', 'Liquity', 'Revain', 'Utrust', 'Storj', 'Moonriver', 'aelf', 'Xido Finance', 'Hermez Network', 'cVault.finance', 'Efinity', 'Aergo', 'Stargaze', 'ZigZag', 'Efforce', 'Fei USD', 'Alchemy Pay', 'Fetch.ai', 'Sun Token', 'SingularityNET', 'Guild of Guardians', 'RAI Finance', 'holoride', 'NKN', 'Minted', 'Adshares', 'BoringDAO [OLD]', 'Sweatcoin - Sweat Economy', 'Orchid Protocol', 'IQ', 'Badger DAO', 'DODO', 'Manifold Finance', 'XSGD', 'Rally', 'Verge', 'Augur', 'Globiance Exchange', 'Yield Guild Games', 'Strike', 'Dawn Protocol', 'MAI', 'Beta Finance', 'Alpha Venture DAO', 'Mancium', 'Phoenix Global [OLD]', 'Koda Cryptocurrency', 'Dopex', 'Dero', 'Aavegotchi', 'UFO Gaming', 'Metadium', 'Celo Dollar', 'Electroneum', 'Divi', 'Saitama', 'Mask Network', 'DUSK Network', 'Santos FC Fan Token', 'Akash Network', 'Nexus', 'BarnBridge', 'RSK Infrastructure Framework', 'MintMe.com Coin', 'DFI.money', 'Wrapped NXM', 'Hifi Finance', 'Bitcicoin', 'Iron Bank EURO', 'Telos', 'Dogechain', 'XCAD Network', 'Automata', 'Alpaca Finance', 'Ferro', 'Bolide', 'World Mobile Token', 'Band Protocol', 'Gods Unchained', 'EverGrow Coin', 'Axion', 'BakerySwap', 'Polkastarter', 'Bounce', 'Kishu Inu', 'MrWeb Finance [OLD]', 'ThunderCore', 'USDK', 'Flamingo Finance', 'agEUR', 'Zignaly', 'Raresama', 'Everdome', 'Vega Protocol', 'SafePal', 'cBAT', 'Verasity', 'Ampleforth', 'Enzyme', 'Dola', 'Keep3rV1', 'Origin Dollar', 'COCOS BCX', 'Hydra', 'Marlin', 'TrueFi', 'DEI', 'Ethernity Chain', 'IDEX', 'Neblio', 'chrono.tech', 'Loom Network (OLD)', 'LCX', 'Qredo', 'AVINOC', 'Perpetual Protocol', 'Hunt', 'Coinweb', 'Bit2Me', 'Klever', 'Clover Finance', 'KI', 'LTO Network', 'Travala.com', 'Ronin', 'HyperCash', 'Optimus OPT2', 'Astrafer', 'Kunci Coin', 'Itheum', 'Doge Killer', 'TomoChain', 'Covalent', 'Volt Inu', 'Orion Protocol', 'Regen', 'SuperFarm', 'GXChain', 'Vite', 'Wanchain', 'Ampleforth Governance', ], coingeckoTop500Ids: [ 'bitcoin', 'ethereum', 'tether', 'binancecoin', 'usd-coin', 'ripple', 'binance-usd', 'cardano', 'solana', 'dogecoin', 'polkadot', 'matic-network', 'staked-ether', 'shiba-inu', 'dai', 'tron', 'avalanche-2', 'wrapped-bitcoin', 'uniswap', 'okb', 'litecoin', 'leo-token', 'cosmos', 'chainlink', 'ethereum-classic', 'ftx-token', 'stellar', 'crypto-com-chain', 'quant-network', 'monero', 'near', 'algorand', 'bitcoin-cash', 'vechain', 'terra-luna', 'flow', 'filecoin', 'apecoin', 'hedera-hashgraph', 'tokenize-xchange', 'elrond-erd-2', 'whitebit', 'internet-computer', 'tezos', 'frax', 'aptos', 'chain-2', 'huobi-token', 'aave', 'the-sandbox', 'decentraland', 'lido-dao', 'eos', 'theta-token', 'chiliz', 'compound-usd-coin', 'kucoin-shares', 'axie-infinity', 'bitcoin-cash-sv', 'paxos-standard', 'true-usd', 'maker', 'evmos', 'bittorrent', 'usdd', 'ecash', 'iota', 'ethereum-pow-iou', 'zcash', 'gatechain-token', 'pancakeswap-token', 'cdai', 'klay-token', 'the-graph', 'neo', 'compound-ether', 'osmosis', 'btse-token', 'havven', 'helium', 'nexo', 'fantom', 'arweave', 'pax-gold', 'radix', 'curve-dao-token', 'casper-network', 'kava', 'bitdao', 'trust-wallet-token', 'dash', 'ethereum-name-service', 'zilliqa', 'thorchain', 'enjincoin', 'basic-attention-token', 'blockstack', 'rocket-pool', 'xdce-crowd-sale', 'tether-gold', 'frax-share', 'mina-protocol', 'celsius-degree-token', 'decred', 'defichain', 'amp-token', 'terra-luna-2', 'ravencoin', '1inch', 'convex-finance', 'terrausd', 'maiar-dex', 'compound-usdt', 'compound-governance-token', 'nem', 'holotoken', 'ecomi', 'gemini-dollar', 'celo', 'theta-fuel', 'loopring', 'waves', 'kusama', 'immutable-x', 'nxm', 'bitcoin-gold', 'oasis-network', 'gnosis', 'gmx', 'olympus', 'qtum', 'stepn', 'sushi', 'safemoon', 'serum', 'oec-token', 'iostoken', 'golem', 'gala', 'iotex', 'kadena', 'huobi-btc', 'yearn-finance', 'reserve-rights-token', 'just', 'convex-crv', 'polymath', 'balancer', 'ankr', 'livepeer', 'escoin-token', 'omisego', 'bone-shibaswap', 'msol', 'rocket-pool-eth', '0x', 'harmony', 'synapse-2', 'ribbon-finance', 'tether-eurt', 'merit-circle', 'energy-web-token', 'magic-internet-money', 'icon', 'dydx', 'moonbeam', 'zelcash', 'woo-network', 'constellation-labs', 'optimism', 'juno-network', 'ontology', 'baby-doge-coin', 'nucypher', 'husd', 'tenset', 'bitrise-token', 'link', 'liquity-usd', 'coinmetro', 'swissborg', 'apenft', 'wax', 'alchemix-usd', 'hive', 'vvs-finance', 'bitclout', 'aleph-zero', 'zencash', 'chia', 'siacoin', 'mdex', 'secret', 'songbird', 'swipe', 'render-token', 'audius', 'injective-protocol', 'uma', 'dao-maker', 'playdapp', 'safemoon-2', 'coinex-token', 'lisk', 'dogelon-mars', 'digibyte', 'skale', 'astar', 'smooth-love-potion', 'gains-farm', 'metis-token', 'pundi-x-2', 'lukso-token', 'mxc', 'fx-coin', 'stasis-eurs', 'lido-staked-sol', 'kirobo', 'ergo', 'republic-protocol', 'reef', 'ethos', 'nervos-network', 'coti', 'api3', 'neutrino', 'looksrare', 'status', 'dexe', 'platoncoin', 'bitkub-coin', 'ceek', 'gains-network', 'tribe-2', 'scholarship-coin', 'usdx', 'veritaseum', 'prometeus', 'medibloc', 'xsushi', 'syscoin', 'vulcan-forged', 'wink', 'civic', 'nano', 'boba-network', 'maple', 'ardor', 'pocket-network', 'uquid-coin', 'orbs', 'mass-vehicle-ledger', 'mobox', 'everscale', 'velas', 'krypton-dao', 'bilira', 'wazirx', 'steem', 'numeraire', 'kujira', 'seth2', 'constitutiondao', 'hxro', 'mx-token', 'my-neighbor-alice', 'kyber-network-crystal', 'radicle', 'spell-token', 'shardus', 'acala', 'multichain', 'biswap', 'pha', 'richquack', 'biconomy', 'coin98', 'euler', 'canto', 'celer-network', 'cartesi', 'request-network', 'illuvium', 'milk-alliance', 'radio-caca', 'telcoin', 'power-ledger', 'bancor', 'poollotto-finance', 'ultra', 'e-radix', 'creditcoin-2', 'conflux-token', 'dkargo', 'chromaway', 'temple', 'magic', 'plex', 'dent', 'benqi-liquid-staked-avax', 'pirate-chain', 'nerve-finance', 'funfair', 'persistence', 'beldex', 'iexec-rlc', 'maidsafecoin', 'euro-coin', 'ankreth', 'wrapped-centrifuge', 'centrifuge', 'stratis', 'stp-network', 'xyo-network', 'asd', 'quark-chain', 'aurora-near', 'renbtc', 'dejitaru-tsuka', 'bitcoin-avalanche-bridged-btc-b', 'project-galaxy', 'origintrail', 'fidu', 'meter', 'vethor-token', 'tokamak-network', 'raydium', 'certik', 'dock', 'sx-network', 'ocean-protocol', 'origin-protocol', 'sharering', 'kilt-protocol', 'morpheus-network', 'joe', 'chainx', 'insure', 'keep-network', 'stargate-finance', 'nusd', 'aragon', 'venus', 'flex-usd', 'bifrost', 'h2o-dao', 'ssv-network', 'nest', 'ark', 'concordium', 'metal', 'floki', 'koinos', 'alien-worlds', 'btc-standard-hashrate-token', 'storm', 'seedify-fund', 'utrust', 'liquity', 'blox', 'revain', 'storj', 'cube', 'moonriver', 'xmon', 'aelf', 'cvault-finance', 'hermez-network-token', 'efinity', 'wozx', 'guild-of-guardians', 'aergo', 'singularitynet', 'alchemy-pay', 'fei-usd', 'fetch-ai', 'zigzag-2', 'minted', 'everipedia', 'sun-token', 'stargaze', 'sweatcoin', 'rai-finance', 'nkn', 'adshares', 'orchid-protocol', 'augur', 'boringdao-[old]', 'holoride', 'badger-dao', 'dodo', 'dogechain', 'santos-fc-fan-token', 'rally-2', 'divi', 'xido-finance', 'dopex', 'strike', 'xsgd', 'yield-guild-games', 'dawn-protocol', 'mancium', 'verge', 'beta-finance', 'manifold-finance', 'alpha-finance', 'mimatic', 'dero', 'metadium', 'celo-dollar', 'red-pulse', 'dusk-network', 'koda-finance', 'aavegotchi', 'ufo-gaming', 'mask-network', 'saitama-inu', 'akash-network', 'electroneum', 'globiance-exchange', 'nexus', 'rif-token', 'barnbridge', 'wrapped-nxm', 'bitcicoin', 'yfii-finance', 'origin-dollar', 'xcad-network', 'automata', 'iron-bank-euro', 'mainframe', 'webchain', 'kishu-inu', 'world-mobile-token', 'telos', 'alpaca-finance', 'band-protocol', 'ferro', 'bolide', 'thunder-token', 'gods-unchained', 'bakerytoken', 'auction', 'axion', 'evergrowcoin', 'vega-protocol', 'polkastarter', 'flamingo-finance', 'mrweb-finance', 'ampleforth', 'everdome', 'usdk', 'ageur', 'compound-basic-attention-token', 'verasity', 'zignaly', 'safepal', 'keep3rv1', 'dola-usd', 'truefi', 'cocos-bcx', 'raresama', 'hydra', 'marlin', 'aurora-dao', 'dei-token', 'ethernity-chain', 'loom-network', 'chronobank', 'qredo', 'hunt-token', 'step-app-fitfi', 'lcx', 'avinoc', 'bit2me', 'neblio', 'melon', 'optimus-opt2', 'perpetual-protocol', 'ronin', 'klever', 'coinweb', 'volt-inu-2', 'astrafer', 'concierge-io', 'leash', 'tomochain', 'clover-finance', 'orion-protocol', 'lto-network', 'hshare', 'regen', 'itheum', 'ki', 'covalent', 'unibright', 'kunci-coin', 'ampleforth-governance-token', 'ellipsis', 'superfarm', ], bannedTickers: ['ebtc', 'xbt'], bannedNames: ['ebitcoin'], }; export function parseAddressForParams(addressString) { // Build return obj const addressInfo = { address: '', queryString: null, amount: null, }; // Parse address string for parameters const paramCheck = addressString.split('?'); let cleanAddress = paramCheck[0]; addressInfo.address = cleanAddress; // Check for parameters // only the amount param is currently supported let queryString = null; let amount = null; if (paramCheck.length > 1) { queryString = paramCheck[1]; addressInfo.queryString = queryString; const addrParams = new URLSearchParams(queryString); if (addrParams.has('amount')) { // Amount in XEC try { amount = new BigNumber( parseFloat(addrParams.get('amount')), ).toString(); } catch (err) { amount = null; } } } addressInfo.amount = amount; return addressInfo; } diff --git a/cashtab/src/config/websocket.js b/cashtab/src/config/websocket.js new file mode 100644 index 000000000..b4442ddff --- /dev/null +++ b/cashtab/src/config/websocket.js @@ -0,0 +1,11 @@ +// Copyright (c) 2023 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +'use strict'; + +// note: time is in ms +export const websocket = { + websocketDisconnectedRefreshInterval: 5000, + websocketConnectedRefreshInterval: 10000, +}; diff --git a/cashtab/src/hooks/useWallet.js b/cashtab/src/hooks/useWallet.js index 58683de4a..04611ffc0 100644 --- a/cashtab/src/hooks/useWallet.js +++ b/cashtab/src/hooks/useWallet.js @@ -1,1656 +1,1658 @@ import { useState, useEffect } from 'react'; import usePrevious from 'hooks/usePrevious'; import useInterval from './useInterval'; import BigNumber from 'bignumber.js'; import { loadStoredWallet, isValidStoredWallet, isLegacyMigrationRequired, getHashArrayFromWallet, isActiveWebsocket, getWalletBalanceFromUtxos, toHash160, } from 'utils/cashMethods'; import { isValidCashtabSettings, isValidCashtabCache, isValidContactList, parseInvalidSettingsForMigration, parseInvalidCashtabCacheForMigration, isValidAliasString, } 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 { getAliasServerHistory, getAliasServerState } from 'utils/aliasUtils'; import { ChronikClient } from 'chronik-client'; import { chronik as chronikConfig } from 'config/chronik'; import cashaddr from 'ecashaddrjs'; import * as bip39 from 'bip39'; import * as randomBytes from 'randombytes'; import * as utxolib from '@bitgo/utxo-lib'; +import { websocket as websocketConfig } from 'config/websocket'; const useWallet = () => { const [chronik, setChronik] = useState( new ChronikClient(chronikConfig.urls[0]), ); const previousChronik = usePrevious(chronik); const [walletRefreshInterval, setWalletRefreshInterval] = useState( - currency.websocketDisconnectedRefreshInterval, + websocketConfig.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 [isAliasServerOnline, setIsAliasServerOnline] = useState(true); const [fiatPrice, setFiatPrice] = useState(null); const [apiError, setApiError] = useState(false); const [checkFiatInterval, setCheckFiatInterval] = useState(null); const [hasUpdated, setHasUpdated] = useState(false); const [loading, setLoading] = useState(true); const [chronikIndex, setChronikIndex] = useState(0); const { balances, tokens } = isValidStoredWallet(wallet) ? wallet.state : { balances: {}, tokens: [], }; const previousBalances = usePrevious(balances); const previousTokens = usePrevious(tokens); const tryNextChronikUrl = () => { console.log(`Error with chronik instance at ${chronik._url}`); let currentChronikIndex = chronikIndex; // How many chronik URLs are available? const chronikUrlCount = chronikConfig.urls.length; console.log( `Cashtab has ${ chronikUrlCount - 1 } alternative chronik instances available`, ); // If only one, exit if (chronikUrlCount === 1) { console.log( `There are no backup chronik servers. Please contact an admin to fix the chronik server.`, ); return; } else if (currentChronikIndex < chronikUrlCount - 1) { // If you have another one, use the next one currentChronikIndex += 1; } else { // If you are at the "end" of the array, use the first one currentChronikIndex = 0; } setChronikIndex(currentChronikIndex); console.log( `Creating new chronik client with URL ${chronikConfig.urls[currentChronikIndex]}`, ); return setChronik( new ChronikClient(chronikConfig.urls[currentChronikIndex]), ); }; const deriveAccount = async ({ masterHDNode, path }) => { const node = masterHDNode.derivePath(path); const publicKey = node.publicKey.toString('hex'); const cashAddress = cashaddr.encode('ecash', 'P2PKH', node.identifier); const hash160 = toHash160(cashAddress); return { publicKey, hash160, cashAddress, fundingWif: node.toWIF(), }; }; 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 }) => { // 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, + websocketConfig.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, }, ]; const chronikUtxos = await getUtxosChronik( chronik, hash160AndAddressObjArray, ); const { preliminarySlpUtxos, nonSlpUtxos } = organizeUtxosByType(chronikUtxos); const preliminaryTokensArray = getPreliminaryTokensArray(preliminarySlpUtxos); const { tokens, 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 slpUtxos = finalizeSlpUtxos( preliminarySlpUtxos, updatedTokenInfoById, ); const { parsedTxHistory, txHistoryUpdatedTokenInfoById, txHistoryNewTokensToCache, } = await getTxHistoryChronik( chronik, 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), slpUtxos, nonSlpUtxos, tokens, parsedTxHistory, }; // 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); // Try another chronik instance tryNextChronikUrl(); } }; const getActiveWalletFromLocalForage = async () => { let wallet; try { wallet = await localforage.getItem('wallet'); } catch (err) { console.log(`Error in getActiveWalletFromLocalForage`, err); wallet = null; } return wallet; }; /* @Returns: aliases: [ {alias: 'foo1', address: 'ecash:asdfjhasfd'}, {alias: 'foo2', address: 'ecash:asdfjhasfd'}, ], cachedAliasCount: 0, */ const getAliasesFromLocalForage = async () => { let cachedAliases, cashtabCache; try { cashtabCache = await localforage.getItem('cashtabCache'); cachedAliases = cashtabCache ? cashtabCache.aliasCache : currency.aliasSettings.defaultCashtabCache.aliasCache; } catch (err) { console.log(`Error in getAliasesFromLocalForage`, err); cachedAliases = currency.aliasSettings.defaultCashtabCache.aliasCache; } return cachedAliases; }; 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 => { setLoading(true); let updateSuccess = true; try { await localforage.setItem('contactList', contactListArray); setContactList(contactListArray); } catch (err) { console.log('Error in updateContactList', err); updateSuccess = false; } setLoading(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(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 wallet => { console.log(`migrateLegacyWallet`); console.log(`legacyWallet`, wallet); const mnemonic = wallet.mnemonic; const rootSeedBuffer = await bip39.mnemonicToSeed(mnemonic, ''); const masterHDNode = utxolib.bip32.fromSeed( rootSeedBuffer, utxolib.networks.ecash, ); const Path245 = await deriveAccount({ masterHDNode, path: "m/44'/245'/0'/0/0", }); const Path145 = await deriveAccount({ masterHDNode, path: "m/44'/145'/0'/0/0", }); const Path1899 = await deriveAccount({ 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); } 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 = utxolib.bip32.fromSeed( rootSeedBuffer, utxolib.networks.ecash, ); const Path245 = await deriveAccount({ masterHDNode, path: "m/44'/245'/0'/0/0", }); const Path145 = await deriveAccount({ masterHDNode, path: "m/44'/145'/0'/0/0", }); const Path1899 = await deriveAccount({ masterHDNode, path: "m/44'/1899'/0'/0/0", }); let name = Path1899.cashAddress.slice(6, 11); // 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 => { setLoading(true); let savedWallets; try { savedWallets = await localforage.getItem('savedWallets'); if (savedWallets === null) { savedWallets = []; } } catch (err) { console.log(`Error in getSavedWallets`, 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); } } setLoading(false); return savedWallets; }; const activateWallet = async (currentlyActiveWallet, 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); // 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(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) => { setLoading(true); // Load savedWallets let savedWallets; try { savedWallets = await localforage.getItem('savedWallets'); } catch (err) { console.log( `Error in await localforage.getItem("savedWallets") in renameSavedWallet`, err, ); setLoading(false); 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 setLoading(false); 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()`, err, ); setLoading(false); return false; } setLoading(false); return true; }; const renameActiveWallet = async (activeWallet, oldName, newName) => { setLoading(true); // Load savedWallets let savedWallets; try { savedWallets = await localforage.getItem('savedWallets'); } catch (err) { console.log( `Error in await localforage.getItem("savedWallets") in renameSavedWallet`, err, ); setLoading(false); 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 setLoading(false); return false; } } // Change name of active wallet at its entry in savedWallets 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("wallet", wallet) in renameActiveWallet()`, err, ); setLoading(false); return false; } // Change name of active wallet param in this function activeWallet.name = newName; // Update the active wallet entry in indexedDb try { await localforage.setItem('wallet', activeWallet); } catch (err) { console.log( `Error in localforage.setItem("wallet", ${activeWallet.name}) in renameActiveWallet()`, err, ); setLoading(false); return false; } // Only set the renamed activeWallet in state if no errors earlier in this function setWallet(activeWallet); setLoading(false); return true; }; const deleteWallet = async walletToBeDeleted => { setLoading(true); // 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`, err, ); setLoading(false); 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) { setLoading(false); 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()`, err, ); setLoading(false); return false; } setLoading(false); return true; }; const addNewSavedWallet = async importMnemonic => { setLoading(true); // 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()`, 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.`); setLoading(false); 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}`, err, ); console.log(`savedWallets`, savedWallets); } setLoading(false); 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 and new blocks, as the only logic to happen is first seen notifications and new block alias history updates // 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' && type !== 'BlockConnected') { return; } if (currency.aliasSettings.aliasEnabled) { // only check for new blocks if alias feature is enabled // when new blocks are found, clear and refresh aliasCache if (type === 'BlockConnected') { console.log(`New block found, updating aliasCache`); try { await getLatestAliases(); } catch (err) { console.log( `Error retrieving latest aliases after finding new block`, err, ); } return; } } else { if (type === 'BlockConnected') { return; // temporary disabling of checking for new blocks if the alias is disabled in ticker.js } } // 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( 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 (chronik, wallet, fiatPrice) => { // 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); } const hasChronikUrlChanged = chronik !== previousChronik; if (hasChronikUrlChanged) { console.log(`Chronik URL has changed to ${chronik._url}.`); } let ws = chronikWebsocket; // If chronik URL has changed and ws is not null, close existing websocket if (hasChronikUrlChanged && ws !== null) { console.log(`Closing websocket connection at ${ws._wsUrl}`); ws.close(); } // Initialize websocket if not in state or if chronik URL has changed if (ws === null || hasChronikUrlChanged) { console.log(`Opening websocket connection at ${chronik._wsUrl}`); ws = chronik.ws({ onMessage: msg => { processChronikWsMsg(msg, wallet, fiatPrice); }, autoReconnect: true, 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 + websocketConfig.websocketConnectedRefreshInterval / + 1000 }s`, ); setWalletRefreshInterval( - currency.websocketConnectedRefreshInterval, + websocketConfig.websocketConnectedRefreshInterval, ); }, }); // Need to put ws in state here so that, if the connection fails, it can be cleared for the next chronik URL setChronikWebsocket(ws); // Wait for websocket to be connected: await ws.waitForOpen(); } else { /* If the websocket connection is not null and the chronik URL has not changed, 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); if (currency.aliasSettings.aliasEnabled) { // only sync alias cache if alias feature is enabled await getLatestAliases(); } }; const getLatestAliases = async () => { let cachedAliases; // retrieve cached aliases try { cachedAliases = await getAliasesFromLocalForage(); } catch (err) { console.log( `getLatestAliases(): Error retrieving aliases from localForage`, err, ); return cachedAliases; } // reset to default aliasCache if any unexpected issues are encountered with local storage version if (!cachedAliases || !cachedAliases.aliases) { console.log(`Error in getLatestAliases(): Invalid cachedAliases`); cashtabCache.aliasCache = currency.defaultCashtabCache.aliasCache; setCashtabCache(cashtabCache); // set array into local forage try { await localforage.setItem('cashtabCache', cashtabCache); } catch (err) { console.log( 'Error updateing cashtabCache object in getLatestAliases', err, ); } return cashtabCache.aliasCache; } // clear aliasCache if at least one cached alias is not alphanumeric // NOTE: this is kept here even though alias-server validates this, in order to catch and migrate pre-alias-server wallet caches that may contain non-alphanumeric aliases let invalidAliasFound = false; for (let element of cachedAliases.aliases) { if (!isValidAliasString(element.alias)) { invalidAliasFound = true; } } // if at least one alias is invalid, reset cachedAliases to default if (invalidAliasFound) { cachedAliases = currency.defaultCashtabCache.aliasCache; } // retrieve alias-server state let aliasServerStateRespJson; try { aliasServerStateRespJson = await getAliasServerState(); } catch (err) { console.log( `getLatestAliases(): Error retrieving server state from alias-server`, err, ); } // if bad response from alias-server, retain the existing alias cache by skipping the remainder of this function which updates the alias cache if (!aliasServerStateRespJson) { setIsAliasServerOnline(false); return; } // get the onchain alias count let onchainAliasCount = 0; if (aliasServerStateRespJson) { onchainAliasCount = aliasServerStateRespJson.registeredAliasCount; } // get the cached alias count let cachedAliasCount = 0; if (cachedAliases) { cachedAliasCount = cachedAliases.cachedAliasCount; } // if cache alias count does not match onchain alias count, update cache if (cachedAliasCount !== onchainAliasCount) { console.log( `cache alias count does not match onchain alias count, refreshing aliasCache`, ); // retrieve onchain aliases via alias-server let aliasServerRespJson; try { aliasServerRespJson = await getAliasServerHistory(); } catch (err) { console.log( `getLatestAliases(): Error retrieving aliases from alias-server`, err, ); } // if bad response from alias-server, retain the existing alias cache if (!aliasServerRespJson) { setIsAliasServerOnline(false); return; } let aliasCacheObject = { aliases: aliasServerRespJson, cachedAliasCount: aliasServerRespJson.length, }; cashtabCache.aliasCache = aliasCacheObject; // set array into local forage try { await localforage.setItem('cashtabCache', cashtabCache); } catch (err) { console.log( 'Error updateing cashtabCache object in getLatestAliases', err, ); } cachedAliases = aliasCacheObject; setCashtabCache(cashtabCache); console.log(`aliasCache refresh complete`); } else { console.log( `Server and cache both have ${onchainAliasCount} aliases. Cashtab alias cache is up to date.`, ); } return cachedAliases; }; 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, parse the cache object, finds what param is missing, and sticks it in const migratedCashtabCache = parseInvalidCashtabCacheForMigration(localCashtabCache); localforage.setItem('cashtabCache', migratedCashtabCache); setCashtabCache(migratedCashtabCache); 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, ); // 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]; // Find where the newTokenId is const receivedTokenObjectIndex = tokens.findIndex( x => x.tokenId === newTokenId, ); // Calculate amount received const receivedSlpQty = tokens[receivedTokenObjectIndex].balance.toString(); const receivedSlpTicker = tokens[receivedTokenObjectIndex].info.tokenTicker; const receivedSlpName = tokens[receivedTokenObjectIndex].info.tokenName; // 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 ( new BigNumber(tokens[i].balance).gt( new BigNumber(previousTokens[i].balance), ) ) { 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 = new BigNumber( tokens[i].balance, ).minus(new BigNumber(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); } catch (err) { console.log(`Error fetching BCH Price`); console.log(err); } try { bchPriceJson = await bchPrice.json(); 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(chronik, wallet, fiatPrice) each time chronik, wallet, or fiatPrice changes Use wallet.mnemonic as the useEffect parameter here because we want to run initializeWebsocket(chronik, wallet, fiatPrice) when a new unique wallet is selected, not when the active wallet changes state */ useEffect(async () => { await initializeWebsocket(chronik, wallet, fiatPrice); }, [chronik, wallet.mnemonic, fiatPrice]); return { chronik, wallet, fiatPrice, loading, apiError, contactList, cashtabSettings, cashtabCache, changeCashtabSettings, getActiveWalletFromLocalForage, getWallet, getWalletDetails, getSavedWallets, migrateLegacyWallet, getLatestAliases, getContactListFromLocalForage, getAliasesFromLocalForage, isAliasServerOnline, updateContactList, createWallet: async importMnemonic => { setLoading(true); const newWallet = await createWallet(importMnemonic); setWallet(newWallet); update({ wallet: newWallet, }).finally(() => setLoading(false)); }, activateWallet: async (currentlyActiveWallet, 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, + websocketConfig.websocketDisconnectedRefreshInterval, ); const newWallet = await activateWallet( currentlyActiveWallet, 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;