diff --git a/web/cashtab/src/utils/__tests__/cashMethods.test.js b/web/cashtab/src/utils/__tests__/cashMethods.test.js --- a/web/cashtab/src/utils/__tests__/cashMethods.test.js +++ b/web/cashtab/src/utils/__tests__/cashMethods.test.js @@ -25,6 +25,7 @@ getWalletBalanceFromUtxos, signUtxosByAddress, getUtxoWif, + adjustTokenQtyForDecimals, } from 'utils/cashMethods'; import { currency } from 'components/Common/Ticker'; import { @@ -1378,4 +1379,64 @@ it(`isActiveWebsocket returns false for an active websocket connection with no subscriptions`, () => { expect(isActiveWebsocket(unsubscribedWebsocket)).toBe(false); }); + + it(`correctly adjusts token balance with decimals when passed a BigNumber as the tokenQty param`, () => { + let tokenQty = new BigNumber(6); + let tokenDecimals = 0; + let result = tokenQty.shiftedBy(-1 * tokenDecimals); + expect( + JSON.stringify(adjustTokenQtyForDecimals(tokenQty, tokenDecimals)), + ).toBe(JSON.stringify(new BigNumber(result))); + }); + it(`correctly adjusts token balance with decimals when passed a string as tokenQty param`, () => { + let tokenBalance = '42816543209900000'; + let tokenDecimals = 9; + let result = new BigNumber(tokenBalance).shiftedBy(-1 * tokenDecimals); + + expect( + JSON.stringify( + adjustTokenQtyForDecimals(tokenBalance, tokenDecimals), + ), + ).toBe(JSON.stringify(new BigNumber(result))); + }); + it(`rejects a string passed as the tokenDecimals param`, () => { + let tokenQty = '999995000000000'; + let tokenDecimals = '9'; + let result = + 'tokenQty param must be a string or BigNumber, tokenDecimals must be a number between 0-9 inclusive.'; + + expect( + JSON.stringify(adjustTokenQtyForDecimals(tokenQty, tokenDecimals)), + ).toBe(JSON.stringify(result)); + }); + it(`rejects a number passed as the tokenQty param`, () => { + let tokenQty = 37013482000000000; + let tokenDecimals = 9; + let result = + 'tokenQty param must be a string or BigNumber, tokenDecimals must be a number between 0-9 inclusive.'; + + expect( + JSON.stringify(adjustTokenQtyForDecimals(tokenQty, tokenDecimals)), + ).toBe(JSON.stringify(result)); + }); + it(`rejects a BigNumber passed as the tokenDecimals param`, () => { + let tokenQty = '100000000000000'; + let tokenDecimals = new BigNumber(8); + let result = + 'tokenQty param must be a string or BigNumber, tokenDecimals must be a number between 0-9 inclusive.'; + + expect( + JSON.stringify(adjustTokenQtyForDecimals(tokenQty, tokenDecimals)), + ).toBe(JSON.stringify(result)); + }); + it(`rejects if both params are invalid`, () => { + let tokenQty = 100000000000000; + let tokenDecimals = '8'; + let result = + 'tokenQty param must be a string or BigNumber, tokenDecimals must be a number between 0-9 inclusive.'; + + expect( + JSON.stringify(adjustTokenQtyForDecimals(tokenQty, tokenDecimals)), + ).toBe(JSON.stringify(result)); + }); }); diff --git a/web/cashtab/src/utils/cashMethods.js b/web/cashtab/src/utils/cashMethods.js --- a/web/cashtab/src/utils/cashMethods.js +++ b/web/cashtab/src/utils/cashMethods.js @@ -832,6 +832,125 @@ return hash160Array; }; +export const parseChronikTx = (tx, walletHash160s) => { + const { inputs, outputs } = tx; + // Assign defaults + let incoming = true; + let xecAmount = new BigNumber(0); + let etokenAmount = new BigNumber(0); + const isEtokenTx = 'slpTxData' in tx && typeof tx.slpTxData !== 'undefined'; + + // Iterate over inputs to see if this is an incoming tx (incoming === true) + for (let i = 0; i < inputs.length; i += 1) { + const thisInput = inputs[i]; + const thisInputSendingHash160 = thisInput.outputScript; + for (let j = 0; j < walletHash160s.length; j += 1) { + const thisWalletHash160 = walletHash160s[j]; + if (thisInputSendingHash160.includes(thisWalletHash160)) { + // Then this is an outgoing tx + incoming = false; + // Break out of this for loop once you know this is an incoming tx + break; + } + } + } + // Iterate over outputs to get the amount sent + for (let i = 0; i < outputs.length; i += 1) { + const thisOutput = outputs[i]; + const thisOutputReceivedAtHash160 = thisOutput.outputScript; + // Find amounts at your wallet's addresses + for (let j = 0; j < walletHash160s.length; j += 1) { + const thisWalletHash160 = walletHash160s[j]; + if (thisOutputReceivedAtHash160.includes(thisWalletHash160)) { + // If incoming tx, this is amount received by the user's wallet + // if outgoing tx (incoming === false), then this is a change amount + const thisOutputAmount = new BigNumber(thisOutput.value); + xecAmount = incoming + ? xecAmount.plus(thisOutputAmount) + : xecAmount.minus(thisOutputAmount); + + // Parse token qty if token tx + // Note: edge case this is a token tx that sends XEC to Cashtab recipient but token somewhere else + if (isEtokenTx) { + try { + const thisEtokenAmount = new BigNumber( + thisOutput.slpToken.amount, + ); + + etokenAmount = incoming + ? etokenAmount.plus(thisEtokenAmount) + : etokenAmount.minus(thisEtokenAmount); + } catch (err) { + // edge case described above; in this case there is zero eToken value for this Cashtab recipient, so add 0 + etokenAmount.plus(new BigNumber(0)); + } + } + } + } + // Output amounts not at your wallet are sent amounts if !incoming + if (!incoming) { + const thisOutputAmount = new BigNumber(thisOutput.value); + xecAmount = xecAmount.plus(thisOutputAmount); + if (isEtokenTx) { + try { + const thisEtokenAmount = new BigNumber( + thisOutput.slpToken.amount, + ); + etokenAmount = etokenAmount.plus(thisEtokenAmount); + } catch (err) { + // NB the edge case described above cannot exist in an outgoing tx + // because the eTokens sent originated from this wallet + } + } + } + } + + // Convert from sats to XEC + xecAmount = xecAmount.shiftedBy(-1 * currency.cashDecimals); + // Convert from BigNumber to string + xecAmount = xecAmount.toString(); + etokenAmount = etokenAmount.toString(); + + // Return eToken specific fields if eToken tx + if (isEtokenTx) { + const { slpMeta } = tx.slpTxData; + return { + incoming, + xecAmount, + isEtokenTx, + etokenAmount, + slpMeta, + }; + } + // Otherwise do not include these fields + return { incoming, xecAmount, isEtokenTx }; +}; + +export const checkWalletForTokenInfo = (tokenId, wallet) => { + /* + Check wallet for cached information about a given tokenId + Return {decimals: tokenDecimals, name: tokenName, ticker: tokenTicker} + If this tokenId does not exist in wallet, return false + */ + try { + const { tokens } = wallet.state; + for (let i = 0; i < tokens.length; i += 1) { + const thisTokenId = tokens[i].tokenId; + if (tokenId === thisTokenId) { + return { + decimals: tokens[i].info.decimals, + ticker: tokens[i].info.tokenTicker, + name: tokens[i].info.tokenName, + }; + } + } + } catch (err) { + return false; + } + + return false; +}; + export const isActiveWebsocket = ws => { // Return true if websocket is connected and subscribed // Otherwise return false @@ -845,3 +964,31 @@ ws._subs.length > 0 ); }; + +export const adjustTokenQtyForDecimals = (tokenQty, tokenDecimals) => { + let tokenQtyAdjustedWithDecimals; + let convertedTokenDecimals; + let convertedTokenQty; + try { + if ( + (typeof tokenDecimals === 'number' && + tokenDecimals >= 0 && + tokenDecimals <= 9 && + typeof tokenQty === 'string') || + BigNumber.isBigNumber(tokenQty) + ) { + convertedTokenDecimals = new BigNumber(tokenDecimals); + convertedTokenQty = new BigNumber(tokenQty); + tokenQtyAdjustedWithDecimals = convertedTokenQty.shiftedBy( + -1 * convertedTokenDecimals, + ); + return new BigNumber(tokenQtyAdjustedWithDecimals); + } else { + throw new TypeError( + 'tokenQty param must be a string or BigNumber, tokenDecimals must be a number between 0-9 inclusive.', + ); + } + } catch (error) { + return error.message; + } +};