diff --git a/web/cashtab/src/utils/__mocks__/mockTxBuilderObj.js b/web/cashtab/src/utils/__mocks__/mockTxBuilderObj.js index 2f740fecd..bfae8ee23 100644 --- a/web/cashtab/src/utils/__mocks__/mockTxBuilderObj.js +++ b/web/cashtab/src/utils/__mocks__/mockTxBuilderObj.js @@ -1,881 +1,1200 @@ // 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, }; export const mockCreateTokenTxBuilderObj = { transaction: { prevTxMap: { '582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d:0': 0, 'f80e305c5c09585c67b4f395b153cd206083fdadb8687aa526611bcd381b5239:0': 1, }, 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: [{}, {}], bitcoinCash: true, tx: { version: 2, locktime: 0, ins: [ { hash: { type: 'Buffer', data: [ 88, 45, 250, 66, 226, 119, 138, 46, 107, 125, 50, 251, 27, 244, 206, 252, 11, 233, 209, 10, 54, 83, 142, 149, 3, 70, 93, 249, 156, 212, 166, 13, ], }, index: 0, script: { type: 'Buffer', data: [], }, sequence: 4294967295, witness: [], }, { hash: { type: 'Buffer', data: [ 248, 14, 48, 92, 92, 9, 88, 92, 103, 180, 243, 149, 177, 83, 205, 32, 96, 131, 253, 173, 184, 104, 122, 165, 38, 97, 27, 205, 56, 27, 82, 57, ], }, index: 0, script: { type: 'Buffer', data: [], }, sequence: 4294967295, witness: [], }, ], outs: [], }, }, 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, }; export const mockSendTokenTxBuilderObj = { transaction: { prevTxMap: { '582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d:0': 0, 'f80e305c5c09585c67b4f395b153cd206083fdadb8687aa526611bcd381b5239:0': 1, '0bac59d79522128668f16fef44083918cd6a1ca2cdada6a6cbf01120837456ef:1': 2, '8c42b01804775b2e23676bdfc7ebbb5144b3d8992bcff13c1d1de5a7649d568b:1': 3, }, 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: [{}, {}, {}, {}], bitcoinCash: true, tx: { version: 2, locktime: 0, ins: [ { hash: { type: 'Buffer', data: [ 88, 45, 250, 66, 226, 119, 138, 46, 107, 125, 50, 251, 27, 244, 206, 252, 11, 233, 209, 10, 54, 83, 142, 149, 3, 70, 93, 249, 156, 212, 166, 13, ], }, index: 0, script: { type: 'Buffer', data: [], }, sequence: 4294967295, witness: [], }, { hash: { type: 'Buffer', data: [ 248, 14, 48, 92, 92, 9, 88, 92, 103, 180, 243, 149, 177, 83, 205, 32, 96, 131, 253, 173, 184, 104, 122, 165, 38, 97, 27, 205, 56, 27, 82, 57, ], }, index: 0, script: { type: 'Buffer', data: [], }, sequence: 4294967295, witness: [], }, { hash: { type: 'Buffer', data: [ 11, 172, 89, 215, 149, 34, 18, 134, 104, 241, 111, 239, 68, 8, 57, 24, 205, 106, 28, 162, 205, 173, 166, 166, 203, 240, 17, 32, 131, 116, 86, 239, ], }, index: 1, script: { type: 'Buffer', data: [], }, sequence: 4294967295, witness: [], }, { hash: { type: 'Buffer', data: [ 140, 66, 176, 24, 4, 119, 91, 46, 35, 103, 107, 223, 199, 235, 187, 81, 68, 179, 216, 153, 43, 207, 241, 60, 29, 29, 229, 167, 100, 157, 86, 139, ], }, index: 1, script: { type: 'Buffer', data: [], }, sequence: 4294967295, witness: [], }, ], outs: [], }, }, 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, }; export const mockBurnTokenTxBuilderObj = { transaction: { prevTxMap: { '582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d:0': 0, 'f80e305c5c09585c67b4f395b153cd206083fdadb8687aa526611bcd381b5239:0': 1, '0bac59d79522128668f16fef44083918cd6a1ca2cdada6a6cbf01120837456ef:1': 2, '8c42b01804775b2e23676bdfc7ebbb5144b3d8992bcff13c1d1de5a7649d568b:1': 3, }, 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: [{}, {}, {}, {}], bitcoinCash: true, tx: { version: 2, locktime: 0, ins: [ { hash: { type: 'Buffer', data: [ 88, 45, 250, 66, 226, 119, 138, 46, 107, 125, 50, 251, 27, 244, 206, 252, 11, 233, 209, 10, 54, 83, 142, 149, 3, 70, 93, 249, 156, 212, 166, 13, ], }, index: 0, script: { type: 'Buffer', data: [], }, sequence: 4294967295, witness: [], }, { hash: { type: 'Buffer', data: [ 248, 14, 48, 92, 92, 9, 88, 92, 103, 180, 243, 149, 177, 83, 205, 32, 96, 131, 253, 173, 184, 104, 122, 165, 38, 97, 27, 205, 56, 27, 82, 57, ], }, index: 0, script: { type: 'Buffer', data: [], }, sequence: 4294967295, witness: [], }, { hash: { type: 'Buffer', data: [ 11, 172, 89, 215, 149, 34, 18, 134, 104, 241, 111, 239, 68, 8, 57, 24, 205, 106, 28, 162, 205, 173, 166, 166, 203, 240, 17, 32, 131, 116, 86, 239, ], }, index: 1, script: { type: 'Buffer', data: [], }, sequence: 4294967295, witness: [], }, { hash: { type: 'Buffer', data: [ 140, 66, 176, 24, 4, 119, 91, 46, 35, 103, 107, 223, 199, 235, 187, 81, 68, 179, 216, 153, 43, 207, 241, 60, 29, 29, 229, 167, 100, 157, 86, 139, ], }, index: 1, script: { type: 'Buffer', data: [], }, sequence: 4294967295, witness: [], }, ], outs: [], }, }, 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, }; + +export const mockCreateTokenOutputsTxBuilderObj = { + transaction: { + prevTxMap: {}, + 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: [], + bitcoinCash: true, + tx: { + version: 2, + locktime: 0, + ins: [], + outs: [ + { + script: { + type: 'Buffer', + data: [ + 106, 4, 83, 76, 80, 0, 1, 1, 7, 71, 69, 78, 69, 83, + 73, 83, 4, 67, 85, 84, 84, 23, 67, 97, 115, 104, + 116, 97, 98, 32, 85, 110, 105, 116, 32, 84, 101, + 115, 116, 32, 84, 111, 107, 101, 110, 23, 104, 116, + 116, 112, 115, 58, 47, 47, 99, 97, 115, 104, 116, + 97, 98, 97, 112, 112, 46, 99, 111, 109, 47, 76, 0, + 1, 2, 76, 0, 8, 0, 0, 0, 0, 0, 0, 39, 16, + ], + }, + value: 0, + }, + { + script: { + type: 'Buffer', + data: [ + 118, 169, 20, 184, 35, 97, 197, 133, 31, 78, 196, + 139, 153, 81, 117, 162, 225, 195, 100, 99, 56, 224, + 118, 136, 172, + ], + }, + value: 546, + }, + ], + }, + }, + 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, +}; + +export const mockSendTokenOutputsTxBuilderObj = { + transaction: { + prevTxMap: {}, + 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: [], + bitcoinCash: true, + tx: { + version: 2, + locktime: 0, + ins: [], + outs: [ + { + script: { + type: 'Buffer', + data: [ + 106, 4, 83, 76, 80, 0, 1, 1, 4, 83, 69, 78, 68, 32, + 249, 234, 191, 148, 237, 236, 24, 233, 31, 81, 140, + 107, 30, 34, 204, 71, 167, 70, 77, 0, 95, 4, 160, + 110, 101, 247, 11, 231, 117, 92, 148, 188, 8, 0, 0, + 0, 0, 0, 0, 19, 136, 8, 0, 0, 0, 23, 72, 114, 21, + 60, + ], + }, + value: 0, + }, + { + script: { + type: 'Buffer', + data: [ + 118, 169, 20, 120, 201, 127, 223, 142, 6, 184, 244, + 56, 210, 145, 181, 166, 165, 10, 97, 254, 115, 208, + 42, 136, 172, + ], + }, + value: 546, + }, + { + 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: 546, + }, + ], + }, + }, + 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, +}; + +export const mockBurnTokenOutputsTxBuilderObj = { + transaction: { + prevTxMap: {}, + 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: [], + bitcoinCash: true, + tx: { + version: 2, + locktime: 0, + ins: [], + outs: [ + { + script: { + type: 'Buffer', + data: [ + 106, 4, 83, 76, 80, 0, 1, 1, 4, 83, 69, 78, 68, 32, + 249, 234, 191, 148, 237, 236, 24, 233, 31, 81, 140, + 107, 30, 34, 204, 71, 167, 70, 77, 0, 95, 4, 160, + 110, 101, 247, 11, 231, 117, 92, 148, 188, 8, 0, 0, + 0, 23, 72, 114, 21, 60, + ], + }, + value: 0, + }, + { + script: { + type: 'Buffer', + data: [ + 118, 169, 20, 120, 201, 127, 223, 142, 6, 184, 244, + 56, 210, 145, 181, 166, 165, 10, 97, 254, 115, 208, + 42, 136, 172, + ], + }, + value: 546, + }, + { + 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: 546, + }, + ], + }, + }, + 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 b4cca9951..985e4be2a 100644 --- a/web/cashtab/src/utils/__tests__/cashMethods.test.js +++ b/web/cashtab/src/utils/__tests__/cashMethods.test.js @@ -1,1467 +1,1540 @@ import BigNumber from 'bignumber.js'; import { fromSatoshisToXec, flattenContactList, loadStoredWallet, isValidStoredWallet, fromLegacyDecimals, convertToEcashPrefix, isLegacyMigrationRequired, toLegacyCash, toLegacyToken, toLegacyCashArray, convertEtokenToEcashAddr, parseOpReturn, convertEcashtoEtokenAddr, getHashArrayFromWallet, isActiveWebsocket, parseXecSendValue, getChangeAddressFromInputUtxos, generateOpReturnScript, generateTxInput, generateTxOutput, generateTokenTxInput, signAndBuildTx, fromXecToSatoshis, getWalletBalanceFromUtxos, signUtxosByAddress, getUtxoWif, + generateTokenTxOutput, } from 'utils/cashMethods'; import { currency } from 'components/Common/Ticker'; import { validAddressArrayInput, validAddressArrayInputMixedPrefixes, validAddressArrayOutput, validLargeAddressArrayInput, validLargeAddressArrayOutput, invalidAddressArrayInput, } from '../__mocks__/mockAddressArray'; import { cachedUtxos, utxosLoadedFromCache, } from '../__mocks__/mockCachedUtxos'; import { validStoredWallet, invalidStoredWallet, invalidpreChronikStoredWallet, } from '../__mocks__/mockStoredWallets'; import { missingPath1899Wallet, missingPublicKeyInPath1899Wallet, missingPublicKeyInPath145Wallet, missingPublicKeyInPath245Wallet, notLegacyWallet, missingHash160, } from '../__mocks__/mockLegacyWalletsUtils'; import { shortCashtabMessageInputHex, longCashtabMessageInputHex, shortExternalMessageInputHex, longExternalMessageInputHex, shortSegmentedExternalMessageInputHex, longSegmentedExternalMessageInputHex, mixedSegmentedExternalMessageInputHex, mockParsedShortCashtabMessageArray, mockParsedLongCashtabMessageArray, mockParsedShortExternalMessageArray, mockParsedLongExternalMessageArray, mockParsedShortSegmentedExternalMessageArray, mockParsedLongSegmentedExternalMessageArray, mockParsedMixedSegmentedExternalMessageArray, eTokenInputHex, mockParsedETokenOutputArray, mockAirdropHexOutput, mockParsedAirdropMessageArray, } from '../__mocks__/mockOpReturnParsedArray'; import mockLegacyWallets from 'hooks/__mocks__/mockLegacyWallets'; import BCHJS from '@psf/bch-js'; import sendBCHMock from '../../hooks/__mocks__/sendBCH'; import { activeWebsocketAlpha, disconnectedWebsocketAlpha, unsubscribedWebsocket, } from '../__mocks__/chronikWs'; import mockNonSlpUtxos from '../../hooks/__mocks__/mockNonSlpUtxos'; import mockSlpUtxos from '../../hooks/__mocks__/mockSlpUtxos'; import { mockOneToOneSendXecTxBuilderObj, mockOneToManySendXecTxBuilderObj, + mockCreateTokenOutputsTxBuilderObj, + mockSendTokenOutputsTxBuilderObj, + mockBurnTokenOutputsTxBuilderObj, mockCreateTokenTxBuilderObj, mockSendTokenTxBuilderObj, mockBurnTokenTxBuilderObj, } from '../__mocks__/mockTxBuilderObj'; import { mockSingleInputUtxo, mockMultipleInputUtxos, mockSingleOutput, mockMultipleOutputs, } from '../__mocks__/mockTxBuilderData'; +import createTokenMock from '../../hooks/__mocks__/createToken'; it(`signUtxosByAddress() successfully returns a txBuilder object for a one to one XEC tx`, () => { const BCH = new BCHJS(); const isOneToMany = false; const { destinationAddress, wallet, utxos } = sendBCHMock; let txBuilder = new BCH.TransactionBuilder(); const satoshisToSendInput = new BigNumber(2184); const feeInSatsPerByte = currency.defaultFee; // mock tx input const inputObj = generateTxInput( BCH, isOneToMany, utxos, txBuilder, null, satoshisToSendInput, feeInSatsPerByte, ); // mock tx output const totalInputUtxoValue = mockOneToOneSendXecTxBuilderObj.transaction.inputs[0].value; const singleSendValue = new BigNumber( fromSatoshisToXec( mockOneToOneSendXecTxBuilderObj.transaction.tx.outs[0].value, ), ); const satoshisToSendOutput = fromXecToSatoshis( new BigNumber(singleSendValue), ); const txFee = new BigNumber(totalInputUtxoValue).minus( new BigNumber(satoshisToSendOutput), ); const changeAddress = wallet.Path1899.cashAddress; const outputObj = generateTxOutput( BCH, isOneToMany, singleSendValue, satoshisToSendOutput, totalInputUtxoValue, destinationAddress, null, changeAddress, txFee, inputObj.txBuilder, ); const txBuilderResponse = signUtxosByAddress( BCH, mockSingleInputUtxo, wallet, outputObj, ); expect(txBuilderResponse.toString()).toStrictEqual( mockOneToOneSendXecTxBuilderObj.toString(), ); }); it(`signUtxosByAddress() successfully returns a txBuilder object for a one to many XEC tx`, () => { const BCH = new BCHJS(); const isOneToMany = true; const { wallet, utxos } = sendBCHMock; let txBuilder = new BCH.TransactionBuilder(); let destinationAddressAndValueArray = [ 'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,3000', 'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,3000', 'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,3000', ]; const satoshisToSendInput = new BigNumber(900000); const feeInSatsPerByte = currency.defaultFee; // mock tx input const inputObj = generateTxInput( BCH, isOneToMany, utxos, txBuilder, destinationAddressAndValueArray, satoshisToSendInput, feeInSatsPerByte, ); // mock tx output const totalInputUtxoValue = mockOneToManySendXecTxBuilderObj.transaction.inputs[0].value + mockOneToManySendXecTxBuilderObj.transaction.inputs[1].value + mockOneToManySendXecTxBuilderObj.transaction.inputs[2].value; const singleSendValue = null; const satoshisToSendOutput = new BigNumber( mockOneToManySendXecTxBuilderObj.transaction.tx.outs[0].value + mockOneToManySendXecTxBuilderObj.transaction.tx.outs[1].value + mockOneToManySendXecTxBuilderObj.transaction.tx.outs[2].value, ); const txFee = new BigNumber(totalInputUtxoValue) .minus(satoshisToSendOutput) .minus( new BigNumber( mockOneToManySendXecTxBuilderObj.transaction.tx.outs[3].value, ), ); // change value destinationAddressAndValueArray = toLegacyCashArray(validAddressArrayInput); const changeAddress = wallet.Path1899.cashAddress; const outputObj = generateTxOutput( BCH, isOneToMany, singleSendValue, satoshisToSendOutput, totalInputUtxoValue, null, destinationAddressAndValueArray, changeAddress, txFee, inputObj.txBuilder, ); const txBuilderResponse = signUtxosByAddress( BCH, mockSingleInputUtxo, wallet, outputObj, ); expect(txBuilderResponse.toString()).toStrictEqual( mockOneToManySendXecTxBuilderObj.toString(), ); }); 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(`generateTokenTxInput() returns a valid object for a valid create token tx`, async () => { const BCH = new BCHJS(); let txBuilder = new BCH.TransactionBuilder(); const tokenId = '1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e'; const tokenInputObj = generateTokenTxInput( BCH, 'GENESIS', mockNonSlpUtxos, null, // no slpUtxos used for genesis tx tokenId, null, // no token send/burn amount for genesis tx currency.defaultFee, txBuilder, ); expect(tokenInputObj.inputXecUtxos).toStrictEqual( [mockNonSlpUtxos[0]].concat([mockNonSlpUtxos[1]]), ); expect(tokenInputObj.txBuilder.toString()).toStrictEqual( mockCreateTokenTxBuilderObj.toString(), ); expect(tokenInputObj.remainderXecValue).toStrictEqual( new BigNumber(699702), // remainder = tokenInputObj.inputXecUtxos - currency.etokenSats - txFee ); }); it(`generateTokenTxInput() returns a valid object for a valid send token tx`, async () => { const BCH = new BCHJS(); let txBuilder = new BCH.TransactionBuilder(); const tokenId = mockSlpUtxos[0].tokenId; const tokenInputObj = generateTokenTxInput( BCH, 'SEND', mockNonSlpUtxos, mockSlpUtxos, tokenId, new BigNumber(500), // sending 500 of these tokens currency.defaultFee, txBuilder, ); expect(tokenInputObj.inputTokenUtxos).toStrictEqual( [mockSlpUtxos[0]].concat([mockSlpUtxos[1]]), // mockSlpUtxos[0] 400 + mockSlpUtxos[1] 6500 ); expect(tokenInputObj.remainderTokenValue).toStrictEqual( new BigNumber(6400), // token change is mockSlpUtxos[0] 400 + mockSlpUtxos[1] 6500 - [tokenAmount] 500 ); expect(tokenInputObj.txBuilder.toString()).toStrictEqual( mockSendTokenTxBuilderObj.toString(), ); }); it(`generateTokenTxInput() returns a valid object for a valid burn token tx`, async () => { const BCH = new BCHJS(); let txBuilder = new BCH.TransactionBuilder(); const tokenId = mockSlpUtxos[0].tokenId; const tokenInputObj = generateTokenTxInput( BCH, 'BURN', mockNonSlpUtxos, mockSlpUtxos, tokenId, new BigNumber(500), // burning 500 of these tokens currency.defaultFee, txBuilder, ); expect(tokenInputObj.inputTokenUtxos).toStrictEqual( [mockSlpUtxos[0]].concat([mockSlpUtxos[1]]), // mockSlpUtxos[0] 400 + mockSlpUtxos[1] 6500 ); expect(tokenInputObj.remainderTokenValue).toStrictEqual( new BigNumber(6400), // token change is mockSlpUtxos[0] 400 + mockSlpUtxos[1] 6500 - [tokenAmount] 500 ); expect(tokenInputObj.txBuilder.toString()).toStrictEqual( mockBurnTokenTxBuilderObj.toString(), ); }); +it(`generateTokenTxOutput() returns a valid object for a valid create token tx`, async () => { + const BCH = new BCHJS(); + let txBuilder = new BCH.TransactionBuilder(); + const { configObj, wallet } = createTokenMock; + const tokenSenderCashAddress = wallet.Path1899.cashAddress; + + const tokenOutputObj = generateTokenTxOutput( + BCH, + txBuilder, + 'GENESIS', + tokenSenderCashAddress, + null, // optional, for SEND or BURN amount + new BigNumber(500), // remainder XEC value + configObj, + ); + + expect(tokenOutputObj.toString()).toStrictEqual( + mockCreateTokenOutputsTxBuilderObj.toString(), + ); +}); + +it(`generateTokenTxOutput() returns a valid object for a valid send token tx`, async () => { + const BCH = new BCHJS(); + let txBuilder = new BCH.TransactionBuilder(); + const { wallet } = createTokenMock; + const tokenSenderCashAddress = wallet.Path1899.cashAddress; + const tokenRecipientTokenAddress = wallet.Path1899.slpAddress; + + const tokenOutputObj = generateTokenTxOutput( + BCH, + txBuilder, + 'SEND', + tokenSenderCashAddress, + mockSlpUtxos, + new BigNumber(500), // remainder XEC value + null, // only for genesis tx + tokenRecipientTokenAddress, // recipient token address + new BigNumber(50), + ); + + expect(tokenOutputObj.toString()).toStrictEqual( + mockSendTokenOutputsTxBuilderObj.toString(), + ); +}); + +it(`generateTokenTxOutput() returns a valid object for a valid burn token tx`, async () => { + const BCH = new BCHJS(); + let txBuilder = new BCH.TransactionBuilder(); + const { wallet } = createTokenMock; + const tokenSenderCashAddress = wallet.Path1899.cashAddress; + + const tokenOutputObj = generateTokenTxOutput( + BCH, + txBuilder, + 'BURN', + tokenSenderCashAddress, + mockSlpUtxos, + new BigNumber(500), // remainder XEC value + null, // only for genesis tx + null, // no token recipients for burn tx + new BigNumber(50), + ); + + expect(tokenOutputObj.toString()).toStrictEqual( + mockBurnTokenOutputsTxBuilderObj.toString(), + ); +}); + it(`generateTxInput() returns an input object for a valid one to one XEC tx`, async () => { const BCH = new BCHJS(); const isOneToMany = false; const utxos = mockNonSlpUtxos; 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 = mockNonSlpUtxos; 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 = mockNonSlpUtxos; 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 = fromSatoshisToXec( mockOneToOneSendXecTxBuilderObj.transaction.tx.outs[0].value, ); const totalInputUtxoValue = mockOneToOneSendXecTxBuilderObj.transaction.inputs[0].value; const satoshisToSend = fromXecToSatoshis(new BigNumber(singleSendValue)); // for unit test purposes, calculate fee by subtracting satoshisToSend from totalInputUtxoValue // no change output to be subtracted in this tx const txFee = new BigNumber(totalInputUtxoValue).minus( new BigNumber(satoshisToSend), ); const destinationAddressAndValueArray = null; let txBuilder = new 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 = fromXecToSatoshis(new BigNumber(singleSendValue)); // for unit test purposes, calculate fee by subtracting satoshisToSend from totalInputUtxoValue // no change output to be subtracted in this tx const txFee = new BigNumber(totalInputUtxoValue).minus(satoshisToSend); const destinationAddressAndValueArray = null; let txBuilder = new 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'); }); it(`signAndBuildTx() successfully returns a raw tx hex for a tx with a single input and a single output`, () => { // txbuilder output params const BCH = new BCHJS(); let txBuilder = new BCH.TransactionBuilder(); const { wallet } = sendBCHMock; // add inputs to txBuilder txBuilder.addInput( mockSingleInputUtxo[0].txid, mockSingleInputUtxo[0].vout, ); // add outputs to txBuilder const outputAddressAndValue = mockSingleOutput.split(','); txBuilder.addOutput( outputAddressAndValue[0], // address parseInt(fromXecToSatoshis(new BigNumber(outputAddressAndValue[1]))), // value ); const rawTxHex = signAndBuildTx( BCH, mockSingleInputUtxo, txBuilder, wallet, ); expect(rawTxHex).toStrictEqual( '0200000001582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006b483045022100b4ee5268cb64c4f097e739df7c6934d1df7e75a4f217d5824db18ae2e12554b102204faf039738181aae80c064b928b3d8079a82cdb080ce9a2d5453939a588f4372412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff0158020000000000001976a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac00000000', ); }); it(`signAndBuildTx() successfully returns a raw tx hex for a tx with a single input and multiple outputs`, () => { // txbuilder output params const BCH = new BCHJS(); let txBuilder = new BCH.TransactionBuilder(); const { wallet } = sendBCHMock; // add inputs to txBuilder txBuilder.addInput( mockSingleInputUtxo[0].txid, mockSingleInputUtxo[0].vout, ); // add outputs to txBuilder for (let i = 0; i < mockMultipleOutputs.length; i++) { const outputAddressAndValue = mockMultipleOutputs[i].split(','); txBuilder.addOutput( outputAddressAndValue[0], // address parseInt( fromXecToSatoshis(new BigNumber(outputAddressAndValue[1])), ), // value ); } const rawTxHex = signAndBuildTx( BCH, mockSingleInputUtxo, txBuilder, wallet, ); expect(rawTxHex).toStrictEqual( '0200000001582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006b483045022100df29734c4fb348b0e8b613ce522c10c5ac14cb3ecd32843dc7fcf004d60f1b8a022023c4ae02b38c7272e29f344902ae2afa4db1ec37d582a31c16650a0abc4f480c412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff0326020000000000001976a914f627e51001a51a1a92d8927808701373cf29267f88ac26020000000000001976a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac26020000000000001976a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac00000000', ); }); it(`signAndBuildTx() successfully returns a raw tx hex for a tx with multiple inputs and a single output`, () => { // txbuilder output params const BCH = new BCHJS(); let txBuilder = new BCH.TransactionBuilder(); const { wallet } = sendBCHMock; // add inputs to txBuilder for (let i = 0; i < mockMultipleInputUtxos.length; i++) { txBuilder.addInput( mockMultipleInputUtxos[i].txid, mockMultipleInputUtxos[i].vout, ); } // add outputs to txBuilder const outputAddressAndValue = mockSingleOutput.split(','); txBuilder.addOutput( outputAddressAndValue[0], // address parseInt(fromXecToSatoshis(new BigNumber(outputAddressAndValue[1]))), // value ); const rawTxHex = signAndBuildTx( BCH, mockMultipleInputUtxos, txBuilder, wallet, ); expect(rawTxHex).toStrictEqual( '0200000003582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006a4730440220541366dd5ea25d65d3044dbde16fc6118ab1aee07c7d0d4c25c9e8aa299f040402203ed2f540948197d4c6a4ae963ad187d145a9fb339e311317b03c6172732e267b412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff7313e804af08113dfa290515390a8ec3ac01448118f2eb556ee168a96ee6acdd000000006b483045022100c1d02c5023f83b87a4f2dd26a7306ed9be9d53ab972bd935b440e45eb54a304302200b99aa2f1a728b3bb1dcbff80742c5fcab991bb74e80fa231255a31d58a6ff7d412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff960dd2f0c47e8a3cf1486b046d879f45a047da3b51aedfb5594138ac857214f1000000006b483045022100bd24d11d7070988848cb4aa2b10748aa0aeb79dc8af39c1f22dc1034b3121e5f02201491026e5f8f6eb566eb17cb195e3da3ff0d9cf01bdd34c944964d33a8d3b1ad412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff0158020000000000001976a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac00000000', ); }); it(`signAndBuildTx() successfully returns a raw tx hex for a tx with multiple inputs and multiple outputs`, () => { // txbuilder output params const BCH = new BCHJS(); let txBuilder = new BCH.TransactionBuilder(); const { wallet } = sendBCHMock; // add inputs to txBuilder for (let i = 0; i < mockMultipleInputUtxos.length; i++) { txBuilder.addInput( mockMultipleInputUtxos[i].txid, mockMultipleInputUtxos[i].vout, ); } // add outputs to txBuilder for (let i = 0; i < mockMultipleOutputs.length; i++) { const outputAddressAndValue = mockMultipleOutputs[i].split(','); txBuilder.addOutput( outputAddressAndValue[0], // address parseInt( fromXecToSatoshis(new BigNumber(outputAddressAndValue[1])), ), // value ); } const rawTxHex = signAndBuildTx( BCH, mockMultipleInputUtxos, txBuilder, wallet, ); expect(rawTxHex).toStrictEqual( '0200000003582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006a47304402203de4e6a512a6bec1d378b6444008484e1be5a0c621dc4b201d67addefffe864602202daf82e76b7594fe1ab54a49380c6b1226ab65551ae6ab9164216b66266f34a1412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff7313e804af08113dfa290515390a8ec3ac01448118f2eb556ee168a96ee6acdd000000006a473044022029f5fcbc9356beb9eae6b9ff9a479e8c8331b95406b6be456fccf9d90f148ea1022028f4e7fa7234f9429535360c8f5dad303e2c5044431615997861b10f26fa8a88412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff960dd2f0c47e8a3cf1486b046d879f45a047da3b51aedfb5594138ac857214f1000000006a473044022049a67738d99006b3523cff818f3626104cf5106bd463be70d22ad179a8cb403b022025829baf67f964202ea77ea7462a5447e32415e7293cdee382ea7ae9374364e8412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff0326020000000000001976a914f627e51001a51a1a92d8927808701373cf29267f88ac26020000000000001976a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac26020000000000001976a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac00000000', ); }); it(`signAndBuildTx() throws error on an empty inputUtxo param`, () => { // txbuilder output params const BCH = new BCHJS(); let txBuilder = new BCH.TransactionBuilder(); const { wallet } = sendBCHMock; let thrownError; try { signAndBuildTx(BCH, [], txBuilder, wallet); } catch (err) { thrownError = err; } expect(thrownError.message).toStrictEqual('Invalid buildTx parameter'); }); it(`signAndBuildTx() throws error on a null inputUtxo param`, () => { // txbuilder output params const BCH = new BCHJS(); let txBuilder = new BCH.TransactionBuilder(); const inputUtxo = null; // invalid input param const { wallet } = sendBCHMock; let thrownError; try { signAndBuildTx(BCH, inputUtxo, txBuilder, wallet); } catch (err) { thrownError = err; } expect(thrownError.message).toStrictEqual('Invalid buildTx parameter'); }); describe('Correctly executes cash utility functions', () => { it(`Correctly converts smallest base unit to smallest decimal for cashDecimals = 2`, () => { expect(fromSatoshisToXec(1, 2)).toStrictEqual(new BigNumber(0.01)); }); it(`Correctly converts largest base unit to smallest decimal for cashDecimals = 2`, () => { expect(fromSatoshisToXec(1000000012345678, 2)).toStrictEqual( new BigNumber(10000000123456.78), ); }); it(`Correctly converts smallest base unit to smallest decimal for cashDecimals = 8`, () => { expect(fromSatoshisToXec(1, 8)).toStrictEqual( new BigNumber(0.00000001), ); }); it(`Correctly converts largest base unit to smallest decimal for cashDecimals = 8`, () => { expect(fromSatoshisToXec(1000000012345678, 8)).toStrictEqual( new BigNumber(10000000.12345678), ); }); it(`Accepts a cachedWalletState that has not preserved BigNumber object types, and returns the same wallet state with BigNumber type re-instituted`, () => { expect(loadStoredWallet(cachedUtxos)).toStrictEqual( utxosLoadedFromCache, ); }); it(`Correctly determines a wallet's balance from its set of non-eToken utxos (nonSlpUtxos)`, () => { expect( getWalletBalanceFromUtxos( validStoredWallet.state.slpBalancesAndUtxos.nonSlpUtxos, ), ).toStrictEqual(validStoredWallet.state.balances); }); it(`Correctly determines a wallet's zero balance from its empty set of non-eToken utxos (nonSlpUtxos)`, () => { expect( getWalletBalanceFromUtxos( utxosLoadedFromCache.slpBalancesAndUtxos.nonSlpUtxos, ), ).toStrictEqual(utxosLoadedFromCache.balances); }); it(`Recognizes a stored wallet as valid if it has all required fields`, () => { expect(isValidStoredWallet(validStoredWallet)).toBe(true); }); it(`Recognizes a stored wallet as invalid if it is missing required fields`, () => { expect(isValidStoredWallet(invalidStoredWallet)).toBe(false); }); it(`Recognizes a stored wallet as invalid if it includes hydratedUtxoDetails in the state field`, () => { expect(isValidStoredWallet(invalidpreChronikStoredWallet)).toBe(false); }); it(`Converts a legacy BCH amount to an XEC amount`, () => { expect(fromLegacyDecimals(0.00000546, 2)).toStrictEqual(5.46); }); it(`Leaves a legacy BCH amount unchanged if currency.cashDecimals is 8`, () => { expect(fromLegacyDecimals(0.00000546, 8)).toStrictEqual(0.00000546); }); it(`convertToEcashPrefix converts a bitcoincash: prefixed address to an ecash: prefixed address`, () => { expect( convertToEcashPrefix( 'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr', ), ).toBe('ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035'); }); it(`convertToEcashPrefix returns an ecash: prefix address unchanged`, () => { expect( convertToEcashPrefix( 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035', ), ).toBe('ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035'); }); it(`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(`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('convertEtokenToEcashAddr successfully converts a valid eToken address to eCash', async () => { const result = convertEtokenToEcashAddr( 'etoken:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs', ); expect(result).toStrictEqual( 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8', ); }); test('convertEtokenToEcashAddr successfully converts prefixless eToken address as input', async () => { const result = convertEtokenToEcashAddr( 'qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs', ); expect(result).toStrictEqual( 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8', ); }); test('convertEtokenToEcashAddr throws error with an invalid eToken address as input', async () => { const result = convertEtokenToEcashAddr('etoken:qpj9gcldpffcs'); expect(result).toStrictEqual( new Error( 'cashMethods.convertToEcashAddr() error: etoken:qpj9gcldpffcs is not a valid etoken address', ), ); }); test('convertEtokenToEcashAddr throws error with an ecash address as input', async () => { const result = convertEtokenToEcashAddr( 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8', ); expect(result).toStrictEqual( new Error( 'cashMethods.convertToEcashAddr() error: ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8 is not a valid etoken address', ), ); }); test('convertEtokenToEcashAddr throws error with null input', async () => { const result = convertEtokenToEcashAddr(null); expect(result).toStrictEqual( new Error( 'cashMethods.convertToEcashAddr() error: No etoken address provided', ), ); }); test('convertEtokenToEcashAddr throws error with empty string input', async () => { const result = convertEtokenToEcashAddr(''); expect(result).toStrictEqual( new Error( 'cashMethods.convertToEcashAddr() error: No etoken address provided', ), ); }); test('convertEcashtoEtokenAddr successfully converts a valid ecash address into an etoken address', async () => { const eCashAddress = 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8'; const eTokenAddress = 'etoken:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs'; const result = convertEcashtoEtokenAddr(eCashAddress); expect(result).toStrictEqual(eTokenAddress); }); test('convertEcashtoEtokenAddr successfully converts a valid prefix-less ecash address into an etoken address', async () => { const eCashAddress = 'qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8'; const eTokenAddress = 'etoken:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs'; const result = convertEcashtoEtokenAddr(eCashAddress); expect(result).toStrictEqual(eTokenAddress); }); test('convertEcashtoEtokenAddr throws error with invalid ecash address input', async () => { const eCashAddress = 'ecash:qpaNOTVALIDADDRESSwu8'; const result = convertEcashtoEtokenAddr(eCashAddress); expect(result).toStrictEqual( new Error(eCashAddress + ' is not a valid ecash address'), ); }); test('convertEcashtoEtokenAddr throws error with a valid etoken address input', async () => { const eTokenAddress = 'etoken:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs'; const result = convertEcashtoEtokenAddr(eTokenAddress); expect(result).toStrictEqual( new Error(eTokenAddress + ' is not a valid ecash address'), ); }); test('convertEcashtoEtokenAddr throws error with a valid bitcoincash address input', async () => { const bchAddress = 'bitcoincash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9g0vsgy56s'; const result = convertEcashtoEtokenAddr(bchAddress); expect(result).toStrictEqual( new Error(bchAddress + ' is not a valid ecash address'), ); }); test('convertEcashtoEtokenPrefix throws error with null ecash address input', async () => { const eCashAddress = null; const result = convertEcashtoEtokenAddr(eCashAddress); expect(result).toStrictEqual( new Error(eCashAddress + ' is not a valid ecash address'), ); }); it(`flattenContactList flattens contactList array by returning an array of addresses`, () => { expect( flattenContactList([ { address: 'ecash:qpdkc5p7f25hwkxsr69m3evlj4h7wqq9xcgmjc8sxr', name: 'Alpha', }, { address: 'ecash:qpq235n3l3u6ampc8slapapnatwfy446auuv64ylt2', name: 'Beta', }, { address: 'ecash:qz50e58nkeg2ej2f34z6mhwylp6ven8emy8pp52r82', name: 'Gamma', }, ]), ).toStrictEqual([ 'ecash:qpdkc5p7f25hwkxsr69m3evlj4h7wqq9xcgmjc8sxr', 'ecash:qpq235n3l3u6ampc8slapapnatwfy446auuv64ylt2', 'ecash:qz50e58nkeg2ej2f34z6mhwylp6ven8emy8pp52r82', ]); }); it(`flattenContactList flattens contactList array of length 1 by returning an array of 1 address`, () => { expect( flattenContactList([ { address: 'ecash:qpdkc5p7f25hwkxsr69m3evlj4h7wqq9xcgmjc8sxr', name: 'Alpha', }, ]), ).toStrictEqual(['ecash:qpdkc5p7f25hwkxsr69m3evlj4h7wqq9xcgmjc8sxr']); }); it(`flattenContactList returns an empty array for invalid input`, () => { expect(flattenContactList(false)).toStrictEqual([]); }); it(`getHashArrayFromWallet returns false for a legacy wallet`, () => { expect( getHashArrayFromWallet(mockLegacyWallets.legacyAlphaMainnet), ).toBe(false); }); it(`Successfully extracts a hash160 array from a migrated wallet object`, () => { expect( getHashArrayFromWallet( mockLegacyWallets.migratedLegacyAlphaMainnet, ), ).toStrictEqual([ '960c9ed561f1699f0c49974d50b3bb7cdc118625', '2be0e0c999e7e77a443ea726f82c441912fca92b', 'ba8257db65f40359989c7b894c5e88ed7b6344f6', ]); }); it(`isActiveWebsocket returns true for an active chronik websocket connection`, () => { expect(isActiveWebsocket(activeWebsocketAlpha)).toBe(true); }); it(`isActiveWebsocket returns false for a disconnected chronik websocket connection`, () => { expect(isActiveWebsocket(disconnectedWebsocketAlpha)).toBe(false); }); it(`isActiveWebsocket returns false for a null chronik websocket connection`, () => { expect(isActiveWebsocket(null)).toBe(false); }); it(`isActiveWebsocket returns false for an active websocket connection with no subscriptions`, () => { expect(isActiveWebsocket(unsubscribedWebsocket)).toBe(false); }); }); diff --git a/web/cashtab/src/utils/cashMethods.js b/web/cashtab/src/utils/cashMethods.js index 4b00048db..f1a14cc7a 100644 --- a/web/cashtab/src/utils/cashMethods.js +++ b/web/cashtab/src/utils/cashMethods.js @@ -1,964 +1,1043 @@ import { currency } from 'components/Common/Ticker'; import { isValidXecAddress, isValidEtokenAddress, isValidContactList, } from 'utils/validation'; import BigNumber from 'bignumber.js'; import cashaddr from 'ecashaddrjs'; import useBCH from '../hooks/useBCH'; export const getUtxoWif = (utxo, wallet) => { if (!wallet) { throw new Error('Invalid wallet parameter'); } const accounts = [wallet.Path245, wallet.Path145, wallet.Path1899]; const wif = accounts .filter(acc => acc.cashAddress === utxo.address) .pop().fundingWif; return wif; }; export const signUtxosByAddress = (BCH, inputUtxos, wallet, txBuilder) => { for (let i = 0; i < inputUtxos.length; i++) { const utxo = inputUtxos[i]; const accounts = [wallet.Path245, wallet.Path145, wallet.Path1899]; const utxoEcPair = BCH.ECPair.fromWIF( accounts.filter(acc => acc.cashAddress === utxo.address).pop() .fundingWif, ); txBuilder.sign( i, utxoEcPair, undefined, txBuilder.hashTypes.SIGHASH_ALL, parseInt(utxo.value), ); } return txBuilder; }; +export const generateTokenTxOutput = ( + BCH, + txBuilder, + tokenAction, + legacyCashOriginAddress, + tokenUtxosBeingSpent = [], // optional - send or burn tx only + remainderXecValue = new BigNumber(0), // optional - only if > dust + tokenConfigObj = {}, // optional - genesis only + tokenRecipientAddress = false, // optional - send tx only + tokenAmount = false, // optional - send or burn amount for send/burn tx only +) => { + try { + if (!BCH || !tokenAction || !legacyCashOriginAddress || !txBuilder) { + throw new Error('Invalid token tx output parameter'); + } + + let script, opReturnObj, destinationAddress; + switch (tokenAction) { + case 'GENESIS': + script = + BCH.SLP.TokenType1.generateGenesisOpReturn(tokenConfigObj); + destinationAddress = legacyCashOriginAddress; + break; + case 'SEND': + opReturnObj = BCH.SLP.TokenType1.generateSendOpReturn( + tokenUtxosBeingSpent, + tokenAmount.toString(), + ); + script = opReturnObj.script; + destinationAddress = BCH.SLP.Address.toLegacyAddress( + tokenRecipientAddress, + ); + break; + case 'BURN': + script = BCH.SLP.TokenType1.generateBurnOpReturn( + tokenUtxosBeingSpent, + tokenAmount, + ); + destinationAddress = BCH.SLP.Address.toLegacyAddress( + legacyCashOriginAddress, + ); + break; + default: + throw new Error('Invalid token transaction type'); + } + + // OP_RETURN needs to be the first output in the transaction. + txBuilder.addOutput(script, 0); + + // add XEC dust output as fee for genesis, send or burn token output + txBuilder.addOutput(destinationAddress, parseInt(currency.etokenSats)); + + // Return any token change back to the sender for send and burn txs + if ( + tokenAction !== 'GENESIS' || + (opReturnObj && opReturnObj.outputs > 1) + ) { + // add XEC dust output as fee + txBuilder.addOutput( + tokenUtxosBeingSpent[0].address, // etoken address + parseInt(currency.etokenSats), + ); + } + + // Send xec change to own address + if (remainderXecValue.gte(new BigNumber(currency.dustSats))) { + txBuilder.addOutput( + legacyCashOriginAddress, + parseInt(remainderXecValue), + ); + } + } catch (err) { + console.log(`generateTokenTxOutput() error: ` + err); + throw err; + } + + return txBuilder; +}; + export const generateTxInput = ( 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.outpoint.outIdx; const txid = utxo.outpoint.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 generateTokenTxInput = ( BCH, tokenAction, // GENESIS, SEND or BURN totalXecUtxos, totalTokenUtxos, tokenId, tokenAmount, // optional - only for sending or burning feeInSatsPerByte, txBuilder, ) => { let totalXecInputUtxoValue = new BigNumber(0); let remainderXecValue = new BigNumber(0); let remainderTokenValue = new BigNumber(0); let totalXecInputUtxos = []; let txFee = 0; const { calcFee } = useBCH(); let tokenUtxosBeingSpent = []; try { if ( !BCH || !tokenAction || !totalXecUtxos || !tokenId || !feeInSatsPerByte || !txBuilder ) { throw new Error('Invalid token tx input parameter'); } // collate XEC UTXOs for this token tx const txOutputs = tokenAction === 'GENESIS' ? 2 // one for genesis OP_RETURN output and one for change : 4; // for SEND/BURN token txs see T2645 on why this is not dynamically generated for (let i = 0; i < totalXecUtxos.length; i++) { const thisXecUtxo = totalXecUtxos[i]; totalXecInputUtxoValue = totalXecInputUtxoValue.plus( new BigNumber(thisXecUtxo.value), ); const vout = thisXecUtxo.outpoint.outIdx; const txid = thisXecUtxo.outpoint.txid; // add input with txid and index of vout txBuilder.addInput(txid, vout); totalXecInputUtxos.push(thisXecUtxo); txFee = calcFee( BCH, totalXecInputUtxos, txOutputs, feeInSatsPerByte, ); remainderXecValue = tokenAction === 'GENESIS' ? totalXecInputUtxoValue .minus(new BigNumber(currency.etokenSats)) .minus(new BigNumber(txFee)) : totalXecInputUtxoValue .minus(new BigNumber(currency.etokenSats * 2)) // one for token send/burn output, one for token change .minus(new BigNumber(txFee)); if (remainderXecValue.gte(0)) { break; } } if (remainderXecValue.lt(0)) { throw new Error(`Insufficient funds`); } let filteredTokenInputUtxos = []; let finalTokenAmountSpent = new BigNumber(0); let tokenAmountBeingSpent = new BigNumber(tokenAmount); if (tokenAction === 'SEND' || tokenAction === 'BURN') { // filter for token UTXOs matching the token being sent/burnt filteredTokenInputUtxos = totalTokenUtxos.filter(utxo => { if ( utxo && // UTXO is associated with a token. utxo.slpMeta.tokenId === tokenId && // UTXO matches the token ID. !utxo.slpToken.isMintBaton // UTXO is not a minting baton. ) { return true; } return false; }); if (filteredTokenInputUtxos.length === 0) { throw new Error( 'No token UTXOs for the specified token could be found.', ); } // collate token UTXOs to cover the token amount being sent/burnt for (let i = 0; i < filteredTokenInputUtxos.length; i++) { finalTokenAmountSpent = finalTokenAmountSpent.plus( new BigNumber(filteredTokenInputUtxos[i].tokenQty), ); txBuilder.addInput( filteredTokenInputUtxos[i].outpoint.txid, filteredTokenInputUtxos[i].outpoint.outIdx, ); tokenUtxosBeingSpent.push(filteredTokenInputUtxos[i]); if (tokenAmountBeingSpent.lte(finalTokenAmountSpent)) { break; } } // calculate token change remainderTokenValue = finalTokenAmountSpent.minus( new BigNumber(tokenAmount), ); if (remainderTokenValue.lt(0)) { throw new Error( 'Insufficient token UTXOs for the specified token amount.', ); } } } catch (err) { console.log(`generateTokenTxInput() error: ` + err); throw err; } return { txBuilder: txBuilder, inputXecUtxos: totalXecInputUtxos, inputTokenUtxos: tokenUtxosBeingSpent, remainderXecValue: remainderXecValue, remainderTokenValue: remainderTokenValue, }; }; export const getChangeAddressFromInputUtxos = (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(fromSatoshisToXec(currency.dustSats).toString()), ) ) { // Throw the same error given by the backend attempting to broadcast such a tx throw new Error('dust'); } } catch (err) { console.log('Error in parseXecSendValue: ' + err); throw err; } return value; }; /* * 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(fromXecToSatoshis(outputValue)), ); } } else { // for one to one mode, add output w/ single address and amount to send txBuilder.addOutput( BCH.Address.toCashAddress(destinationAddress), parseInt(fromXecToSatoshis(singleSendValue)), ); } // if a remainder exists, return to change address as the final output if (remainder.gte(new BigNumber(currency.dustSats))) { txBuilder.addOutput(changeAddress, parseInt(remainder)); } } catch (err) { console.log('Error in generateTxOutput(): ' + err); throw err; } return txBuilder; }; export const signAndBuildTx = (BCH, inputUtxos, txBuilder, wallet) => { if ( !BCH || !inputUtxos || inputUtxos.length === 0 || !txBuilder || !wallet || // txBuilder.transaction.tx.ins is empty until the inputUtxos are signed txBuilder.transaction.tx.outs.length === 0 ) { throw new Error('Invalid buildTx parameter'); } // Sign each XEC UTXO being consumed and refresh transactionBuilder txBuilder = signUtxosByAddress(BCH, inputUtxos, wallet, txBuilder); let hex; try { // build tx const tx = txBuilder.build(); // output rawhex hex = tx.toHex(); } catch (err) { throw new Error('Transaction build failed'); } return hex; }; export function parseOpReturn(hexStr) { if ( !hexStr || typeof hexStr !== 'string' || hexStr.substring(0, 2) !== currency.opReturn.opReturnPrefixHex ) { return false; } hexStr = hexStr.slice(2); // remove the first byte i.e. 6a /* * @Return: resultArray is structured as follows: * resultArray[0] is the transaction type i.e. eToken prefix, cashtab prefix, external message itself if unrecognized prefix * resultArray[1] is the actual cashtab message or the 2nd part of an external message * resultArray[2 - n] are the additional messages for future protcols */ let resultArray = []; let message = ''; let hexStrLength = hexStr.length; for (let i = 0; hexStrLength !== 0; i++) { // part 1: check the preceding byte value for the subsequent message let byteValue = hexStr.substring(0, 2); let msgByteSize = 0; if (byteValue === currency.opReturn.opPushDataOne) { // if this byte is 4c then the next byte is the message byte size - retrieve the message byte size only msgByteSize = parseInt(hexStr.substring(2, 4), 16); // hex base 16 to decimal base 10 hexStr = hexStr.slice(4); // strip the 4c + message byte size info } else { // take the byte as the message byte size msgByteSize = parseInt(hexStr.substring(0, 2), 16); // hex base 16 to decimal base 10 hexStr = hexStr.slice(2); // strip the message byte size info } // part 2: parse the subsequent message based on bytesize const msgCharLength = 2 * msgByteSize; message = hexStr.substring(0, msgCharLength); if (i === 0 && message === currency.opReturn.appPrefixesHex.eToken) { // add the extracted eToken prefix to array then exit loop resultArray[i] = currency.opReturn.appPrefixesHex.eToken; break; } else if ( i === 0 && message === currency.opReturn.appPrefixesHex.cashtab ) { // add the extracted Cashtab prefix to array resultArray[i] = currency.opReturn.appPrefixesHex.cashtab; } else if ( i === 0 && message === currency.opReturn.appPrefixesHex.cashtabEncrypted ) { // add the Cashtab encryption prefix to array resultArray[i] = currency.opReturn.appPrefixesHex.cashtabEncrypted; } else if ( i === 0 && message === currency.opReturn.appPrefixesHex.airdrop ) { // add the airdrop prefix to array resultArray[i] = currency.opReturn.appPrefixesHex.airdrop; } else { // this is either an external message or a subsequent cashtab message loop to extract the message resultArray[i] = message; } // strip out the parsed message hexStr = hexStr.slice(msgCharLength); hexStrLength = hexStr.length; } return resultArray; } export const fromLegacyDecimals = ( amount, cashDecimals = currency.cashDecimals, ) => { // Input 0.00000546 BCH // Output 5.46 XEC or 0.00000546 BCH, depending on currency.cashDecimals const amountBig = new BigNumber(amount); const conversionFactor = new BigNumber(10 ** (8 - cashDecimals)); const amountSmallestDenomination = amountBig .times(conversionFactor) .toNumber(); return amountSmallestDenomination; }; export const fromSatoshisToXec = ( amount, cashDecimals = currency.cashDecimals, ) => { const amountBig = new BigNumber(amount); const multiplier = new BigNumber(10 ** (-1 * cashDecimals)); const amountInBaseUnits = amountBig.times(multiplier); return amountInBaseUnits; }; export const fromXecToSatoshis = ( sendAmount, cashDecimals = currency.cashDecimals, ) => { // Replace the BCH.toSatoshi method with an equivalent function that works for arbitrary decimal places // Example, for an 8 decimal place currency like Bitcoin // Input: a BigNumber of the amount of Bitcoin to be sent // Output: a BigNumber of the amount of satoshis to be sent, or false if input is invalid // Validate // Input should be a BigNumber with no more decimal places than cashDecimals const isValidSendAmount = BigNumber.isBigNumber(sendAmount) && sendAmount.dp() <= cashDecimals; if (!isValidSendAmount) { return false; } const conversionFactor = new BigNumber(10 ** cashDecimals); const sendAmountSmallestDenomination = sendAmount.times(conversionFactor); return sendAmountSmallestDenomination; }; export const flattenContactList = contactList => { /* Converts contactList from array of objects of type {address: , name: } to array of addresses only If contact list is invalid, returns and empty array */ if (!isValidContactList(contactList)) { return []; } let flattenedContactList = []; for (let i = 0; i < contactList.length; i += 1) { const thisAddress = contactList[i].address; flattenedContactList.push(thisAddress); } return flattenedContactList; }; export const loadStoredWallet = walletStateFromStorage => { // Accept cached tokens array that does not save BigNumber type of BigNumbers // Return array with BigNumbers converted // See BigNumber.js api for how to create a BigNumber object from an object // https://mikemcl.github.io/bignumber.js/ const liveWalletState = walletStateFromStorage; const { slpBalancesAndUtxos, tokens } = liveWalletState; for (let i = 0; i < tokens.length; i += 1) { const thisTokenBalance = tokens[i].balance; thisTokenBalance._isBigNumber = true; tokens[i].balance = new BigNumber(thisTokenBalance); } // Also confirm balance is correct // Necessary step in case currency.decimals changed since last startup const balancesRebased = getWalletBalanceFromUtxos( slpBalancesAndUtxos.nonSlpUtxos, ); liveWalletState.balances = balancesRebased; return liveWalletState; }; export const getWalletBalanceFromUtxos = nonSlpUtxos => { const totalBalanceInSatoshis = nonSlpUtxos.reduce( (previousBalance, utxo) => previousBalance.plus(new BigNumber(utxo.value)), new BigNumber(0), ); return { totalBalanceInSatoshis: totalBalanceInSatoshis.toString(), totalBalance: fromSatoshisToXec(totalBalanceInSatoshis).toString(), }; }; export const isValidStoredWallet = walletStateFromStorage => { return ( typeof walletStateFromStorage === 'object' && 'state' in walletStateFromStorage && typeof walletStateFromStorage.state === 'object' && 'balances' in walletStateFromStorage.state && 'utxos' in walletStateFromStorage.state && !('hydratedUtxoDetails' in walletStateFromStorage.state) && 'slpBalancesAndUtxos' in walletStateFromStorage.state && 'tokens' in walletStateFromStorage.state ); }; export const getWalletState = wallet => { if (!wallet || !wallet.state) { return { balances: { totalBalance: 0, totalBalanceInSatoshis: 0 }, hydratedUtxoDetails: {}, tokens: [], slpBalancesAndUtxos: {}, parsedTxHistory: [], utxos: [], }; } return wallet.state; }; export function convertEtokenToEcashAddr(eTokenAddress) { if (!eTokenAddress) { return new Error( `cashMethods.convertToEcashAddr() error: No etoken address provided`, ); } // Confirm input is a valid eToken address const isValidInput = isValidEtokenAddress(eTokenAddress); if (!isValidInput) { return new Error( `cashMethods.convertToEcashAddr() error: ${eTokenAddress} is not a valid etoken address`, ); } // Check for etoken: prefix const isPrefixedEtokenAddress = eTokenAddress.slice(0, 7) === 'etoken:'; // If no prefix, assume it is checksummed for an etoken: prefix const testedEtokenAddr = isPrefixedEtokenAddress ? eTokenAddress : `etoken:${eTokenAddress}`; let ecashAddress; try { const { type, hash } = cashaddr.decode(testedEtokenAddr); ecashAddress = cashaddr.encode('ecash', type, hash); } catch (err) { return err; } return ecashAddress; } export function convertToEcashPrefix(bitcoincashPrefixedAddress) { // Prefix-less addresses may be valid, but the cashaddr.decode function used below // will throw an error without a prefix. Hence, must ensure prefix to use that function. const hasPrefix = bitcoincashPrefixedAddress.includes(':'); if (hasPrefix) { // Is it bitcoincash: or simpleledger: const { type, hash, prefix } = cashaddr.decode( bitcoincashPrefixedAddress, ); let newPrefix; if (prefix === 'bitcoincash') { newPrefix = 'ecash'; } else if (prefix === 'simpleledger') { newPrefix = 'etoken'; } else { return bitcoincashPrefixedAddress; } const convertedAddress = cashaddr.encode(newPrefix, type, hash); return convertedAddress; } else { return bitcoincashPrefixedAddress; } } export function convertEcashtoEtokenAddr(eCashAddress) { const isValidInput = isValidXecAddress(eCashAddress); if (!isValidInput) { return new Error(`${eCashAddress} is not a valid ecash address`); } // Check for ecash: prefix const isPrefixedEcashAddress = eCashAddress.slice(0, 6) === 'ecash:'; // If no prefix, assume it is checksummed for an ecash: prefix const testedEcashAddr = isPrefixedEcashAddress ? eCashAddress : `ecash:${eCashAddress}`; let eTokenAddress; try { const { type, hash } = cashaddr.decode(testedEcashAddr); eTokenAddress = cashaddr.encode('etoken', type, hash); } catch (err) { return new Error('eCash to eToken address conversion error'); } return eTokenAddress; } 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; } /* Converts a serialized buffer containing encrypted data into an object * that can be interpreted by the ecies-lite library. * * For reference on the parsing logic in this function refer to the link below on the segment of * ecies-lite's encryption function where the encKey, macKey, iv and cipher are sliced and concatenated * https://github.com/tibetty/ecies-lite/blob/8fd97e80b443422269d0223ead55802378521679/index.js#L46-L55 * * A similar PSF implmentation can also be found at: * https://github.com/Permissionless-Software-Foundation/bch-encrypt-lib/blob/master/lib/encryption.js * * For more detailed overview on the ecies encryption scheme, see https://cryptobook.nakov.com/asymmetric-key-ciphers/ecies-public-key-encryption */ export const convertToEncryptStruct = encryptionBuffer => { // based on ecies-lite's encryption logic, the encryption buffer is concatenated as follows: // [ epk + iv + ct + mac ] whereby: // - The first 32 or 64 chars of the encryptionBuffer is the epk // - Both iv and ct params are 16 chars each, hence their combined substring is 32 chars from the end of the epk string // - within this combined iv/ct substring, the first 16 chars is the iv param, and ct param being the later half // - The mac param is appended to the end of the encryption buffer // validate input buffer if (!encryptionBuffer) { throw new Error( 'cashmethods.convertToEncryptStruct() error: input must be a buffer', ); } try { // variable tracking the starting char position for string extraction purposes let startOfBuf = 0; // *** epk param extraction *** // The first char of the encryptionBuffer indicates the type of the public key // If the first char is 4, then the public key is 64 chars // If the first char is 3 or 2, then the public key is 32 chars // Otherwise this is not a valid encryption buffer compatible with the ecies-lite library let publicKey; switch (encryptionBuffer[0]) { case 4: publicKey = encryptionBuffer.slice(0, 65); // extract first 64 chars as public key break; case 3: case 2: publicKey = encryptionBuffer.slice(0, 33); // extract first 32 chars as public key break; default: throw new Error(`Invalid type: ${encryptionBuffer[0]}`); } // *** iv and ct param extraction *** startOfBuf += publicKey.length; // sets the starting char position to the end of the public key (epk) in order to extract subsequent iv and ct substrings const encryptionTagLength = 32; // the length of the encryption tag (i.e. mac param) computed from each block of ciphertext, and is used to verify no one has tampered with the encrypted data const ivCtSubstring = encryptionBuffer.slice( startOfBuf, encryptionBuffer.length - encryptionTagLength, ); // extract the substring containing both iv and ct params, which is after the public key but before the mac param i.e. the 'encryption tag' const ivbufParam = ivCtSubstring.slice(0, 16); // extract the first 16 chars of substring as the iv param const ctbufParam = ivCtSubstring.slice(16); // extract the last 16 chars as substring the ct param // *** mac param extraction *** const macParam = encryptionBuffer.slice( encryptionBuffer.length - encryptionTagLength, encryptionBuffer.length, ); // extract the mac param appended to the end of the buffer return { iv: ivbufParam, epk: publicKey, ct: ctbufParam, mac: macParam, }; } catch (err) { console.error(`useBCH.convertToEncryptStruct() error: `, err); throw err; } }; export const isLegacyMigrationRequired = wallet => { // If the wallet does not have Path1899, // Or each Path1899, Path145, Path245 does not have a public key // Then it requires migration if ( !wallet.Path1899 || !wallet.Path1899.publicKey || !wallet.Path1899.hash160 || !wallet.Path145.publicKey || !wallet.Path145.hash160 || !wallet.Path245.publicKey || !wallet.Path245.hash160 ) { return true; } return false; }; export const getHashArrayFromWallet = wallet => { // If the wallet has wallet.Path1899.hash160, it's migrated and will have all of them // Return false for an umigrated wallet const hash160Array = wallet && wallet.Path1899 && 'hash160' in wallet.Path1899 ? [ wallet.Path245.hash160, wallet.Path145.hash160, wallet.Path1899.hash160, ] : false; return hash160Array; }; export const isActiveWebsocket = ws => { // Return true if websocket is connected and subscribed // Otherwise return false return ( ws !== null && ws && '_ws' in ws && 'readyState' in ws._ws && ws._ws.readyState === 1 && '_subs' in ws && ws._subs.length > 0 ); };