diff --git a/web/cashtab/src/utils/__mocks__/mockTxBuilderObj.js b/web/cashtab/src/utils/__mocks__/mockTxBuilderObj.js
new file mode 100644
index 000000000..e876332b6
--- /dev/null
+++ b/web/cashtab/src/utils/__mocks__/mockTxBuilderObj.js
@@ -0,0 +1,476 @@
+// satoshisToSend: 600
+// totalInputUtxos: 1100
+// txFee: 455
+// output:
+//    bitcoincash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqqkm80dnl6,6
+export const mockOneToOneSendXecTxBuilderObj = {
+    transaction: {
+        prevTxMap: {
+            '9a6bdfba2a33ce3e1615d19651cdeb8771e3228ef2706179f113ec153ffd378a:0': 0,
+        },
+        network: {
+            hashGenesisBlock:
+                '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f',
+            port: 8333,
+            portRpc: 8332,
+            protocol: {
+                magic: 3652501241,
+            },
+            seedsDns: [
+                'seed.bitcoinabc.org',
+                'seed-abc.bitcoinforks.org',
+                'btccash-seeder.bitcoinunlimited.info',
+                'seed.bitprim.org',
+                'seed.deadalnix.me',
+                'seeder.criptolayer.net',
+            ],
+            versions: {
+                bip32: {
+                    private: 76066276,
+                    public: 76067358,
+                },
+                bip44: 145,
+                private: 128,
+                public: 0,
+                scripthash: 5,
+                messagePrefix: '\u0018BitcoinCash Signed Message:\n',
+            },
+            name: 'BitcoinCash',
+            per1: 100000000,
+            unit: 'BCH',
+            testnet: false,
+            messagePrefix: '\u0018BitcoinCash Signed Message:\n',
+            bip32: {
+                public: 76067358,
+                private: 76066276,
+            },
+            pubKeyHash: 0,
+            scriptHash: 5,
+            wif: 128,
+            dustThreshold: null,
+        },
+        maximumFeeRate: 2500,
+        inputs: [
+            {
+                value: 1100,
+                pubKeys: [
+                    {
+                        type: 'Buffer',
+                        data: [
+                            3, 30, 148, 131, 7, 74, 159, 14, 231, 56, 1, 49,
+                            168, 112, 237, 190, 148, 3, 231, 184, 7, 164, 181,
+                            97, 27, 1, 84, 10, 21, 15, 106, 164, 84,
+                        ],
+                    },
+                ],
+                signatures: [
+                    {
+                        type: 'Buffer',
+                        data: [
+                            48, 68, 2, 32, 14, 123, 142, 157, 227, 237, 219, 37,
+                            223, 63, 183, 205, 107, 192, 95, 233, 228, 86, 51,
+                            13, 114, 64, 239, 83, 215, 155, 206, 1, 13, 5, 169,
+                            166, 2, 32, 100, 18, 61, 85, 212, 59, 28, 148, 8,
+                            231, 70, 207, 116, 66, 40, 236, 58, 92, 76, 223, 74,
+                            207, 145, 202, 252, 102, 218, 43, 226, 229, 199, 60,
+                            65,
+                        ],
+                    },
+                ],
+                signScript: {
+                    type: 'Buffer',
+                    data: [
+                        118, 169, 20, 152, 70, 182, 179, 143, 247, 19, 51, 74,
+                        193, 159, 227, 207, 133, 26, 31, 152, 192, 123, 0, 136,
+                        172,
+                    ],
+                },
+                signType: 'pubkeyhash',
+                prevOutScript: {
+                    type: 'Buffer',
+                    data: [
+                        118, 169, 20, 152, 70, 182, 179, 143, 247, 19, 51, 74,
+                        193, 159, 227, 207, 133, 26, 31, 152, 192, 123, 0, 136,
+                        172,
+                    ],
+                },
+                prevOutType: 'pubkeyhash',
+                witness: false,
+            },
+        ],
+        bitcoinCash: true,
+        tx: {
+            version: 2,
+            locktime: 0,
+            ins: [
+                {
+                    hash: {
+                        type: 'Buffer',
+                        data: [
+                            154, 107, 223, 186, 42, 51, 206, 62, 22, 21, 209,
+                            150, 81, 205, 235, 135, 113, 227, 34, 142, 242, 112,
+                            97, 121, 241, 19, 236, 21, 63, 253, 55, 138,
+                        ],
+                    },
+                    index: 0,
+                    script: {
+                        type: 'Buffer',
+                        data: [],
+                    },
+                    sequence: 4294967295,
+                    witness: [],
+                },
+            ],
+            outs: [
+                {
+                    script: {
+                        type: 'Buffer',
+                        data: [
+                            118, 169, 20, 152, 70, 182, 179, 143, 247, 19, 51,
+                            74, 193, 159, 227, 207, 133, 26, 31, 152, 192, 123,
+                            0, 136, 172,
+                        ],
+                    },
+                    value: 600,
+                },
+            ],
+        },
+    },
+    DEFAULT_SEQUENCE: 4294967295,
+    hashTypes: {
+        SIGHASH_ALL: 1,
+        SIGHASH_NONE: 2,
+        SIGHASH_SINGLE: 3,
+        SIGHASH_ANYONECANPAY: 128,
+        SIGHASH_BITCOINCASH_BIP143: 64,
+        ADVANCED_TRANSACTION_MARKER: 0,
+        ADVANCED_TRANSACTION_FLAG: 1,
+    },
+    signatureAlgorithms: {
+        ECDSA: 0,
+        SCHNORR: 1,
+    },
+    bip66: {},
+    bip68: {},
+    p2shInput: false,
+};
+
+// satoshisToSend: 1650
+// totalInputUtxos: 20586
+// txFee: 1186
+// outputs:
+//    bitcoincash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0ul96a2ens,5.5
+//    bitcoincash:qq9h6d0a5q65fgywv4ry64x04ep906mdku7ymranw3,5.5
+//    bitcoincash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqqkm80dnl6,5.5
+export const mockOneToManySendXecTxBuilderObj = {
+    transaction: {
+        prevTxMap: {
+            '5be79a985c819b4f6393ab7aba48a2f1ce9d13320a8dbb674f6ba55c7f96b7cb:0': 0,
+            '19cf1a4004a4cd67a953239b0cea35c9690e878a1abb62512b34cb9e0c137500:0': 1,
+            '19cf1a4004a4cd67a953239b0cea35c9690e878a1abb62512b34cb9e0c137500:1': 2,
+        },
+        network: {
+            hashGenesisBlock:
+                '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f',
+            port: 8333,
+            portRpc: 8332,
+            protocol: {
+                magic: 3652501241,
+            },
+            seedsDns: [
+                'seed.bitcoinabc.org',
+                'seed-abc.bitcoinforks.org',
+                'btccash-seeder.bitcoinunlimited.info',
+                'seed.bitprim.org',
+                'seed.deadalnix.me',
+                'seeder.criptolayer.net',
+            ],
+            versions: {
+                bip32: {
+                    private: 76066276,
+                    public: 76067358,
+                },
+                bip44: 145,
+                private: 128,
+                public: 0,
+                scripthash: 5,
+                messagePrefix: '\u0018BitcoinCash Signed Message:\n',
+            },
+            name: 'BitcoinCash',
+            per1: 100000000,
+            unit: 'BCH',
+            testnet: false,
+            messagePrefix: '\u0018BitcoinCash Signed Message:\n',
+            bip32: {
+                public: 76067358,
+                private: 76066276,
+            },
+            pubKeyHash: 0,
+            scriptHash: 5,
+            wif: 128,
+            dustThreshold: null,
+        },
+        maximumFeeRate: 2500,
+        inputs: [
+            {
+                value: 1000,
+                pubKeys: [
+                    {
+                        type: 'Buffer',
+                        data: [
+                            3, 30, 148, 131, 7, 74, 159, 14, 231, 56, 1, 49,
+                            168, 112, 237, 190, 148, 3, 231, 184, 7, 164, 181,
+                            97, 27, 1, 84, 10, 21, 15, 106, 164, 84,
+                        ],
+                    },
+                ],
+                signatures: [
+                    {
+                        type: 'Buffer',
+                        data: [
+                            48, 69, 2, 33, 0, 224, 8, 173, 44, 219, 146, 255,
+                            243, 66, 255, 60, 118, 32, 120, 36, 228, 23, 230,
+                            39, 9, 231, 51, 235, 155, 70, 173, 162, 75, 48, 23,
+                            180, 101, 2, 32, 94, 137, 18, 23, 35, 68, 146, 141,
+                            57, 11, 155, 214, 31, 239, 219, 58, 41, 237, 26, 25,
+                            201, 47, 1, 254, 64, 183, 107, 91, 229, 209, 106,
+                            134, 65,
+                        ],
+                    },
+                ],
+                signScript: {
+                    type: 'Buffer',
+                    data: [
+                        118, 169, 20, 152, 70, 182, 179, 143, 247, 19, 51, 74,
+                        193, 159, 227, 207, 133, 26, 31, 152, 192, 123, 0, 136,
+                        172,
+                    ],
+                },
+                signType: 'pubkeyhash',
+                prevOutScript: {
+                    type: 'Buffer',
+                    data: [
+                        118, 169, 20, 152, 70, 182, 179, 143, 247, 19, 51, 74,
+                        193, 159, 227, 207, 133, 26, 31, 152, 192, 123, 0, 136,
+                        172,
+                    ],
+                },
+                prevOutType: 'pubkeyhash',
+                witness: false,
+            },
+            {
+                value: 600,
+                pubKeys: [
+                    {
+                        type: 'Buffer',
+                        data: [
+                            3, 30, 148, 131, 7, 74, 159, 14, 231, 56, 1, 49,
+                            168, 112, 237, 190, 148, 3, 231, 184, 7, 164, 181,
+                            97, 27, 1, 84, 10, 21, 15, 106, 164, 84,
+                        ],
+                    },
+                ],
+                signatures: [
+                    {
+                        type: 'Buffer',
+                        data: [
+                            48, 69, 2, 33, 0, 195, 158, 165, 43, 132, 105, 113,
+                            114, 23, 168, 71, 99, 197, 87, 40, 47, 40, 110, 147,
+                            215, 147, 216, 116, 59, 210, 157, 182, 155, 2, 255,
+                            68, 211, 2, 32, 66, 120, 191, 80, 126, 119, 43, 247,
+                            94, 7, 31, 103, 171, 189, 152, 154, 215, 202, 239,
+                            163, 61, 176, 130, 109, 228, 54, 12, 76, 0, 212, 31,
+                            191, 65,
+                        ],
+                    },
+                ],
+                signScript: {
+                    type: 'Buffer',
+                    data: [
+                        118, 169, 20, 152, 70, 182, 179, 143, 247, 19, 51, 74,
+                        193, 159, 227, 207, 133, 26, 31, 152, 192, 123, 0, 136,
+                        172,
+                    ],
+                },
+                signType: 'pubkeyhash',
+                prevOutScript: {
+                    type: 'Buffer',
+                    data: [
+                        118, 169, 20, 152, 70, 182, 179, 143, 247, 19, 51, 74,
+                        193, 159, 227, 207, 133, 26, 31, 152, 192, 123, 0, 136,
+                        172,
+                    ],
+                },
+                prevOutType: 'pubkeyhash',
+                witness: false,
+            },
+            {
+                value: 18986,
+                pubKeys: [
+                    {
+                        type: 'Buffer',
+                        data: [
+                            3, 30, 148, 131, 7, 74, 159, 14, 231, 56, 1, 49,
+                            168, 112, 237, 190, 148, 3, 231, 184, 7, 164, 181,
+                            97, 27, 1, 84, 10, 21, 15, 106, 164, 84,
+                        ],
+                    },
+                ],
+                signatures: [
+                    {
+                        type: 'Buffer',
+                        data: [
+                            48, 69, 2, 33, 0, 135, 77, 245, 24, 246, 28, 182,
+                            147, 160, 170, 164, 162, 110, 145, 86, 219, 183,
+                            218, 112, 251, 254, 145, 158, 49, 214, 144, 157,
+                            185, 166, 253, 116, 42, 2, 32, 80, 178, 234, 92, 86,
+                            155, 55, 122, 238, 144, 189, 174, 28, 69, 217, 241,
+                            169, 124, 217, 253, 159, 149, 183, 191, 193, 47, 50,
+                            151, 254, 150, 167, 180, 65,
+                        ],
+                    },
+                ],
+                signScript: {
+                    type: 'Buffer',
+                    data: [
+                        118, 169, 20, 152, 70, 182, 179, 143, 247, 19, 51, 74,
+                        193, 159, 227, 207, 133, 26, 31, 152, 192, 123, 0, 136,
+                        172,
+                    ],
+                },
+                signType: 'pubkeyhash',
+                prevOutScript: {
+                    type: 'Buffer',
+                    data: [
+                        118, 169, 20, 152, 70, 182, 179, 143, 247, 19, 51, 74,
+                        193, 159, 227, 207, 133, 26, 31, 152, 192, 123, 0, 136,
+                        172,
+                    ],
+                },
+                prevOutType: 'pubkeyhash',
+                witness: false,
+            },
+        ],
+        bitcoinCash: true,
+        tx: {
+            version: 2,
+            locktime: 0,
+            ins: [
+                {
+                    hash: {
+                        type: 'Buffer',
+                        data: [
+                            91, 231, 154, 152, 92, 129, 155, 79, 99, 147, 171,
+                            122, 186, 72, 162, 241, 206, 157, 19, 50, 10, 141,
+                            187, 103, 79, 107, 165, 92, 127, 150, 183, 203,
+                        ],
+                    },
+                    index: 0,
+                    script: {
+                        type: 'Buffer',
+                        data: [],
+                    },
+                    sequence: 4294967295,
+                    witness: [],
+                },
+                {
+                    hash: {
+                        type: 'Buffer',
+                        data: [
+                            25, 207, 26, 64, 4, 164, 205, 103, 169, 83, 35, 155,
+                            12, 234, 53, 201, 105, 14, 135, 138, 26, 187, 98,
+                            81, 43, 52, 203, 158, 12, 19, 117, 0,
+                        ],
+                    },
+                    index: 0,
+                    script: {
+                        type: 'Buffer',
+                        data: [],
+                    },
+                    sequence: 4294967295,
+                    witness: [],
+                },
+                {
+                    hash: {
+                        type: 'Buffer',
+                        data: [
+                            25, 207, 26, 64, 4, 164, 205, 103, 169, 83, 35, 155,
+                            12, 234, 53, 201, 105, 14, 135, 138, 26, 187, 98,
+                            81, 43, 52, 203, 158, 12, 19, 117, 0,
+                        ],
+                    },
+                    index: 1,
+                    script: {
+                        type: 'Buffer',
+                        data: [],
+                    },
+                    sequence: 4294967295,
+                    witness: [],
+                },
+            ],
+            outs: [
+                {
+                    script: {
+                        type: 'Buffer',
+                        data: [
+                            118, 169, 20, 246, 39, 229, 16, 1, 165, 26, 26, 146,
+                            216, 146, 120, 8, 112, 19, 115, 207, 41, 38, 127,
+                            136, 172,
+                        ],
+                    },
+                    value: 550,
+                },
+                {
+                    script: {
+                        type: 'Buffer',
+                        data: [
+                            118, 169, 20, 11, 125, 53, 253, 160, 53, 68, 160,
+                            142, 101, 70, 77, 84, 207, 174, 66, 87, 235, 109,
+                            183, 136, 172,
+                        ],
+                    },
+                    value: 550,
+                },
+                {
+                    script: {
+                        type: 'Buffer',
+                        data: [
+                            118, 169, 20, 152, 70, 182, 179, 143, 247, 19, 51,
+                            74, 193, 159, 227, 207, 133, 26, 31, 152, 192, 123,
+                            0, 136, 172,
+                        ],
+                    },
+                    value: 550,
+                },
+                {
+                    script: {
+                        type: 'Buffer',
+                        data: [
+                            118, 169, 20, 152, 70, 182, 179, 143, 247, 19, 51,
+                            74, 193, 159, 227, 207, 133, 26, 31, 152, 192, 123,
+                            0, 136, 172,
+                        ],
+                    },
+                    value: 17750,
+                },
+            ],
+        },
+    },
+    DEFAULT_SEQUENCE: 4294967295,
+    hashTypes: {
+        SIGHASH_ALL: 1,
+        SIGHASH_NONE: 2,
+        SIGHASH_SINGLE: 3,
+        SIGHASH_ANYONECANPAY: 128,
+        SIGHASH_BITCOINCASH_BIP143: 64,
+        ADVANCED_TRANSACTION_MARKER: 0,
+        ADVANCED_TRANSACTION_FLAG: 1,
+    },
+    signatureAlgorithms: {
+        ECDSA: 0,
+        SCHNORR: 1,
+    },
+    bip66: {},
+    bip68: {},
+    p2shInput: false,
+};
diff --git a/web/cashtab/src/utils/__tests__/cashMethods.test.js b/web/cashtab/src/utils/__tests__/cashMethods.test.js
index a4600f7f6..070ae9245 100644
--- a/web/cashtab/src/utils/__tests__/cashMethods.test.js
+++ b/web/cashtab/src/utils/__tests__/cashMethods.test.js
@@ -1,1392 +1,1573 @@
 import { ValidationError } from 'ecashaddrjs';
 import BigNumber from 'bignumber.js';
 import {
     fromSmallestDenomination,
     batchArray,
     flattenContactList,
     flattenBatchedHydratedUtxos,
     loadStoredWallet,
     isValidStoredWallet,
     fromLegacyDecimals,
     convertToEcashPrefix,
     checkNullUtxosForTokenStatus,
     confirmNonEtokenUtxos,
     isLegacyMigrationRequired,
     toLegacyCash,
     toLegacyToken,
     toLegacyCashArray,
     convertEtokenToEcashAddr,
     parseOpReturn,
     isExcludedUtxo,
     whichUtxosWereAdded,
     whichUtxosWereConsumed,
     addNewHydratedUtxos,
     removeConsumedUtxos,
     getUtxoCount,
     areAllUtxosIncludedInIncrementallyHydratedUtxos,
     convertEcashtoEtokenAddr,
     getHashArrayFromWallet,
     parseChronikTx,
     checkWalletForTokenInfo,
     isActiveWebsocket,
     parseXecSendValue,
     getChangeAddressFromInputUtxos,
     generateOpReturnScript,
     generateTxInput,
+    generateTxOutput,
+    toSmallestDenomination,
 } from 'utils/cashMethods';
 import { currency } from 'components/Common/Ticker';
 import {
     unbatchedArray,
     arrayBatchedByThree,
 } from '../__mocks__/mockBatchedArrays';
 import {
     validAddressArrayInput,
     validAddressArrayInputMixedPrefixes,
     validAddressArrayOutput,
     validLargeAddressArrayInput,
     validLargeAddressArrayOutput,
     invalidAddressArrayInput,
 } from '../__mocks__/mockAddressArray';
 
 import {
     unflattenedHydrateUtxosResponse,
     flattenedHydrateUtxosResponse,
 } from '../__mocks__/flattenBatchedHydratedUtxosMocks';
 import {
     cachedUtxos,
     utxosLoadedFromCache,
 } from '../__mocks__/mockCachedUtxos';
 import {
     validStoredWallet,
     invalidStoredWallet,
 } from '../__mocks__/mockStoredWallets';
 
 import {
     mockTxDataResults,
     mockNonEtokenUtxos,
     mockTxDataResultsWithEtoken,
     mockHydratedUtxosWithNullValues,
     mockHydratedUtxosWithNullValuesSetToFalse,
 } from '../__mocks__/nullUtxoMocks';
 
 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 {
     excludedUtxoAlpha,
     excludedUtxoBeta,
     includedUtxoAlpha,
     includedUtxoBeta,
     previousUtxosObjUtxoArray,
     previousUtxosTemplate,
     currentUtxosAfterSingleXecReceiveTxTemplate,
     utxosAddedBySingleXecReceiveTxTemplate,
     previousUtxosBeforeSingleXecReceiveTx,
     currentUtxosAfterSingleXecReceiveTx,
     utxosAddedBySingleXecReceiveTx,
     currentUtxosAfterMultiXecReceiveTxTemplate,
     utxosAddedByMultiXecReceiveTxTemplate,
     previousUtxosBeforeMultiXecReceiveTx,
     currentUtxosAfterMultiXecReceiveTx,
     utxosAddedByMultiXecReceiveTx,
     currentUtxosAfterEtokenReceiveTxTemplate,
     utxosAddedByEtokenReceiveTxTemplate,
     previousUtxosBeforeEtokenReceiveTx,
     currentUtxosAfterEtokenReceiveTx,
     utxosAddedByEtokenReceiveTx,
     previousUtxosBeforeSendAllTxTemplate,
     currentUtxosAfterSendAllTxTemplate,
     previousUtxosBeforeSendAllTx,
     currentUtxosAfterSendAllTx,
     previousUtxosBeforeSingleXecSendTx,
     currentUtxosAfterSingleXecSendTx,
     utxosAddedBySingleXecSendTx,
     currentUtxosAfterSingleXecSendTxTemplate,
     utxosAddedBySingleXecSendTxTemplate,
     currentUtxosAfterEtokenSendTxTemplate,
     utxosAddedByEtokenSendTxTemplate,
     previousUtxosBeforeEtokenSendTx,
     currentUtxosAfterEtokenSendTx,
     utxosAddedByEtokenSendTx,
     utxosConsumedByEtokenSendTx,
     utxosConsumedByEtokenSendTxTemplate,
     utxosConsumedBySingleXecSendTx,
     utxosConsumedBySingleXecSendTxTemplate,
     utxosConsumedBySendAllTx,
     utxosConsumedBySendAllTxTemplate,
     hydratedUtxoDetailsBeforeAddingTemplate,
     hydratedUtxoDetailsAfterAddingSingleUtxoTemplate,
     newHydratedUtxosSingleTemplate,
     addedHydratedUtxosOverTwenty,
     existingHydratedUtxoDetails,
     existingHydratedUtxoDetailsAfterAdd,
     hydratedUtxoDetailsBeforeConsumedTemplate,
     consumedUtxoTemplate,
     hydratedUtxoDetailsAfterRemovingConsumedUtxoTemplate,
     consumedUtxos,
     hydratedUtxoDetailsBeforeRemovingConsumedUtxos,
     hydratedUtxoDetailsAfterRemovingConsumedUtxos,
     consumedUtxosMoreThanTwenty,
     hydratedUtxoDetailsAfterRemovingMoreThanTwentyConsumedUtxos,
     consumedUtxosMoreThanTwentyInRandomObjects,
     utxoCountMultiTemplate,
     utxoCountSingleTemplate,
     incrementalUtxosTemplate,
     incrementallyHydratedUtxosTemplate,
     incrementallyHydratedUtxosTemplateMissing,
     utxosAfterSentTxIncremental,
     incrementallyHydratedUtxosAfterProcessing,
     incrementallyHydratedUtxosAfterProcessingOneMissing,
 } from '../__mocks__/incrementalUtxoMocks';
 import mockLegacyWallets from 'hooks/__mocks__/mockLegacyWallets';
 import BCHJS from '@psf/bch-js';
+import sendBCHMock from '../../hooks/__mocks__/sendBCH';
 import {
     lambdaHash160s,
     lambdaIncomingXecTx,
     lambdaOutgoingXecTx,
     lambdaIncomingEtokenTx,
     lambdaOutgoingEtokenTx,
     activeWebsocketAlpha,
     disconnectedWebsocketAlpha,
     unsubscribedWebsocket,
 } from '../__mocks__/chronikWs';
-import sendBCHMock from '../../hooks/__mocks__/sendBCH';
 import mockReturnGetSlpBalancesAndUtxos from '../../hooks/__mocks__/mockReturnGetSlpBalancesAndUtxos';
+import {
+    mockOneToOneSendXecTxBuilderObj,
+    mockOneToManySendXecTxBuilderObj,
+} from '../__mocks__/mockTxBuilderObj';
 
 it(`getChangeAddressFromInputUtxos() returns a correct change address from a valid inputUtxo`, () => {
     const BCH = new BCHJS();
     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(
         BCH,
         inputUtxo,
         wallet,
     );
     expect(changeAddress).toStrictEqual(inputUtxo[0].address);
 });
 
 it(`getChangeAddressFromInputUtxos() throws error upon a malformed input utxo`, () => {
     const BCH = new BCHJS();
     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(BCH, 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 BCH = new BCHJS();
     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(BCH, invalidInputUtxo, wallet);
     } catch (err) {
         thrownError = err;
     }
     expect(thrownError.message).toStrictEqual('Invalid input utxo');
 });
 
 it(`getChangeAddressFromInputUtxos() throws an error upon a null inputUtxos param`, () => {
     const BCH = new BCHJS();
     const { wallet } = sendBCHMock;
     const inputUtxo = null;
 
     let thrownError;
     try {
         getChangeAddressFromInputUtxos(BCH, 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 BCH = new BCHJS();
     const optionalOpReturnMsg = 'testing generateOpReturnScript()';
     const encryptionFlag = true;
     const airdropFlag = false;
     const airdropTokenId = null;
     const mockEncryptedEj =
         '04688f9907fe3c7c0b78a73c4ab4f75e15e7e2b79641add519617086126fe6f6b1405a14eed48e90c9c8c0fc77f0f36984a78173e76ce51f0a44af94b59e9da703c9ff82758cfdb9cc46437d662423400fb731d3bfc1df0599279356ca261213fbb40d398c041e1bac966afed1b404581ab1bcfcde1fa039d53b7c7b70e8edf26d64bea9fbeed24cc80909796e6af5863707fa021f2a2ebaa2fe894904702be19d';
 
     const encodedScript = generateOpReturnScript(
         BCH,
         optionalOpReturnMsg,
         encryptionFlag,
         airdropFlag,
         airdropTokenId,
         mockEncryptedEj,
     );
     expect(encodedScript.toString('hex')).toBe(
         '6a04657461624d420130343638386639393037666533633763306237386137336334616234663735653135653765326237393634316164643531393631373038363132366665366636623134303561313465656434386539306339633863306663373766306633363938346137383137336537366365353166306134346166393462353965396461373033633966663832373538636664623963633436343337643636323432333430306662373331643362666331646630353939323739333536636132363132313366626234306433393863303431653162616339363661666564316234303435383161623162636663646531666130333964353362376337623730653865646632366436346265613966626565643234636338303930393739366536616635383633373037666130323166326132656261613266653839343930343730326265313964',
     );
 });
 
 it('generateOpReturnScript() correctly generates an un-encrypted non-airdrop message script', () => {
     const BCH = new BCHJS();
     const optionalOpReturnMsg = 'testing generateOpReturnScript()';
     const encryptionFlag = false;
     const airdropFlag = false;
 
     const encodedScript = generateOpReturnScript(
         BCH,
         optionalOpReturnMsg,
         encryptionFlag,
         airdropFlag,
     );
     expect(encodedScript.toString('hex')).toBe(
         '6a04007461622074657374696e672067656e65726174654f7052657475726e5363726970742829',
     );
 });
 
 it('generateOpReturnScript() correctly generates an un-encrypted airdrop message script', () => {
     const BCH = new BCHJS();
     const optionalOpReturnMsg = 'testing generateOpReturnScript()';
     const encryptionFlag = false;
     const airdropFlag = true;
     const airdropTokenId =
         '1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e';
 
     const encodedScript = generateOpReturnScript(
         BCH,
         optionalOpReturnMsg,
         encryptionFlag,
         airdropFlag,
         airdropTokenId,
     );
     expect(encodedScript.toString('hex')).toBe(
         '6a0464726f70201c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e04007461622074657374696e672067656e65726174654f7052657475726e5363726970742829',
     );
 });
 
 it('generateOpReturnScript() correctly generates an un-encrypted airdrop with no message script', () => {
     const BCH = new BCHJS();
     const optionalOpReturnMsg = null;
     const encryptionFlag = false;
     const airdropFlag = true;
     const airdropTokenId =
         '1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e';
 
     const encodedScript = generateOpReturnScript(
         BCH,
         optionalOpReturnMsg,
         encryptionFlag,
         airdropFlag,
         airdropTokenId,
     );
     expect(encodedScript.toString('hex')).toBe(
         '6a0464726f70201c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e0400746162',
     );
 });
 
 it('generateOpReturnScript() correctly throws an error on an invalid encryption input', () => {
     const BCH = new BCHJS();
     const optionalOpReturnMsg = null;
     const encryptionFlag = true;
     const airdropFlag = false;
     const airdropTokenId = null;
     const mockEncryptedEj = null; // invalid given encryptionFlag is true
     let thrownError;
 
     try {
         generateOpReturnScript(
             BCH,
             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 BCH = new BCHJS();
     const optionalOpReturnMsg = null;
     const encryptionFlag = false;
     const airdropFlag = true;
     const airdropTokenId = null; // invalid given airdropFlag is true
 
     let thrownError;
 
     try {
         generateOpReturnScript(
             BCH,
             optionalOpReturnMsg,
             encryptionFlag,
             airdropFlag,
             airdropTokenId,
         );
     } catch (err) {
         thrownError = err;
     }
     expect(thrownError.message).toStrictEqual('Invalid OP RETURN script input');
 });
 
 it(`generateTxInput() returns an input object for a valid one to one XEC tx `, async () => {
     const BCH = new BCHJS();
     const isOneToMany = false;
     const utxos = mockReturnGetSlpBalancesAndUtxos.nonSlpUtxos;
     let txBuilder = new BCH.TransactionBuilder();
     const destinationAddressAndValueArray = null;
     const satoshisToSend = new BigNumber(2184);
     const feeInSatsPerByte = currency.defaultFee;
 
     const inputObj = generateTxInput(
         BCH,
         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 BCH = new BCHJS();
     const isOneToMany = true;
     const utxos = mockReturnGetSlpBalancesAndUtxos.nonSlpUtxos;
     let txBuilder = new BCH.TransactionBuilder();
     const destinationAddressAndValueArray = [
         'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,3000',
         'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,3000',
         'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,3000',
     ];
     const satoshisToSend = new BigNumber(900000);
     const feeInSatsPerByte = currency.defaultFee;
 
     const inputObj = generateTxInput(
         BCH,
         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 BCH = new BCHJS();
     const isOneToMany = true;
     const utxos = mockReturnGetSlpBalancesAndUtxos.nonSlpUtxos;
     let txBuilder = new BCH.TransactionBuilder();
     const destinationAddressAndValueArray = null; // invalid since isOneToMany is true
     const satoshisToSend = new BigNumber(900000);
     const feeInSatsPerByte = currency.defaultFee;
 
     let thrownError;
     try {
         generateTxInput(
             BCH,
             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 BCH = new BCHJS();
     const isOneToMany = true;
     const utxos = null;
     let txBuilder = new BCH.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(
             BCH,
             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 BCH = new BCHJS();
+    const { destinationAddress, wallet } = sendBCHMock;
+    const isOneToMany = false;
+    const singleSendValue = new BigNumber(
+        fromSmallestDenomination(
+            mockOneToOneSendXecTxBuilderObj.transaction.tx.outs[0].value,
+        ),
+    );
+    const totalInputUtxoValue =
+        mockOneToOneSendXecTxBuilderObj.transaction.inputs[0].value;
+    const satoshisToSend = toSmallestDenomination(
+        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 BCH.TransactionBuilder();
+    const changeAddress = wallet.Path1899.cashAddress;
+
+    const outputObj = generateTxOutput(
+        BCH,
+        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 BCH = new BCHJS();
+    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 = toLegacyCashArray(
+        validAddressArrayInput,
+    );
+    let txBuilder = new BCH.TransactionBuilder();
+    const changeAddress = wallet.Path1899.cashAddress;
+
+    const outputObj = generateTxOutput(
+        BCH,
+        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 BCH = new BCHJS();
+    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 = toSmallestDenomination(
+        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 BCH.TransactionBuilder();
+    const changeAddress = wallet.Path1899.cashAddress;
+
+    let thrownError;
+    try {
+        generateTxOutput(
+            BCH,
+            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 BCH = new BCHJS();
+    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 BCH.TransactionBuilder();
+    const changeAddress = wallet.Path1899.cashAddress;
+
+    let thrownError;
+    try {
+        generateTxOutput(
+            BCH,
+            isOneToMany,
+            singleSendValue,
+            satoshisToSend,
+            totalInputUtxoValue,
+            null,
+            destinationAddressAndValueArray,
+            changeAddress,
+            txFee,
+            txBuilder,
+        );
+    } catch (err) {
+        thrownError = err;
+    }
+    expect(thrownError.message).toStrictEqual('Invalid tx input parameter');
+});
+
 describe('Correctly executes cash utility functions', () => {
     it(`Correctly converts smallest base unit to smallest decimal for cashDecimals = 2`, () => {
         expect(fromSmallestDenomination(1, 2)).toBe(0.01);
     });
     it(`Correctly converts largest base unit to smallest decimal for cashDecimals = 2`, () => {
         expect(fromSmallestDenomination(1000000012345678, 2)).toBe(
             10000000123456.78,
         );
     });
     it(`Correctly converts smallest base unit to smallest decimal for cashDecimals = 8`, () => {
         expect(fromSmallestDenomination(1, 8)).toBe(0.00000001);
     });
     it(`Correctly converts largest base unit to smallest decimal for cashDecimals = 8`, () => {
         expect(fromSmallestDenomination(1000000012345678, 8)).toBe(
             10000000.12345678,
         );
     });
     it(`Correctly converts an array of length 10 to an array of 4 arrays, each with max length 3`, () => {
         expect(batchArray(unbatchedArray, 3)).toStrictEqual(
             arrayBatchedByThree,
         );
     });
     it(`If array length is less than batch size, return original array as first and only element of new array`, () => {
         expect(batchArray(unbatchedArray, 20)).toStrictEqual([unbatchedArray]);
     });
     it(`Flattens hydrateUtxos from Promise.all() response into array that can be parsed by getSlpBalancesAndUtxos`, () => {
         expect(
             flattenBatchedHydratedUtxos(unflattenedHydrateUtxosResponse),
         ).toStrictEqual(flattenedHydrateUtxosResponse);
     });
     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(`Recognizes a stored wallet as valid if it has all required fields`, () => {
         expect(isValidStoredWallet(validStoredWallet)).toBe(true);
     });
     it(`Recognizes a stored wallet as invalid if it is missing required fields`, () => {
         expect(isValidStoredWallet(invalidStoredWallet)).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(`toLegacyToken returns an etoken: prefix address as simpleledger:`, () => {
         expect(
             toLegacyToken('etoken:qz2708636snqhsxu8wnlka78h6fdp77ar5tv2tzg4r'),
         ).toBe('simpleledger:qz2708636snqhsxu8wnlka78h6fdp77ar5syue64fa');
     });
     it(`toLegacyToken returns an prefixless valid etoken address in simpleledger: format with prefix`, () => {
         expect(
             toLegacyToken('qz2708636snqhsxu8wnlka78h6fdp77ar5tv2tzg4r'),
         ).toBe('simpleledger:qz2708636snqhsxu8wnlka78h6fdp77ar5syue64fa');
     });
     it(`Correctly parses utxo vout tx data to confirm the transactions are not eToken txs`, () => {
         expect(checkNullUtxosForTokenStatus(mockTxDataResults)).toStrictEqual(
             mockNonEtokenUtxos,
         );
     });
     it(`Correctly parses utxo vout tx data and screens out an eToken by asm field`, () => {
         expect(
             checkNullUtxosForTokenStatus(mockTxDataResultsWithEtoken),
         ).toStrictEqual([]);
     });
     it(`Changes isValid from 'null' to 'false' for confirmed nonEtokenUtxos`, () => {
         expect(
             confirmNonEtokenUtxos(
                 mockHydratedUtxosWithNullValues,
                 mockNonEtokenUtxos,
             ),
         ).toStrictEqual(mockHydratedUtxosWithNullValuesSetToFalse);
     });
     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('toLegacyCash() converts a valid ecash: prefix address to a valid bitcoincash: prefix address', async () => {
         const result = toLegacyCash(
             'ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc',
         );
         expect(result).toStrictEqual(
             'bitcoincash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gjykk3wa0',
         );
     });
 
     test('toLegacyCash() converts a valid ecash: prefixless address to a valid bitcoincash: prefix address', async () => {
         const result = toLegacyCash(
             'qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc',
         );
         expect(result).toStrictEqual(
             'bitcoincash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gjykk3wa0',
         );
     });
 
     test('toLegacyCash throws error if input address has invalid checksum', async () => {
         const result = toLegacyCash(
             'ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25m',
         );
         expect(result).toStrictEqual(
             new Error(
                 'ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25m is not a valid ecash address',
             ),
         );
     });
 
     test('toLegacyCash() throws error with input of etoken: address', async () => {
         const result = toLegacyCash(
             'etoken:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4g9htlunl0',
         );
         expect(result).toStrictEqual(
             new Error(
                 'etoken:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4g9htlunl0 is not a valid ecash address',
             ),
         );
     });
 
     test('toLegacyCash() throws error with input of legacy address', async () => {
         const result = toLegacyCash('13U6nDrkRsC3Eb1pxPhNY8XJ5W9zdp6rNk');
         expect(result).toStrictEqual(
             new Error(
                 '13U6nDrkRsC3Eb1pxPhNY8XJ5W9zdp6rNk is not a valid ecash address',
             ),
         );
     });
 
     test('toLegacyCash() throws error with input of bitcoincash: address', async () => {
         const result = toLegacyCash(
             'bitcoincash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gjykk3wa0',
         );
         expect(result).toStrictEqual(
             new Error(
                 'bitcoincash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gjykk3wa0 is not a valid ecash address',
             ),
         );
     });
 
     test('toLegacyCashArray throws error if the addressArray input is null', async () => {
         const result = toLegacyCashArray(null);
 
         expect(result).toStrictEqual(new Error('Invalid addressArray input'));
     });
 
     test('toLegacyCashArray throws error if the addressArray input is empty', async () => {
         const result = toLegacyCashArray([]);
 
         expect(result).toStrictEqual(new Error('Invalid addressArray input'));
     });
 
     test('toLegacyCashArray throws error if the addressArray input is a number', async () => {
         const result = toLegacyCashArray(12345);
 
         expect(result).toStrictEqual(new Error('Invalid addressArray input'));
     });
 
     test('toLegacyCashArray throws error if the addressArray input is undefined', async () => {
         const result = toLegacyCashArray(undefined);
 
         expect(result).toStrictEqual(new Error('Invalid addressArray input'));
     });
 
     test('toLegacyCashArray successfully converts a standard sized valid addressArray input', async () => {
         const result = toLegacyCashArray(validAddressArrayInput);
 
         expect(result).toStrictEqual(validAddressArrayOutput);
     });
 
     test('toLegacyCashArray successfully converts a standard sized valid addressArray input including prefixless ecash addresses', async () => {
         const result = toLegacyCashArray(validAddressArrayInputMixedPrefixes);
 
         expect(result).toStrictEqual(validAddressArrayOutput);
     });
 
     test('toLegacyCashArray successfully converts a large valid addressArray input', async () => {
         const result = toLegacyCashArray(validLargeAddressArrayInput);
 
         expect(result).toStrictEqual(validLargeAddressArrayOutput);
     });
 
     test('toLegacyCashArray throws an error on an addressArray with invalid addresses', async () => {
         const result = toLegacyCashArray(invalidAddressArrayInput);
 
         expect(result).toStrictEqual(
             new Error(
                 'ecash:qrqgwxrrrrrrrrrrrrrrrrrrrrrrrrr7zsvk is not a valid ecash 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('isExcludedUtxo returns true for a utxo with different tx_pos and same txid as an existing utxo in the set', async () => {
         expect(
             isExcludedUtxo(excludedUtxoAlpha, previousUtxosObjUtxoArray),
         ).toBe(true);
     });
     test('isExcludedUtxo returns true for a utxo with different value and same txid as an existing utxo in the set', async () => {
         expect(
             isExcludedUtxo(excludedUtxoBeta, previousUtxosObjUtxoArray),
         ).toBe(true);
     });
     test('isExcludedUtxo returns false for a utxo with different tx_pos and same txid', async () => {
         expect(
             isExcludedUtxo(includedUtxoAlpha, previousUtxosObjUtxoArray),
         ).toBe(false);
     });
     test('isExcludedUtxo returns false for a utxo with different value and same txid', async () => {
         expect(
             isExcludedUtxo(includedUtxoBeta, previousUtxosObjUtxoArray),
         ).toBe(false);
     });
     test('whichUtxosWereAdded correctly identifies a single added utxo after a received XEC tx [template]', async () => {
         expect(
             whichUtxosWereAdded(
                 previousUtxosTemplate,
                 currentUtxosAfterSingleXecReceiveTxTemplate,
             ),
         ).toStrictEqual(utxosAddedBySingleXecReceiveTxTemplate);
     });
     test('whichUtxosWereAdded correctly identifies a single added utxo after a received XEC tx', async () => {
         expect(
             whichUtxosWereAdded(
                 previousUtxosBeforeSingleXecReceiveTx,
                 currentUtxosAfterSingleXecReceiveTx,
             ),
         ).toStrictEqual(utxosAddedBySingleXecReceiveTx);
     });
     test('whichUtxosWereAdded correctly identifies multiple added utxos with the same txid [template]', async () => {
         expect(
             whichUtxosWereAdded(
                 previousUtxosTemplate,
                 currentUtxosAfterMultiXecReceiveTxTemplate,
             ),
         ).toStrictEqual(utxosAddedByMultiXecReceiveTxTemplate);
     });
     test('whichUtxosWereAdded correctly identifies multiple added utxos with the same txid', async () => {
         expect(
             whichUtxosWereAdded(
                 previousUtxosBeforeMultiXecReceiveTx,
                 currentUtxosAfterMultiXecReceiveTx,
             ),
         ).toStrictEqual(utxosAddedByMultiXecReceiveTx);
     });
     test('whichUtxosWereAdded correctly identifies an added utxos from received eToken tx [template]', async () => {
         expect(
             whichUtxosWereAdded(
                 previousUtxosTemplate,
                 currentUtxosAfterEtokenReceiveTxTemplate,
             ),
         ).toStrictEqual(utxosAddedByEtokenReceiveTxTemplate);
     });
     test('whichUtxosWereAdded correctly identifies an added utxos from received eToken tx', async () => {
         expect(
             whichUtxosWereAdded(
                 previousUtxosBeforeEtokenReceiveTx,
                 currentUtxosAfterEtokenReceiveTx,
             ),
         ).toStrictEqual(utxosAddedByEtokenReceiveTx);
     });
     test('whichUtxosWereAdded correctly identifies no utxos were added in a send all XEC tx (no change) [template]', async () => {
         expect(
             whichUtxosWereAdded(
                 previousUtxosBeforeSendAllTxTemplate,
                 currentUtxosAfterSendAllTxTemplate,
             ),
         ).toStrictEqual(false);
     });
     test('whichUtxosWereAdded correctly identifies no utxos were added in a send all XEC tx (no change)', async () => {
         expect(
             whichUtxosWereAdded(
                 previousUtxosBeforeSendAllTx,
                 currentUtxosAfterSendAllTx,
             ),
         ).toStrictEqual(false);
     });
     test('whichUtxosWereAdded correctly identifies an added utxo from a single send XEC tx', async () => {
         expect(
             whichUtxosWereAdded(
                 previousUtxosBeforeSingleXecSendTx,
                 currentUtxosAfterSingleXecSendTx,
             ),
         ).toStrictEqual(utxosAddedBySingleXecSendTx);
     });
     test('whichUtxosWereAdded correctly identifies an added utxo from a single send XEC tx [template]', async () => {
         expect(
             whichUtxosWereAdded(
                 previousUtxosTemplate,
                 currentUtxosAfterSingleXecSendTxTemplate,
             ),
         ).toStrictEqual(utxosAddedBySingleXecSendTxTemplate);
     });
     test('whichUtxosWereAdded correctly identifies added change utxos from a send eToken tx [template]', async () => {
         expect(
             whichUtxosWereAdded(
                 previousUtxosTemplate,
                 currentUtxosAfterEtokenSendTxTemplate,
             ),
         ).toStrictEqual(utxosAddedByEtokenSendTxTemplate);
     });
     test('whichUtxosWereAdded correctly identifies added change utxos from a send eToken tx', async () => {
         expect(
             whichUtxosWereAdded(
                 previousUtxosBeforeEtokenSendTx,
                 currentUtxosAfterEtokenSendTx,
             ),
         ).toStrictEqual(utxosAddedByEtokenSendTx);
     });
     test('whichUtxosWereConsumed correctly identifies no utxos consumed after a received XEC tx [template]', async () => {
         expect(
             whichUtxosWereConsumed(
                 previousUtxosTemplate,
                 currentUtxosAfterSingleXecReceiveTxTemplate,
             ),
         ).toStrictEqual(false);
     });
     test('whichUtxosWereConsumed correctly identifies no utxos consumed a received XEC tx', async () => {
         expect(
             whichUtxosWereConsumed(
                 previousUtxosBeforeSingleXecReceiveTx,
                 currentUtxosAfterSingleXecReceiveTx,
             ),
         ).toStrictEqual(false);
     });
     test('whichUtxosWereConsumed correctly identifies no consumed utxos after receiving an XEC multi-send tx [template]', async () => {
         expect(
             whichUtxosWereConsumed(
                 previousUtxosTemplate,
                 currentUtxosAfterMultiXecReceiveTxTemplate,
             ),
         ).toStrictEqual(false);
     });
     test('whichUtxosWereConsumed correctly identifies no consumed utxos after receiving an XEC multi-send tx', async () => {
         expect(
             whichUtxosWereConsumed(
                 previousUtxosBeforeMultiXecReceiveTx,
                 currentUtxosAfterMultiXecReceiveTx,
             ),
         ).toStrictEqual(false);
     });
     test('whichUtxosWereConsumed correctly identifies consumed utxos from a single send XEC tx', async () => {
         expect(
             whichUtxosWereConsumed(
                 previousUtxosBeforeSingleXecSendTx,
                 currentUtxosAfterSingleXecSendTx,
             ),
         ).toStrictEqual(utxosConsumedBySingleXecSendTx);
     });
     test('whichUtxosWereConsumed correctly identifies consumed utxos from a send all XEC tx [template]', async () => {
         expect(
             whichUtxosWereConsumed(
                 previousUtxosBeforeSendAllTxTemplate,
                 currentUtxosAfterSendAllTxTemplate,
             ),
         ).toStrictEqual(utxosConsumedBySendAllTxTemplate);
     });
     test('whichUtxosWereConsumed correctly identifies consumed utxos from a send all XEC tx', async () => {
         expect(
             whichUtxosWereConsumed(
                 previousUtxosBeforeSendAllTx,
                 currentUtxosAfterSendAllTx,
             ),
         ).toStrictEqual(utxosConsumedBySendAllTx);
     });
     test('whichUtxosWereConsumed correctly identifies consumed utxos from a single send XEC tx [template]', async () => {
         expect(
             whichUtxosWereConsumed(
                 previousUtxosTemplate,
                 currentUtxosAfterSingleXecSendTxTemplate,
             ),
         ).toStrictEqual(utxosConsumedBySingleXecSendTxTemplate);
     });
     test('whichUtxosWereConsumed correctly identifies consumed utxos from a send eToken tx [template]', async () => {
         expect(
             whichUtxosWereConsumed(
                 previousUtxosTemplate,
                 currentUtxosAfterEtokenSendTxTemplate,
             ),
         ).toStrictEqual(utxosConsumedByEtokenSendTxTemplate);
     });
     test('whichUtxosWereConsumed correctly identifies consumed utxos from a send eToken tx', async () => {
         expect(
             whichUtxosWereConsumed(
                 previousUtxosBeforeEtokenSendTx,
                 currentUtxosAfterEtokenSendTx,
             ),
         ).toStrictEqual(utxosConsumedByEtokenSendTx);
     });
     test('addNewHydratedUtxos correctly adds new utxos object to existing hydratedUtxoDetails object', async () => {
         expect(
             addNewHydratedUtxos(
                 newHydratedUtxosSingleTemplate,
                 hydratedUtxoDetailsBeforeAddingTemplate,
             ),
         ).toStrictEqual(hydratedUtxoDetailsAfterAddingSingleUtxoTemplate);
     });
     test('addNewHydratedUtxos correctly adds more than 20 new hydrated utxos to existing hydratedUtxoDetails object', async () => {
         expect(
             addNewHydratedUtxos(
                 addedHydratedUtxosOverTwenty,
                 existingHydratedUtxoDetails,
             ),
         ).toStrictEqual(existingHydratedUtxoDetailsAfterAdd);
     });
     test('removeConsumedUtxos correctly removes a single utxo from hydratedUtxoDetails - template', async () => {
         expect(
             removeConsumedUtxos(
                 consumedUtxoTemplate,
                 hydratedUtxoDetailsBeforeConsumedTemplate,
             ),
         ).toStrictEqual(hydratedUtxoDetailsAfterRemovingConsumedUtxoTemplate);
     });
     test('removeConsumedUtxos correctly removes a single utxo from hydratedUtxoDetails', async () => {
         expect(
             removeConsumedUtxos(
                 consumedUtxos,
                 hydratedUtxoDetailsBeforeRemovingConsumedUtxos,
             ),
         ).toStrictEqual(hydratedUtxoDetailsAfterRemovingConsumedUtxos);
     });
     test('removeConsumedUtxos correctly removes more than twenty utxos from hydratedUtxoDetails', async () => {
         expect(
             removeConsumedUtxos(
                 consumedUtxosMoreThanTwenty,
                 hydratedUtxoDetailsBeforeRemovingConsumedUtxos,
             ),
         ).toStrictEqual(
             hydratedUtxoDetailsAfterRemovingMoreThanTwentyConsumedUtxos,
         );
     });
     test('removeConsumedUtxos correctly removes more than twenty utxos from multiple utxo objects from hydratedUtxoDetails', async () => {
         expect(
             removeConsumedUtxos(
                 consumedUtxosMoreThanTwentyInRandomObjects,
                 hydratedUtxoDetailsBeforeRemovingConsumedUtxos,
             ),
         ).toStrictEqual(
             hydratedUtxoDetailsAfterRemovingMoreThanTwentyConsumedUtxos,
         );
     });
     test('getUtxoCount correctly calculates the total for a utxo object with empty addresses [template]', async () => {
         expect(getUtxoCount(utxoCountSingleTemplate)).toStrictEqual(1);
     });
     test('getUtxoCount correctly calculates the total for multiple utxos [template]', async () => {
         expect(getUtxoCount(utxoCountMultiTemplate)).toStrictEqual(12);
     });
     test('areAllUtxosIncludedInIncrementallyHydratedUtxos correctly identifies all utxos are in incrementally hydrated utxos [template]', async () => {
         expect(
             areAllUtxosIncludedInIncrementallyHydratedUtxos(
                 incrementalUtxosTemplate,
                 incrementallyHydratedUtxosTemplate,
             ),
         ).toBe(true);
     });
     test('areAllUtxosIncludedInIncrementallyHydratedUtxos returns false if a utxo in the utxo set is not in incrementally hydrated utxos [template]', async () => {
         expect(
             areAllUtxosIncludedInIncrementallyHydratedUtxos(
                 incrementalUtxosTemplate,
                 incrementallyHydratedUtxosTemplateMissing,
             ),
         ).toBe(false);
     });
     test('areAllUtxosIncludedInIncrementallyHydratedUtxos correctly identifies all utxos are in incrementally hydrated utxos', async () => {
         expect(
             areAllUtxosIncludedInIncrementallyHydratedUtxos(
                 utxosAfterSentTxIncremental,
                 incrementallyHydratedUtxosAfterProcessing,
             ),
         ).toBe(true);
     });
     test('areAllUtxosIncludedInIncrementallyHydratedUtxos returns false if a utxo in the utxo set is not in incrementally hydrated utxos', async () => {
         expect(
             areAllUtxosIncludedInIncrementallyHydratedUtxos(
                 utxosAfterSentTxIncremental,
                 incrementallyHydratedUtxosAfterProcessingOneMissing,
             ),
         ).toBe(false);
     });
     test('areAllUtxosIncludedInIncrementallyHydratedUtxos returns false if utxo set is invalid', async () => {
         expect(
             areAllUtxosIncludedInIncrementallyHydratedUtxos(
                 {},
                 incrementallyHydratedUtxosAfterProcessing,
             ),
         ).toBe(false);
     });
     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(`Successfully parses an incoming XEC tx`, () => {
         expect(
             parseChronikTx(lambdaIncomingXecTx, lambdaHash160s),
         ).toStrictEqual({
             incoming: true,
             xecAmount: '42',
             isEtokenTx: false,
         });
     });
     it(`Successfully parses an outgoing XEC tx`, () => {
         expect(
             parseChronikTx(lambdaOutgoingXecTx, lambdaHash160s),
         ).toStrictEqual({
             incoming: false,
             xecAmount: '222',
             isEtokenTx: false,
         });
     });
     it(`Successfully parses an incoming eToken tx`, () => {
         expect(
             parseChronikTx(lambdaIncomingEtokenTx, lambdaHash160s),
         ).toStrictEqual({
             incoming: true,
             xecAmount: '5.46',
             isEtokenTx: true,
             slpMeta: {
                 tokenId:
                     '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3',
                 tokenType: 'FUNGIBLE',
                 txType: 'SEND',
             },
             etokenAmount: '12',
         });
     });
     it(`Successfully parses an outgoing eToken tx`, () => {
         expect(
             parseChronikTx(lambdaOutgoingEtokenTx, lambdaHash160s),
         ).toStrictEqual({
             incoming: false,
             xecAmount: '5.46',
             isEtokenTx: true,
             slpMeta: {
                 tokenId:
                     '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3',
                 tokenType: 'FUNGIBLE',
                 txType: 'SEND',
             },
             etokenAmount: '17',
         });
     });
     it(`Returns decimals, name, and ticker for an eToken stored in wallet object`, () => {
         expect(
             checkWalletForTokenInfo(
                 '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3',
                 validStoredWallet,
             ),
         ).toStrictEqual({
             decimals: 0,
             name: 'Covid19 Lifetime Immunity',
             ticker: 'NOCOVID',
         });
     });
     it(`Returns false for an eToken not stored in a wallet object`, () => {
         expect(
             checkWalletForTokenInfo(
                 '98183238638ecb4ddc365056e22de0e8a05448c1e6084bae247fae5a74ad4f48',
                 validStoredWallet,
             ),
         ).toBe(false);
     });
     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);
     });
 });
diff --git a/web/cashtab/src/utils/cashMethods.js b/web/cashtab/src/utils/cashMethods.js
index 8f1d80226..c1462ebbc 100644
--- a/web/cashtab/src/utils/cashMethods.js
+++ b/web/cashtab/src/utils/cashMethods.js
@@ -1,1343 +1,1412 @@
 import { currency } from 'components/Common/Ticker';
 import {
     isValidXecAddress,
     isValidEtokenAddress,
     isValidBchApiUtxoObject,
     isValidContactList,
 } from 'utils/validation';
 import BigNumber from 'bignumber.js';
 import cashaddr from 'ecashaddrjs';
 import useBCH from '../hooks/useBCH';
 
 export const generateTxInput = (
     BCH,
     isOneToMany,
     utxos,
     txBuilder,
     destinationAddressAndValueArray,
     satoshisToSend,
     feeInSatsPerByte,
 ) => {
     const { calcFee } = useBCH();
     let txInputObj = {};
     const inputUtxos = [];
     let txFee = 0;
     let totalInputUtxoValue = new BigNumber(0);
     try {
         if (
             !BCH ||
             (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.vout;
             const txid = utxo.txid;
             // add input with txid and index of vout
             txBuilder.addInput(txid, vout);
 
             inputUtxos.push(utxo);
             txFee = calcFee(BCH, 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 getChangeAddressFromInputUtxos = (BCH, inputUtxos, wallet) => {
     if (!BCH || !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;
         BCH.Address.isCashAddress(changeAddress);
     } 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(
                     fromSmallestDenomination(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;
 };
 
 /*
  * 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 = (
     BCH,
     optionalOpReturnMsg,
     encryptionFlag,
     airdropFlag,
     airdropTokenId,
     encryptedEj,
 ) => {
     // encrypted mesage is mandatory when encryptionFlag is true
     // airdrop token id is mandatory when airdropFlag is true
     if (
         !BCH ||
         (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;
     }
 
     const data = BCH.Script.encode(script);
     return data;
 };
 
+export const generateTxOutput = (
+    BCH,
+    isOneToMany,
+    singleSendValue,
+    satoshisToSend,
+    totalInputUtxoValue,
+    destinationAddress,
+    destinationAddressAndValueArray,
+    changeAddress,
+    txFee,
+    txBuilder,
+) => {
+    try {
+        if (
+            !BCH ||
+            (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(
+                    BCH.Address.toCashAddress(outputAddress),
+                    parseInt(toSmallestDenomination(outputValue)),
+                );
+            }
+        } else {
+            // for one to one mode, add output w/ single address and amount to send
+            txBuilder.addOutput(
+                BCH.Address.toCashAddress(destinationAddress),
+                parseInt(toSmallestDenomination(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 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 fromSmallestDenomination = (
     amount,
     cashDecimals = currency.cashDecimals,
 ) => {
     const amountBig = new BigNumber(amount);
     const multiplier = new BigNumber(10 ** (-1 * cashDecimals));
     const amountInBaseUnits = amountBig.times(multiplier);
     return amountInBaseUnits.toNumber();
 };
 
 export const toSmallestDenomination = (
     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 batchArray = (inputArray, batchSize) => {
     // take an array of n elements, return an array of arrays each of length batchSize
 
     const batchedArray = [];
     for (let i = 0; i < inputArray.length; i += batchSize) {
         const tempArray = inputArray.slice(i, i + batchSize);
         batchedArray.push(tempArray);
     }
     return batchedArray;
 };
 
 export const flattenBatchedHydratedUtxos = batchedHydratedUtxoDetails => {
     // Return same result as if only the bulk API call were made
     // to do this, just need to move all utxos under one slpUtxos
     /*
     given 
     [
       {
         slpUtxos: [
             {
                 utxos: [],
                 address: '',
             }
           ],
       },
       {
         slpUtxos: [
             {
                 utxos: [],
                 address: '',
             }
           ],
       }
     ]
   return [
     {
         slpUtxos: [
             {
             utxos: [],
             address: ''
             },
             {
             utxos: [],
             address: ''
             },
           ]
         }
   */
     const flattenedBatchedHydratedUtxos = { slpUtxos: [] };
     for (let i = 0; i < batchedHydratedUtxoDetails.length; i += 1) {
         const theseSlpUtxos = batchedHydratedUtxoDetails[i].slpUtxos[0];
         flattenedBatchedHydratedUtxos.slpUtxos.push(theseSlpUtxos);
     }
     return flattenedBatchedHydratedUtxos;
 };
 
 export const flattenContactList = contactList => {
     /*
     Converts contactList from array of objects of type {address: <valid XEC address>, name: <string>} 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 = normalizeBalance(slpBalancesAndUtxos);
     liveWalletState.balances = balancesRebased;
     return liveWalletState;
 };
 
 export const normalizeBalance = slpBalancesAndUtxos => {
     const totalBalanceInSatoshis = slpBalancesAndUtxos.nonSlpUtxos.reduce(
         (previousBalance, utxo) => previousBalance + utxo.value,
         0,
     );
     return {
         totalBalanceInSatoshis,
         totalBalance: fromSmallestDenomination(totalBalanceInSatoshis),
     };
 };
 
 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;
 }
 
 export function toLegacyCash(addr) {
     // Confirm input is a valid ecash address
     const isValidInput = isValidXecAddress(addr);
     if (!isValidInput) {
         return new Error(`${addr} is not a valid ecash address`);
     }
 
     // Check for ecash: prefix
     const isPrefixedXecAddress = addr.slice(0, 6) === 'ecash:';
 
     // If no prefix, assume it is checksummed for an ecash: prefix
     const testedXecAddr = isPrefixedXecAddress ? addr : `ecash:${addr}`;
 
     let legacyCashAddress;
     try {
         const { type, hash } = cashaddr.decode(testedXecAddr);
         legacyCashAddress = cashaddr.encode(currency.legacyPrefix, type, hash);
     } catch (err) {
         return err;
     }
     return legacyCashAddress;
 }
 
 export function toLegacyCashArray(addressArray) {
     let cleanArray = []; // array of bch converted addresses to be returned
 
     if (
         addressArray === null ||
         addressArray === undefined ||
         !addressArray.length ||
         addressArray === ''
     ) {
         return new Error('Invalid addressArray input');
     }
 
     const arrayLength = addressArray.length;
 
     for (let i = 0; i < arrayLength; i++) {
         let addressValueArr = addressArray[i].split(',');
         let address = addressValueArr[0];
         let value = addressValueArr[1];
 
         // NB that toLegacyCash() includes address validation; will throw error for invalid address input
         const legacyAddress = toLegacyCash(address);
         if (legacyAddress instanceof Error) {
             return legacyAddress;
         }
         let convertedArrayData = legacyAddress + ',' + value + '\n';
         cleanArray.push(convertedArrayData);
     }
 
     return cleanArray;
 }
 
 export function toLegacyToken(addr) {
     // Confirm input is a valid ecash address
     const isValidInput = isValidEtokenAddress(addr);
     if (!isValidInput) {
         return new Error(`${addr} is not a valid etoken address`);
     }
 
     // Check for ecash: prefix
     const isPrefixedEtokenAddress = addr.slice(0, 7) === 'etoken:';
 
     // If no prefix, assume it is checksummed for an ecash: prefix
     const testedEtokenAddr = isPrefixedEtokenAddress ? addr : `etoken:${addr}`;
 
     let legacyTokenAddress;
     try {
         const { type, hash } = cashaddr.decode(testedEtokenAddr);
         legacyTokenAddress = cashaddr.encode('simpleledger', type, hash);
     } catch (err) {
         return err;
     }
     return legacyTokenAddress;
 }
 
 export const confirmNonEtokenUtxos = (hydratedUtxos, nonEtokenUtxos) => {
     // scan through hydratedUtxoDetails
     for (let i = 0; i < hydratedUtxos.length; i += 1) {
         // Find utxos with txids matching nonEtokenUtxos
         if (nonEtokenUtxos.includes(hydratedUtxos[i].txid)) {
             // Confirm that such utxos are not eToken utxos
             hydratedUtxos[i].isValid = false;
         }
     }
     return hydratedUtxos;
 };
 
 export const checkNullUtxosForTokenStatus = txDataResults => {
     const nonEtokenUtxos = [];
     for (let j = 0; j < txDataResults.length; j += 1) {
         const thisUtxoTxid = txDataResults[j].txid;
         const thisUtxoVout = txDataResults[j].details.vout;
         // Iterate over outputs
         for (let k = 0; k < thisUtxoVout.length; k += 1) {
             const thisOutput = thisUtxoVout[k];
             if (thisOutput.scriptPubKey.type === 'nulldata') {
                 const asmOutput = thisOutput.scriptPubKey.asm;
                 if (asmOutput.includes('OP_RETURN 5262419')) {
                     // then it's an eToken tx that has not been properly validated
                     // Do not include it in nonEtokenUtxos
                     // App will ignore it until SLPDB is able to validate it
                     /*
                     console.log(
                         `utxo ${thisUtxoTxid} requires further eToken validation, ignoring`,
                     );*/
                 } else {
                     // Otherwise it's just an OP_RETURN tx that SLPDB has some issue with
                     // It should still be in the user's utxo set
                     // Include it in nonEtokenUtxos
                     /*
                     console.log(
                         `utxo ${thisUtxoTxid} is not an eToken tx, adding to nonSlpUtxos`,
                     );
                     */
                     nonEtokenUtxos.push(thisUtxoTxid);
                 }
             }
         }
     }
     return nonEtokenUtxos;
 };
 
 /* 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 getPublicKey = async (BCH, address) => {
     try {
         const publicKey = await BCH.encryption.getPubKey(address);
         return publicKey.publicKey;
     } catch (err) {
         if (err['error'] === 'No transaction history.') {
             throw new Error(
                 'Cannot send an encrypted message to a wallet with no outgoing transactions',
             );
         } else {
             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 isExcludedUtxo = (utxo, utxoArray) => {
     /*
     utxo is a single utxo of model
     {
         height: 724992
         tx_hash: "8d4bdedb7c4443412e0c2f316a330863aef54d9ba73560ca60cca6408527b247"
         tx_pos: 0
         value: 10200
     }
 
     utxoArray is an array of utxos
     */
     let isExcludedUtxo = true;
     const { tx_hash, tx_pos, value } = utxo;
     for (let i = 0; i < utxoArray.length; i += 1) {
         const thisUtxo = utxoArray[i];
         // NOTE
         // You can't match height, as this changes from 0 to blockheight after confirmation
         //const thisUtxoHeight = thisUtxo.height;
         const thisUtxoTxid = thisUtxo.tx_hash;
         const thisUtxoTxPos = thisUtxo.tx_pos;
         const thisUtxoValue = thisUtxo.value;
         // If you find a utxo such that each object key is identical
         if (
             tx_hash === thisUtxoTxid &&
             tx_pos === thisUtxoTxPos &&
             value === thisUtxoValue
         ) {
             // Then this utxo is not excluded from the array
             isExcludedUtxo = false;
         }
     }
 
     return isExcludedUtxo;
 };
 
 export const whichUtxosWereAdded = (previousUtxos, currentUtxos) => {
     let utxosAddedFlag = false;
     const utxosAdded = [];
 
     // Iterate over currentUtxos
     // For each currentUtxo -- does it exist in previousUtxos?
     // If no, it's added
 
     // Note that the inputs are arrays of arrays, model
     /*
     previousUtxos = [{address: 'string', utxos: []}, ...]
     */
 
     // Iterate over the currentUtxos array of {address: 'string', utxos: []} objects
     for (let i = 0; i < currentUtxos.length; i += 1) {
         // Take the first object
         const thisCurrentUtxoObject = currentUtxos[i];
         const thisCurrentUtxoObjectAddress = thisCurrentUtxoObject.address;
         const thisCurrentUtxoObjectUtxos = thisCurrentUtxoObject.utxos;
         // Iterate over the previousUtxos array of {address: 'string', utxos: []} objects
         for (let j = 0; j < previousUtxos.length; j += 1) {
             const thisPreviousUtxoObject = previousUtxos[j];
             const thisPreviousUtxoObjectAddress =
                 thisPreviousUtxoObject.address;
             // When you find the utxos object at the same address
             if (
                 thisCurrentUtxoObjectAddress === thisPreviousUtxoObjectAddress
             ) {
                 // Create a utxosAddedObject with the address
                 const utxosAddedObject = {
                     address: thisCurrentUtxoObjectAddress,
                     utxos: [],
                 };
                 utxosAdded.push(utxosAddedObject);
 
                 // Grab the previousUtxoObject utxos array. thisCurrentUtxoObjectUtxos has changed compared to thisPreviousUtxoObjectUtxos
                 const thisPreviousUtxoObjectUtxos =
                     thisPreviousUtxoObject.utxos;
                 // To see if any utxos exist in thisCurrentUtxoObjectUtxos that do not exist in thisPreviousUtxoObjectUtxos
                 // iterate over thisPreviousUtxoObjectUtxos for each utxo in thisCurrentUtxoObjectUtxos
                 for (let k = 0; k < thisCurrentUtxoObjectUtxos.length; k += 1) {
                     const thisCurrentUtxo = thisCurrentUtxoObjectUtxos[k];
 
                     if (
                         isExcludedUtxo(
                             thisCurrentUtxo,
                             thisPreviousUtxoObjectUtxos,
                         )
                     ) {
                         // If thisCurrentUtxo was not in the corresponding previous utxos
                         // Then it was added
                         utxosAdded[j].utxos.push(thisCurrentUtxo);
                         utxosAddedFlag = true;
                     }
                 }
             }
         }
     }
     // If utxos were added, return them
     if (utxosAddedFlag) {
         return utxosAdded;
     }
     // Else return false
     return utxosAddedFlag;
 };
 
 export const whichUtxosWereConsumed = (previousUtxos, currentUtxos) => {
     let utxosConsumedFlag = false;
     const utxosConsumed = [];
     // Iterate over previousUtxos
     // For each previousUtxo -- does it exist in currentUtxos?
     // If no, it's consumed
 
     // Note that the inputs are arrays of arrays, model
     /*
     previousUtxos = [{address: 'string', utxos: []}, ...]
     */
 
     // Iterate over the previousUtxos array of {address: 'string', utxos: []} objects
     for (let i = 0; i < previousUtxos.length; i += 1) {
         // Take the first object
         const thisPreviousUtxoObject = previousUtxos[i];
         const thisPreviousUtxoObjectAddress = thisPreviousUtxoObject.address;
         const thisPreviousUtxoObjectUtxos = thisPreviousUtxoObject.utxos;
         // Iterate over the currentUtxos array of {address: 'string', utxos: []} objects
         for (let j = 0; j < currentUtxos.length; j += 1) {
             const thisCurrentUtxoObject = currentUtxos[j];
             const thisCurrentUtxoObjectAddress = thisCurrentUtxoObject.address;
             // When you find the utxos object at the same address
             if (
                 thisCurrentUtxoObjectAddress === thisPreviousUtxoObjectAddress
             ) {
                 // Create a utxosConsumedObject with the address
                 const utxosConsumedObject = {
                     address: thisCurrentUtxoObjectAddress,
                     utxos: [],
                 };
                 utxosConsumed.push(utxosConsumedObject);
                 // Grab the currentUtxoObject utxos array. thisCurrentUtxoObjectUtxos has changed compared to thisPreviousUtxoObjectUtxos
                 const thisCurrentUtxoObjectUtxos = thisCurrentUtxoObject.utxos;
                 // To see if any utxos exist in thisPreviousUtxoObjectUtxos that do not exist in thisCurrentUtxoObjectUtxos
                 // iterate over thisCurrentUtxoObjectUtxos for each utxo in thisPreviousUtxoObjectUtxos
                 for (
                     let k = 0;
                     k < thisPreviousUtxoObjectUtxos.length;
                     k += 1
                 ) {
                     const thisPreviousUtxo = thisPreviousUtxoObjectUtxos[k];
                     // If thisPreviousUtxo was not in the corresponding current utxos
 
                     if (
                         isExcludedUtxo(
                             thisPreviousUtxo,
                             thisCurrentUtxoObjectUtxos,
                         )
                     ) {
                         // Then it was consumed
                         utxosConsumed[j].utxos.push(thisPreviousUtxo);
                         utxosConsumedFlag = true;
                     }
                 }
             }
         }
     }
     // If utxos were consumed, return them
     if (utxosConsumedFlag) {
         return utxosConsumed;
     }
     // Else return false
     return utxosConsumedFlag;
 };
 
 export const addNewHydratedUtxos = (
     addedHydratedUtxos,
     hydratedUtxoDetails,
 ) => {
     const theseAdditionalHydratedUtxos = addedHydratedUtxos.slpUtxos;
     for (let i = 0; i < theseAdditionalHydratedUtxos.length; i += 1) {
         const thisHydratedUtxoObj = theseAdditionalHydratedUtxos[i];
         hydratedUtxoDetails.slpUtxos.push(thisHydratedUtxoObj);
     }
     return hydratedUtxoDetails;
     // Add hydrateUtxos(addedUtxos) to hydratedUtxoDetails
     /*
     e.g. add this
     {
     "slpUtxos": 
         [
             {
                 "utxos": [
                     {
                         "height": 725886,
                         "tx_hash": "29985c01444bf80ade764e5d40d7ec2c12317e03301243170139c75f20c51f78",
                         "tx_pos": 0,
                         "value": 3300,
                         "txid": "29985c01444bf80ade764e5d40d7ec2c12317e03301243170139c75f20c51f78",
                         "vout": 0,
                         "isValid": false
                     }
                 ],
                 "address": "bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr"
             }
         ]
     }
 
 to this
 
 {
     "slpUtxos": 
         [
             {
                 "utxos": [
                     {
                         "height": 725886,
                         "tx_hash": "29985c01444bf80ade764e5d40d7ec2c12317e03301243170139c75f20c51f78",
                         "tx_pos": 0,
                         "value": 3300,
                         "txid": "29985c01444bf80ade764e5d40d7ec2c12317e03301243170139c75f20c51f78",
                         "vout": 0,
                         "isValid": false
                     }
                     ... up to 20
                 ],
                 "address": "bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr"
             },
             {
                 "utxos": [
                     {
                         "height": 725886,
                         "tx_hash": "29985c01444bf80ade764e5d40d7ec2c12317e03301243170139c75f20c51f78",
                         "tx_pos": 0,
                         "value": 3300,
                         "txid": "29985c01444bf80ade764e5d40d7ec2c12317e03301243170139c75f20c51f78",
                         "vout": 0,
                         "isValid": false
                     }
                     ... up to 20
                 ],
                 "address": "bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr"
             }
             ,
             ... a bunch of these in batches of 20
         ]
     }
     */
 };
 
 export const removeConsumedUtxos = (consumedUtxos, hydratedUtxoDetails) => {
     let hydratedUtxoDetailsWithConsumedUtxosRemoved = hydratedUtxoDetails;
     const slpUtxosArray = hydratedUtxoDetails.slpUtxos;
     // Iterate over consumedUtxos
     // Every utxo in consumedUtxos must be removed from hydratedUtxoDetails
     for (let i = 0; i < consumedUtxos.length; i += 1) {
         const thisConsumedUtxoObject = consumedUtxos[i]; // {address: 'string', utxos: [{},{},...{}]}
         const thisConsumedUtxoObjectAddr = thisConsumedUtxoObject.address;
         const thisConsumedUtxoObjectUtxoArray = thisConsumedUtxoObject.utxos;
         for (let j = 0; j < thisConsumedUtxoObjectUtxoArray.length; j += 1) {
             const thisConsumedUtxo = thisConsumedUtxoObjectUtxoArray[j];
             // Iterate through slpUtxosArray to find thisConsumedUtxo
             slpUtxosArrayLoop: for (
                 let k = 0;
                 k < slpUtxosArray.length;
                 k += 1
             ) {
                 const thisSlpUtxosArrayUtxoObject = slpUtxosArray[k]; // {address: 'string', utxos: [{},{},...{}]}
                 const thisSlpUtxosArrayUtxoObjectAddr =
                     thisSlpUtxosArrayUtxoObject.address;
                 // If this address matches the address of the consumed utxo, check for a consumedUtxo match
                 // Note, slpUtxos may have many utxo objects with the same address, need to check them all until you find and remove this consumed utxo
                 if (
                     thisConsumedUtxoObjectAddr ===
                     thisSlpUtxosArrayUtxoObjectAddr
                 ) {
                     const thisSlpUtxosArrayUtxoObjectUtxoArray =
                         thisSlpUtxosArrayUtxoObject.utxos;
 
                     // Iterate to find it and remove it
                     for (
                         let m = 0;
                         m < thisSlpUtxosArrayUtxoObjectUtxoArray.length;
                         m += 1
                     ) {
                         const thisHydratedUtxo =
                             thisSlpUtxosArrayUtxoObjectUtxoArray[m];
                         if (
                             thisConsumedUtxo.tx_hash ===
                                 thisHydratedUtxo.tx_hash &&
                             thisConsumedUtxo.tx_pos ===
                                 thisHydratedUtxo.tx_pos &&
                             thisConsumedUtxo.value === thisHydratedUtxo.value
                         ) {
                             // remove it
                             hydratedUtxoDetailsWithConsumedUtxosRemoved.slpUtxos[
                                 k
                             ].utxos.splice(m, 1);
                             // go to the next consumedUtxo
                             break slpUtxosArrayLoop;
                         }
                     }
                 }
             }
         }
     }
     return hydratedUtxoDetailsWithConsumedUtxosRemoved;
 };
 
 export const getUtxoCount = utxos => {
     // return how many utxos
     // return false if input is invalid
     /*
     Both utxos and hydratedUtxoDetails.slpUtxos are build like so
     [
         {
             address: 'string',
             utxos: [{}, {}, {}...{}]
         },
         {
             address: 'string',
             utxos: [{}, {}, {}...{}]
         },
         {
             address: 'string',
             utxos: [{}, {}, {}...{}]
         },
     ]
 
     We want a function that quickly determines how many utxos are here
     */
 
     // First, validate that you are getting a valid bch-api utxo set
     // if you are not, then return false -- which would cause areAllUtxosIncludedInIncrementallyHydratedUtxos to return false and calculate utxo set the legacy way
     const isValidUtxoObject = isValidBchApiUtxoObject(utxos);
     if (!isValidUtxoObject) {
         return false;
     }
 
     let utxoCount = 0;
     for (let i = 0; i < utxos.length; i += 1) {
         const thisUtxoArrLength = utxos[i].utxos.length;
         utxoCount += thisUtxoArrLength;
     }
     return utxoCount;
 };
 
 export const areAllUtxosIncludedInIncrementallyHydratedUtxos = (
     utxos,
     incrementallyHydratedUtxos,
 ) => {
     let incrementallyHydratedUtxosIncludesAllUtxosInLatestUtxoApiFetch = false;
     // check
     const { slpUtxos } = incrementallyHydratedUtxos;
 
     // Iterate over utxos array
     for (let i = 0; i < utxos.length; i += 1) {
         const thisUtxoObject = utxos[i];
         const thisUtxoObjectAddr = thisUtxoObject.address;
         const thisUtxoObjectUtxos = thisUtxoObject.utxos;
         let utxoFound;
         for (let j = 0; j < thisUtxoObjectUtxos.length; j += 1) {
             const thisUtxo = thisUtxoObjectUtxos[j];
             utxoFound = false;
             // Now iterate over slpUtxos to find it
             slpUtxosLoop: for (let k = 0; k < slpUtxos.length; k += 1) {
                 const thisSlpUtxosObject = slpUtxos[k];
                 const thisSlpUtxosObjectAddr = thisSlpUtxosObject.address;
                 if (thisUtxoObjectAddr === thisSlpUtxosObjectAddr) {
                     const thisSlpUtxosObjectUtxos = thisSlpUtxosObject.utxos;
                     for (
                         let m = 0;
                         m < thisSlpUtxosObjectUtxos.length;
                         m += 1
                     ) {
                         const thisSlpUtxo = thisSlpUtxosObjectUtxos[m];
                         if (
                             thisUtxo.tx_hash === thisSlpUtxo.tx_hash &&
                             thisUtxo.tx_pos === thisSlpUtxo.tx_pos &&
                             thisUtxo.value === thisSlpUtxo.value
                         ) {
                             utxoFound = true;
                             // goto next utxo
                             break slpUtxosLoop;
                         }
                     }
                 }
                 if (k === slpUtxos.length - 1 && !utxoFound) {
                     // return false
                     return incrementallyHydratedUtxosIncludesAllUtxosInLatestUtxoApiFetch;
                 }
             }
         }
     }
     // It's possible that hydratedUtxoDetails includes every utxo from the utxos array, but for some reason also includes additional utxos
     const utxosInUtxos = getUtxoCount(utxos);
     const utxosInIncrementallyHydratedUtxos = getUtxoCount(slpUtxos);
     if (
         !utxosInUtxos ||
         !utxosInIncrementallyHydratedUtxos ||
         utxosInUtxos !== utxosInIncrementallyHydratedUtxos
     ) {
         return incrementallyHydratedUtxosIncludesAllUtxosInLatestUtxoApiFetch;
     }
     // If you make it here, good to go
     incrementallyHydratedUtxosIncludesAllUtxosInLatestUtxoApiFetch = true;
     return incrementallyHydratedUtxosIncludesAllUtxosInLatestUtxoApiFetch;
 };
 
 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 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
     return (
         ws !== null &&
         ws &&
         '_ws' in ws &&
         'readyState' in ws._ws &&
         ws._ws.readyState === 1 &&
         '_subs' in ws &&
         ws._subs.length > 0
     );
 };