diff --git a/web/cashtab/.env b/web/cashtab/.env --- a/web/cashtab/.env +++ b/web/cashtab/.env @@ -1,3 +1,3 @@ REACT_APP_NETWORK=mainnet -REACT_APP_API=https://rest.kingbch.com/v3/ -REACT_APP_API_TEST=https://free-test.fullstack.cash/v3/ \ No newline at end of file +REACT_APP_BCHA_APIS=https://rest.kingbch.com/v3/,https://wallet-service-prod.bitframe.org/v3/ +REACT_APP_BCHA_APIS_TEST=https://free-test.fullstack.cash/v3/ \ No newline at end of file diff --git a/web/cashtab/README.md b/web/cashtab/README.md --- a/web/cashtab/README.md +++ b/web/cashtab/README.md @@ -9,7 +9,7 @@ ## Development -Cashtab relies on some modules that retain legacy dependencies. NPM version 7 or later no longer supports automatic resolution of these peer dependencies. To successfully install modules such as `qrcode.react`, with NPM > 7, run `npm install` with the flag `--legacy-peer-deps` +CashTab relies on some modules that retain legacy dependencies. NPM version 7 or later no longer supports automatic resolution of these peer dependencies. To successfully install modules such as `qrcode.react`, with NPM > 7, run `npm install` with the flag `--legacy-peer-deps` ``` npm install --legacy-peer-deps @@ -50,6 +50,22 @@ docker-compose up ``` +## Redundant APIs + +CashTab accepts multiple instances of `bch-api` as its backend. Input your desired API URLs separated commas into the `REACT_APP_BCHA_APIS` variable. For example, to run CashTab with three redundant APIs, use: + +``` +REACT_APP_BCHA_APIS=https://rest.kingbch.com/v3/,https://wallet-service-prod.bitframe.org/v3/,https://free-main.fullstack.cash/v3/ +``` + +You can also run CashTab with a single API, e.g. + +``` +REACT_APP_BCHA_APIS=https://rest.kingbch.com/v3/ +``` + +CashTab will start with the first API in your list. If it receives an error from that API, it will try the next one. + Navigate to `localhost:8080` to see the app. ## CashTab Roadmap @@ -58,4 +74,4 @@ - Transaction history - Simple Ledger Postage Protocol Support -- Cashtab as browser extension +- CashTab browser extension diff --git a/web/cashtab/src/hooks/__tests__/useBCH.test.js b/web/cashtab/src/hooks/__tests__/useBCH.test.js --- a/web/cashtab/src/hooks/__tests__/useBCH.test.js +++ b/web/cashtab/src/hooks/__tests__/useBCH.test.js @@ -12,26 +12,28 @@ process = { env: { REACT_APP_NETWORK: `testnet`, - REACT_APP_API_TEST: `https://free-test.fullstack.cash/v3/`, - REACT_APP_API: `https://free-main.fullstack.cash/v3/`, + REACT_APP_BCHA_APIS: + 'https://rest.kingbch.com/v3/,https://wallet-service-prod.bitframe.org/v3/,notevenaurl,https://rest.kingbch.com/v3/', + REACT_APP_BCHA_APIS_TEST: + 'https://free-test.fullstack.cash/v3/', }, }; const { getRestUrl } = useBCH(); const expectedApiUrl = `https://free-test.fullstack.cash/v3/`; - expect(getRestUrl()).toBe(expectedApiUrl); + expect(getRestUrl(0)).toBe(expectedApiUrl); }); - it('gets Rest Api Url on mainnet', () => { + it('gets primary Rest API URL on mainnet', () => { process = { env: { - REACT_APP_NETWORK: `mainnet`, - REACT_APP_API_TEST: `https://free-test.fullstack.cash/v3/`, - REACT_APP_API: `https://free-main.fullstack.cash/v3/`, + REACT_APP_BCHA_APIS: + 'https://rest.kingbch.com/v3/,https://wallet-service-prod.bitframe.org/v3/,notevenaurl,https://rest.kingbch.com/v3/', + REACT_APP_NETWORK: 'mainnet', }, }; const { getRestUrl } = useBCH(); - const expectedApiUrl = `https://free-main.fullstack.cash/v3/`; - expect(getRestUrl()).toBe(expectedApiUrl); + const expectedApiUrl = `https://rest.kingbch.com/v3/`; + expect(getRestUrl(0)).toBe(expectedApiUrl); }); it('calculates fee correctly for 2 P2PKH outputs', () => { diff --git a/web/cashtab/src/hooks/useBCH.js b/web/cashtab/src/hooks/useBCH.js --- a/web/cashtab/src/hooks/useBCH.js +++ b/web/cashtab/src/hooks/useBCH.js @@ -11,10 +11,14 @@ MAX_UNCONFIRMED_TXS: 64, }; - const getRestUrl = () => - process.env.REACT_APP_NETWORK === `mainnet` - ? process.env.REACT_APP_API - : process.env.REACT_APP_API_TEST; + const getRestUrl = (apiIndex = 0) => { + const apiString = + process.env.REACT_APP_NETWORK === `mainnet` + ? process.env.REACT_APP_BCHA_APIS + : process.env.REACT_APP_BCHA_APIS_TEST; + const apiArray = apiString.split(','); + return apiArray[apiIndex]; + }; const getTxHistory = async (BCH, addresses) => { let txHistoryResponse; @@ -679,10 +683,10 @@ } }; - const getBCH = (fromWindowObject = true) => { + const getBCH = (apiIndex = 0, fromWindowObject = true) => { if (fromWindowObject && window.SlpWallet) { const SlpWallet = new window.SlpWallet('', { - restURL: getRestUrl(), + restURL: getRestUrl(apiIndex), }); return SlpWallet.bchjs; } diff --git a/web/cashtab/src/hooks/useWallet.js b/web/cashtab/src/hooks/useWallet.js --- a/web/cashtab/src/hooks/useWallet.js +++ b/web/cashtab/src/hooks/useWallet.js @@ -24,7 +24,8 @@ }); const { getBCH, getUtxos, getSlpBalancesAndUtxos, getTxHistory } = useBCH(); const [loading, setLoading] = useState(true); - const [BCH] = useState(getBCH()); + const [apiIndex, setApiIndex] = useState(0); + const [BCH, setBCH] = useState(getBCH(apiIndex)); const [utxos, setUtxos] = useState(null); const { balances, tokens, slpBalancesAndUtxos, txHistory } = walletState; const previousBalances = usePrevious(balances); @@ -32,6 +33,40 @@ const previousWallet = usePrevious(wallet); const previousUtxos = usePrevious(utxos); + // 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 normalizeSlpBalancesAndUtxos = (slpBalancesAndUtxos, wallet) => { const Accounts = [wallet.Path245, wallet.Path145]; slpBalancesAndUtxos.nonSlpUtxos.forEach(utxo => { @@ -122,7 +157,7 @@ //console.log(`utxos`, utxos); // If an error is returned or utxos from only 1 address are returned - if (utxos.error || utxos.length < 2) { + if (!utxos || _.isEmpty(utxos) || utxos.error || utxos.length < 2) { // Throw error here to prevent more attempted api calls // as you are likely already at rate limits throw new Error('Error fetching utxos'); @@ -182,6 +217,9 @@ // 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"); }; @@ -737,7 +775,12 @@ ); // Subscribe to new addresses - ws.send(JSON.stringify({ op: 'addr_sub', addr: cashAddress })); + ws.send( + JSON.stringify({ + op: 'addr_sub', + addr: cashAddress, + }), + ); console.log(`Subscribed to BCH address at ${cashAddress}`); // Subscribe to SLP address ws.send( @@ -841,7 +884,10 @@ // Subscribe to BCH address newWs.send( - JSON.stringify({ op: 'addr_sub', addr: cashAddress }), + JSON.stringify({ + op: 'addr_sub', + addr: cashAddress, + }), ); console.log(`Subscribed to BCH address at ${cashAddress}`); @@ -865,7 +911,10 @@ // Unsubscribe on close to prevent double subscribing //{"op":"addr_unsub", "addr":"$bitcoin_address"} newWs.send( - JSON.stringify({ op: 'addr_unsub', addr: cashAddress }), + JSON.stringify({ + op: 'addr_unsub', + addr: cashAddress, + }), ); console.log(`Unsubscribed from BCH address at ${cashAddress}`); newWs.send(