diff --git a/cashtab/src/utils/__mocks__/mockLegacyWalletsUtils.js b/cashtab/src/utils/__mocks__/mockLegacyWalletsUtils.js index 1e6223836..a1290c7c4 100644 --- a/cashtab/src/utils/__mocks__/mockLegacyWalletsUtils.js +++ b/cashtab/src/utils/__mocks__/mockLegacyWalletsUtils.js @@ -1,207 +1,327 @@ export const missingPath1899Wallet = { Path145: { cashAddress: 'bitcoincash:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9v0lgx569z', fundingAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', fundingWif: 'L2xvTe6CdNxroR6pbdpGWNjAa55AZX5Wm59W5TXMuH31ihNJdDjt', legacyAddress: '1511T3ynXKgCwXhFijCUWKuTfqbPxFV1AF', slpAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', }, Path245: { cashAddress: 'bitcoincash:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy54hkry298', fundingAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', fundingWif: 'KwgNkyijAaxFr5XQdnaYyNMXVSZobgHzSoKKfWiC3Q7Xr4n7iYMG', legacyAddress: '1EgPUfBgU7ekho3EjtGze87dRADnUE8ojP', slpAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', }, mnemonic: 'apart vacuum color cream drama kind foil history hurt alone ask census', name: 'Missing Path1899', state: {}, }; export const missingPublicKeyInPath1899Wallet = { Path145: { cashAddress: 'bitcoincash:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9v0lgx569z', fundingAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', fundingWif: 'L2xvTe6CdNxroR6pbdpGWNjAa55AZX5Wm59W5TXMuH31ihNJdDjt', legacyAddress: '1511T3ynXKgCwXhFijCUWKuTfqbPxFV1AF', publicKey: '02fe5308d77bcce825068a9e46adc6f032dbbe39167a7b6d05ac563ac71d8b186e', slpAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', }, Path1899: { cashAddress: 'bitcoincash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cptzgcqy6', fundingAddress: 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', fundingWif: 'Kx4FiBMvKK1iXjFk5QTaAK6E4mDGPjmwDZ2HDKGUZpE4gCXMaPe9', legacyAddress: '1J1Aq5tAAYxZgSDRo8soKM2Rb41z3xrYpm', slpAddress: 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', }, Path245: { cashAddress: 'bitcoincash:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy54hkry298', fundingAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', fundingWif: 'KwgNkyijAaxFr5XQdnaYyNMXVSZobgHzSoKKfWiC3Q7Xr4n7iYMG', legacyAddress: '1EgPUfBgU7ekho3EjtGze87dRADnUE8ojP', publicKey: '03c4a69fd90c8b196683216cffd2943a7b13b0db0812e44a4ff156ac7e03fc4ed7', slpAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', }, mnemonic: 'apart vacuum color cream drama kind foil history hurt alone ask census', name: 'Missing Public Key in Path1899', state: {}, }; export const missingPublicKeyInPath145Wallet = { Path145: { cashAddress: 'bitcoincash:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9v0lgx569z', fundingAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', fundingWif: 'L2xvTe6CdNxroR6pbdpGWNjAa55AZX5Wm59W5TXMuH31ihNJdDjt', legacyAddress: '1511T3ynXKgCwXhFijCUWKuTfqbPxFV1AF', slpAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', }, Path1899: { cashAddress: 'bitcoincash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cptzgcqy6', fundingAddress: 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', fundingWif: 'Kx4FiBMvKK1iXjFk5QTaAK6E4mDGPjmwDZ2HDKGUZpE4gCXMaPe9', legacyAddress: '1J1Aq5tAAYxZgSDRo8soKM2Rb41z3xrYpm', publicKey: '02a06bb380cf180d703f6f80796a13555aefff817d1f6f842f1e5c555b15f0fa70', slpAddress: 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', }, Path245: { cashAddress: 'bitcoincash:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy54hkry298', fundingAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', fundingWif: 'KwgNkyijAaxFr5XQdnaYyNMXVSZobgHzSoKKfWiC3Q7Xr4n7iYMG', legacyAddress: '1EgPUfBgU7ekho3EjtGze87dRADnUE8ojP', publicKey: '03c4a69fd90c8b196683216cffd2943a7b13b0db0812e44a4ff156ac7e03fc4ed7', slpAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', }, mnemonic: 'apart vacuum color cream drama kind foil history hurt alone ask census', name: 'Missing Public Key in Path145', state: {}, }; export const missingPublicKeyInPath245Wallet = { Path145: { cashAddress: 'bitcoincash:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9v0lgx569z', fundingAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', fundingWif: 'L2xvTe6CdNxroR6pbdpGWNjAa55AZX5Wm59W5TXMuH31ihNJdDjt', legacyAddress: '1511T3ynXKgCwXhFijCUWKuTfqbPxFV1AF', publicKey: '02fe5308d77bcce825068a9e46adc6f032dbbe39167a7b6d05ac563ac71d8b186e', slpAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', }, Path1899: { cashAddress: 'bitcoincash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cptzgcqy6', fundingAddress: 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', fundingWif: 'Kx4FiBMvKK1iXjFk5QTaAK6E4mDGPjmwDZ2HDKGUZpE4gCXMaPe9', legacyAddress: '1J1Aq5tAAYxZgSDRo8soKM2Rb41z3xrYpm', publicKey: '02a06bb380cf180d703f6f80796a13555aefff817d1f6f842f1e5c555b15f0fa70', slpAddress: 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', }, Path245: { cashAddress: 'bitcoincash:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy54hkry298', fundingAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', fundingWif: 'KwgNkyijAaxFr5XQdnaYyNMXVSZobgHzSoKKfWiC3Q7Xr4n7iYMG', legacyAddress: '1EgPUfBgU7ekho3EjtGze87dRADnUE8ojP', slpAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', }, mnemonic: 'apart vacuum color cream drama kind foil history hurt alone ask census', name: 'Missing Public Key in Path245', state: {}, }; -export const notLegacyWallet = { +export const notLegacyWalletWithXecPrefixes = { Path145: { - cashAddress: 'bitcoincash:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9v0lgx569z', + cashAddress: 'ecash:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vkjud0qr4', fundingAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', fundingWif: 'L2xvTe6CdNxroR6pbdpGWNjAa55AZX5Wm59W5TXMuH31ihNJdDjt', hash160: '2be0e0c999e7e77a443ea726f82c441912fca92b', legacyAddress: '1511T3ynXKgCwXhFijCUWKuTfqbPxFV1AF', publicKey: '02fe5308d77bcce825068a9e46adc6f032dbbe39167a7b6d05ac563ac71d8b186e', slpAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', }, Path1899: { - cashAddress: 'bitcoincash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cptzgcqy6', + cashAddress: 'ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd', + fundingAddress: + 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', + fundingWif: 'Kx4FiBMvKK1iXjFk5QTaAK6E4mDGPjmwDZ2HDKGUZpE4gCXMaPe9', + hash160: 'ba8257db65f40359989c7b894c5e88ed7b6344f6', + legacyAddress: '1J1Aq5tAAYxZgSDRo8soKM2Rb41z3xrYpm', + publicKey: + '02a06bb380cf180d703f6f80796a13555aefff817d1f6f842f1e5c555b15f0fa70', + slpAddress: 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', + }, + Path245: { + cashAddress: 'ecash:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5v6zglsrs', + fundingAddress: + 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + fundingWif: 'KwgNkyijAaxFr5XQdnaYyNMXVSZobgHzSoKKfWiC3Q7Xr4n7iYMG', + hash160: '960c9ed561f1699f0c49974d50b3bb7cdc118625', + legacyAddress: '1EgPUfBgU7ekho3EjtGze87dRADnUE8ojP', + publicKey: + '03c4a69fd90c8b196683216cffd2943a7b13b0db0812e44a4ff156ac7e03fc4ed7', + slpAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + }, + mnemonic: + 'apart vacuum color cream drama kind foil history hurt alone ask census', + name: 'Current Wallet, Not Legacy', + state: {}, +}; + +export const notLegacyWalletWithPath245OnBchPrefix = { + Path145: { + cashAddress: 'ecash:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vkjud0qr4', + fundingAddress: + 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + fundingWif: 'L2xvTe6CdNxroR6pbdpGWNjAa55AZX5Wm59W5TXMuH31ihNJdDjt', + hash160: '2be0e0c999e7e77a443ea726f82c441912fca92b', + legacyAddress: '1511T3ynXKgCwXhFijCUWKuTfqbPxFV1AF', + publicKey: + '02fe5308d77bcce825068a9e46adc6f032dbbe39167a7b6d05ac563ac71d8b186e', + slpAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + }, + Path1899: { + cashAddress: 'ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd', fundingAddress: 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', fundingWif: 'Kx4FiBMvKK1iXjFk5QTaAK6E4mDGPjmwDZ2HDKGUZpE4gCXMaPe9', hash160: 'ba8257db65f40359989c7b894c5e88ed7b6344f6', legacyAddress: '1J1Aq5tAAYxZgSDRo8soKM2Rb41z3xrYpm', publicKey: '02a06bb380cf180d703f6f80796a13555aefff817d1f6f842f1e5c555b15f0fa70', slpAddress: 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', }, Path245: { cashAddress: 'bitcoincash:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy54hkry298', fundingAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', fundingWif: 'KwgNkyijAaxFr5XQdnaYyNMXVSZobgHzSoKKfWiC3Q7Xr4n7iYMG', hash160: '960c9ed561f1699f0c49974d50b3bb7cdc118625', legacyAddress: '1EgPUfBgU7ekho3EjtGze87dRADnUE8ojP', publicKey: '03c4a69fd90c8b196683216cffd2943a7b13b0db0812e44a4ff156ac7e03fc4ed7', slpAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', }, mnemonic: 'apart vacuum color cream drama kind foil history hurt alone ask census', name: 'Current Wallet, Not Legacy', state: {}, }; +export const notLegacyWalletWithPath1899OnBchPrefix = { + Path145: { + cashAddress: 'ecash:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vkjud0qr4', + fundingAddress: + 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + fundingWif: 'L2xvTe6CdNxroR6pbdpGWNjAa55AZX5Wm59W5TXMuH31ihNJdDjt', + hash160: '2be0e0c999e7e77a443ea726f82c441912fca92b', + legacyAddress: '1511T3ynXKgCwXhFijCUWKuTfqbPxFV1AF', + publicKey: + '02fe5308d77bcce825068a9e46adc6f032dbbe39167a7b6d05ac563ac71d8b186e', + slpAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + }, + Path1899: { + cashAddress: 'bitcoincash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cptzgcqy6', + fundingAddress: + 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', + fundingWif: 'Kx4FiBMvKK1iXjFk5QTaAK6E4mDGPjmwDZ2HDKGUZpE4gCXMaPe9', + hash160: 'ba8257db65f40359989c7b894c5e88ed7b6344f6', + legacyAddress: '1J1Aq5tAAYxZgSDRo8soKM2Rb41z3xrYpm', + publicKey: + '02a06bb380cf180d703f6f80796a13555aefff817d1f6f842f1e5c555b15f0fa70', + slpAddress: 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', + }, + Path245: { + cashAddress: 'ecash:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5v6zglsrs', + fundingAddress: + 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + fundingWif: 'KwgNkyijAaxFr5XQdnaYyNMXVSZobgHzSoKKfWiC3Q7Xr4n7iYMG', + hash160: '960c9ed561f1699f0c49974d50b3bb7cdc118625', + legacyAddress: '1EgPUfBgU7ekho3EjtGze87dRADnUE8ojP', + publicKey: + '03c4a69fd90c8b196683216cffd2943a7b13b0db0812e44a4ff156ac7e03fc4ed7', + slpAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + }, + mnemonic: + 'apart vacuum color cream drama kind foil history hurt alone ask census', + name: 'Current Wallet, Not Legacy', + state: {}, +}; + +export const notLegacyWalletWithPath145OnBchPrefix = { + Path145: { + cashAddress: 'bitcoincash:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9v0lgx569z', + fundingAddress: + 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + fundingWif: 'L2xvTe6CdNxroR6pbdpGWNjAa55AZX5Wm59W5TXMuH31ihNJdDjt', + hash160: '2be0e0c999e7e77a443ea726f82c441912fca92b', + legacyAddress: '1511T3ynXKgCwXhFijCUWKuTfqbPxFV1AF', + publicKey: + '02fe5308d77bcce825068a9e46adc6f032dbbe39167a7b6d05ac563ac71d8b186e', + slpAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + }, + Path1899: { + cashAddress: 'ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd', + fundingAddress: + 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', + fundingWif: 'Kx4FiBMvKK1iXjFk5QTaAK6E4mDGPjmwDZ2HDKGUZpE4gCXMaPe9', + hash160: 'ba8257db65f40359989c7b894c5e88ed7b6344f6', + legacyAddress: '1J1Aq5tAAYxZgSDRo8soKM2Rb41z3xrYpm', + publicKey: + '02a06bb380cf180d703f6f80796a13555aefff817d1f6f842f1e5c555b15f0fa70', + slpAddress: 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', + }, + Path245: { + cashAddress: 'ecash:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5v6zglsrs', + fundingAddress: + 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + fundingWif: 'KwgNkyijAaxFr5XQdnaYyNMXVSZobgHzSoKKfWiC3Q7Xr4n7iYMG', + hash160: '960c9ed561f1699f0c49974d50b3bb7cdc118625', + legacyAddress: '1EgPUfBgU7ekho3EjtGze87dRADnUE8ojP', + publicKey: + '03c4a69fd90c8b196683216cffd2943a7b13b0db0812e44a4ff156ac7e03fc4ed7', + slpAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + }, + mnemonic: + 'apart vacuum color cream drama kind foil history hurt alone ask census', + name: 'Current Wallet, Not Legacy', + state: {}, +}; + export const missingHash160 = { Path145: { cashAddress: 'bitcoincash:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9v0lgx569z', fundingAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', fundingWif: 'L2xvTe6CdNxroR6pbdpGWNjAa55AZX5Wm59W5TXMuH31ihNJdDjt', legacyAddress: '1511T3ynXKgCwXhFijCUWKuTfqbPxFV1AF', publicKey: '02fe5308d77bcce825068a9e46adc6f032dbbe39167a7b6d05ac563ac71d8b186e', slpAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', }, Path1899: { cashAddress: 'bitcoincash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cptzgcqy6', fundingAddress: 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', fundingWif: 'Kx4FiBMvKK1iXjFk5QTaAK6E4mDGPjmwDZ2HDKGUZpE4gCXMaPe9', legacyAddress: '1J1Aq5tAAYxZgSDRo8soKM2Rb41z3xrYpm', publicKey: '02a06bb380cf180d703f6f80796a13555aefff817d1f6f842f1e5c555b15f0fa70', slpAddress: 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', }, Path245: { cashAddress: 'bitcoincash:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy54hkry298', fundingAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', fundingWif: 'KwgNkyijAaxFr5XQdnaYyNMXVSZobgHzSoKKfWiC3Q7Xr4n7iYMG', legacyAddress: '1EgPUfBgU7ekho3EjtGze87dRADnUE8ojP', publicKey: '03c4a69fd90c8b196683216cffd2943a7b13b0db0812e44a4ff156ac7e03fc4ed7', slpAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', }, mnemonic: 'apart vacuum color cream drama kind foil history hurt alone ask census', name: 'Missing Public Key in Path1899', state: {}, }; diff --git a/cashtab/src/utils/__tests__/cashMethods.test.js b/cashtab/src/utils/__tests__/cashMethods.test.js index a7001c51d..5650fbab9 100644 --- a/cashtab/src/utils/__tests__/cashMethods.test.js +++ b/cashtab/src/utils/__tests__/cashMethods.test.js @@ -1,1587 +1,1607 @@ import { BN } from 'slp-mdm'; import * as utxolib from '@bitgo/utxo-lib'; import cashaddr from 'ecashaddrjs'; import { fromSatoshisToXec, flattenContactList, loadStoredWallet, isValidStoredWallet, fromLegacyDecimals, convertToEcashPrefix, isLegacyMigrationRequired, convertEtokenToEcashAddr, parseOpReturn, convertEcashtoEtokenAddr, getHashArrayFromWallet, isActiveWebsocket, getChangeAddressFromInputUtxos, generateAliasOpReturnScript, generateTxInput, generateTxOutput, generateTokenTxInput, signAndBuildTx, fromXecToSatoshis, getWalletBalanceFromUtxos, signUtxosByAddress, generateTokenTxOutput, getCashtabByteCount, calcFee, toHash160, generateGenesisOpReturn, generateSendOpReturn, generateBurnOpReturn, hash160ToAddress, outputScriptToAddress, sumOneToManyXec, toSatoshis, } from 'utils/cashMethods'; import { validAddressArrayInput } from '../__mocks__/mockAddressArray'; import { mockGenesisOpReturnScript, mockSendOpReturnScript, mockSendOpReturnTokenUtxos, mockBurnOpReturnScript, mockBurnOpReturnTokenUtxos, } from '../__mocks__/mockOpReturnScript'; import { cachedUtxos, utxosLoadedFromCache, } from '../__mocks__/mockCachedUtxos'; import { validStoredWallet, invalidStoredWallet, invalidpreChronikStoredWallet, validStoredWalletAfter20221123Streamline, invalidStoredWalletMissingPath1899AndMnemonic, } from '../__mocks__/mockStoredWallets'; import { missingPath1899Wallet, missingPublicKeyInPath1899Wallet, missingPublicKeyInPath145Wallet, missingPublicKeyInPath245Wallet, - notLegacyWallet, + notLegacyWalletWithXecPrefixes, + notLegacyWalletWithPath145OnBchPrefix, + notLegacyWalletWithPath1899OnBchPrefix, + notLegacyWalletWithPath245OnBchPrefix, 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 sendBCHMock from '../__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 '../__mocks__/createToken'; import { opReturn as opreturnConfig } from 'config/opreturn'; import appConfig from 'config/app'; it(`generateSendOpReturn() returns correct script object for valid tokenUtxo and send quantity`, () => { const tokensToSend = 50; const sendOpReturnScriptObj = generateSendOpReturn( mockSendOpReturnTokenUtxos, tokensToSend, ); expect(JSON.stringify(sendOpReturnScriptObj.script)).toStrictEqual( JSON.stringify(mockSendOpReturnScript), ); }); it(`generateSendOpReturnScript() throws error on invalid input`, () => { const mockSendOpReturnTokenUtxos = null; const tokensToSend = 50; let errorThrown; try { generateSendOpReturn(mockSendOpReturnTokenUtxos, tokensToSend); } catch (err) { errorThrown = err.message; } expect(errorThrown).toStrictEqual('Invalid send token parameter'); }); it(`generateBurnOpReturn() returns correct script for valid tokenUtxo and burn quantity`, () => { const tokensToBurn = 7000; const burnOpReturnScript = generateBurnOpReturn( mockBurnOpReturnTokenUtxos, tokensToBurn, ); expect(JSON.stringify(burnOpReturnScript)).toStrictEqual( JSON.stringify(mockBurnOpReturnScript), ); }); it(`generateBurnOpReturn() throws error on invalid input`, () => { const tokensToBurn = 7000; let errorThrown; try { generateBurnOpReturn(null, tokensToBurn); } catch (err) { errorThrown = err.message; } expect(errorThrown).toStrictEqual('Invalid burn token parameter'); }); it(`generateGenesisOpReturn() returns correct script for a valid configObj`, () => { const configObj = { name: 'ethantest', ticker: 'ETN', documentUrl: 'https://cashtab.com/', decimals: '3', initialQty: '5000', documentHash: '', mintBatonVout: null, }; const genesisOpReturnScript = generateGenesisOpReturn(configObj); expect(JSON.stringify(genesisOpReturnScript)).toStrictEqual( JSON.stringify(mockGenesisOpReturnScript), ); }); it(`generateGenesisOpReturn() throws error on invalid configObj`, () => { const configObj = null; let errorThrown; try { generateGenesisOpReturn(configObj); } catch (err) { errorThrown = err.message; } expect(errorThrown).toStrictEqual('Invalid token configuration'); }); it(`signUtxosByAddress() successfully returns a txBuilder object for a one to one XEC tx`, () => { const isOneToMany = false; const { destinationAddress, wallet, utxos } = sendBCHMock; let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); const satoshisToSendInput = new BN(2184); const feeInSatsPerByte = appConfig.defaultFee; // mock tx input const inputObj = generateTxInput( isOneToMany, utxos, txBuilder, null, satoshisToSendInput, feeInSatsPerByte, ); // mock tx output const totalInputUtxoValue = mockOneToOneSendXecTxBuilderObj.transaction.inputs[0].value; const singleSendValue = new BN( fromSatoshisToXec( mockOneToOneSendXecTxBuilderObj.transaction.tx.outs[0].value, ), ); const satoshisToSendOutput = fromXecToSatoshis(new BN(singleSendValue)); const txFee = new BN(totalInputUtxoValue).minus( new BN(satoshisToSendOutput), ); const changeAddress = wallet.Path1899.cashAddress; const outputObj = generateTxOutput( isOneToMany, singleSendValue, satoshisToSendOutput, totalInputUtxoValue, destinationAddress, null, changeAddress, txFee, inputObj.txBuilder, ); const txBuilderResponse = signUtxosByAddress( mockSingleInputUtxo, wallet, outputObj, ); expect(txBuilderResponse.toString()).toStrictEqual( mockOneToOneSendXecTxBuilderObj.toString(), ); }); it(`signUtxosByAddress() successfully returns a txBuilder object for a one to many XEC tx`, () => { const isOneToMany = true; const { wallet, utxos } = sendBCHMock; let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); let destinationAddressAndValueArray = [ 'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,3000', 'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,3000', 'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,3000', ]; const satoshisToSendInput = new BN(900000); const feeInSatsPerByte = appConfig.defaultFee; // mock tx input const inputObj = generateTxInput( 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 BN( mockOneToManySendXecTxBuilderObj.transaction.tx.outs[0].value + mockOneToManySendXecTxBuilderObj.transaction.tx.outs[1].value + mockOneToManySendXecTxBuilderObj.transaction.tx.outs[2].value, ); const txFee = new BN(totalInputUtxoValue) .minus(satoshisToSendOutput) .minus( new BN( mockOneToManySendXecTxBuilderObj.transaction.tx.outs[3].value, ), ); // change value destinationAddressAndValueArray = validAddressArrayInput; const changeAddress = wallet.Path1899.cashAddress; const outputObj = generateTxOutput( isOneToMany, singleSendValue, satoshisToSendOutput, totalInputUtxoValue, null, destinationAddressAndValueArray, changeAddress, txFee, inputObj.txBuilder, ); const txBuilderResponse = signUtxosByAddress( mockSingleInputUtxo, wallet, outputObj, ); expect(txBuilderResponse.toString()).toStrictEqual( mockOneToManySendXecTxBuilderObj.toString(), ); }); it(`getChangeAddressFromInputUtxos() returns a correct change address from a valid inputUtxo`, () => { 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(inputUtxo, wallet); expect(changeAddress).toStrictEqual(inputUtxo[0].address); }); it(`getChangeAddressFromInputUtxos() returns a correct change address from a valid inputUtxo and accepts ecash: format`, () => { const { wallet } = sendBCHMock; const inputUtxo = [ { height: 669639, tx_hash: '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58', tx_pos: 0, value: 1000, txid: '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58', vout: 0, isValid: false, address: 'ecash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxav9up3h67g', wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH', }, ]; const changeAddress = getChangeAddressFromInputUtxos(inputUtxo, wallet); expect(changeAddress).toStrictEqual(inputUtxo[0].address); }); it(`getChangeAddressFromInputUtxos() throws error upon a malformed input utxo`, () => { 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(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 { 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(invalidInputUtxo, wallet); } catch (err) { thrownError = err; } expect(thrownError.message).toStrictEqual('Invalid input utxo'); }); it(`getChangeAddressFromInputUtxos() throws an error upon a null inputUtxos param`, () => { const { wallet } = sendBCHMock; const inputUtxo = null; let thrownError; try { getChangeAddressFromInputUtxos(inputUtxo, wallet); } catch (err) { thrownError = err; } expect(thrownError.message).toStrictEqual( 'Invalid getChangeAddressFromWallet input parameter', ); }); it(`sumOneToManyXec() correctly parses the value for a valid one to many send XEC transaction`, () => { const destinationAddressAndValueArray = [ 'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,1', 'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,2', 'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,3', ]; expect(sumOneToManyXec(destinationAddressAndValueArray)).toStrictEqual(6); }); it(`sumOneToManyXec() correctly parses the value for a valid one to many send XEC transaction with decimals`, () => { const destinationAddressAndValueArray = [ 'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,1.23', 'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,2.45', 'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,3.67', ]; expect(sumOneToManyXec(destinationAddressAndValueArray)).toStrictEqual( 7.35, ); }); it(`sumOneToManyXec() returns NaN for an address and value array that is partially typed or has invalid format`, () => { const destinationAddressAndValueArray = [ 'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,1', 'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,', ]; expect(sumOneToManyXec(destinationAddressAndValueArray)).toStrictEqual(NaN); }); it('generateAliasOpReturnScript() correctly generates OP_RETURN script for a valid alias registration for a p2pkh address', () => { const alias = 'test'; const address = 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035'; const { hash } = cashaddr.decode(address, true); // Manually build the expected outputScript const opReturn = '6a'; // push protocol identifier const prefixBytesHex = '04'; const aliasIdentifier = '2e786563'; // push alias tx version const aliasProtocolVersionNumberHex = '00'; // push the alias const aliasHexBytes = '04'; // alias.length in one byte of hex const aliasHex = Buffer.from(alias).toString('hex'); // push the address const aliasAddressBytesHex = '15'; // (1 + 20) in one byte of hex const p2pkhVersionByteHex = '00'; const aliasTxOpReturnOutputScript = [ opReturn, prefixBytesHex, aliasIdentifier, aliasProtocolVersionNumberHex, aliasHexBytes, aliasHex, aliasAddressBytesHex, p2pkhVersionByteHex, hash, ].join(''); // Calculate the expected outputScript with the tested function const aliasOutputScript = generateAliasOpReturnScript(alias, address); // aliasOutputScript.toString('hex') // 6a042e78656301000474657374150095e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d expect(aliasOutputScript.toString('hex')).toBe(aliasTxOpReturnOutputScript); }); it('generateAliasOpReturnScript() correctly generates OP_RETURN script for a valid alias registration for a p2sh address', () => { const alias = 'testtwo'; const address = 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07'; const { hash } = cashaddr.decode(address, true); // Manually build the expected outputScript const opReturn = '6a'; // push protocol identifier const prefixBytesHex = '04'; const aliasIdentifier = '2e786563'; // push alias tx version const aliasProtocolVersionNumberHex = '00'; // push the alias const aliasHexBytes = '07'; // alias.length in one byte of hex const aliasHex = Buffer.from(alias).toString('hex'); // push the address const aliasAddressBytesHex = '15'; // (1 + 20) in one byte of hex const p2shVersionByteHex = '08'; const aliasTxOpReturnOutputScript = [ opReturn, prefixBytesHex, aliasIdentifier, aliasProtocolVersionNumberHex, aliasHexBytes, aliasHex, aliasAddressBytesHex, p2shVersionByteHex, hash, ].join(''); // Calculate the expected outputScript with the tested function const aliasOutputScript = generateAliasOpReturnScript(alias, address); // aliasOutputScript.toString('hex') // 6a042e7865630100077465737474776f1508d37c4c809fe9840e7bfa77b86bd47163f6fb6c60 expect(aliasOutputScript.toString('hex')).toBe(aliasTxOpReturnOutputScript); }); it(`generateTokenTxInput() returns a valid object for a valid create token tx`, async () => { let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); const tokenId = '1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e'; const tokenInputObj = generateTokenTxInput( 'GENESIS', mockNonSlpUtxos, null, // no slpUtxos used for genesis tx tokenId, null, // no token send/burn amount for genesis tx appConfig.defaultFee, txBuilder, ); expect(tokenInputObj.inputXecUtxos).toStrictEqual([mockNonSlpUtxos[0]]); expect(tokenInputObj.txBuilder.toString()).toStrictEqual( mockCreateTokenTxBuilderObj.toString(), ); expect(tokenInputObj.remainderXecValue).toStrictEqual( new BN(698999), // tokenInputObj.inputXecUtxos - appConfig.etokenSats 546 - txFee ); }); it(`generateTokenTxInput() returns a valid object for a valid send token tx`, async () => { let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); const tokenId = mockSlpUtxos[0].tokenId; const tokenInputObj = generateTokenTxInput( 'SEND', mockNonSlpUtxos, mockSlpUtxos, tokenId, new BN(500), // sending 500 of these tokens appConfig.defaultFee, txBuilder, ); expect(tokenInputObj.inputTokenUtxos).toStrictEqual( [mockSlpUtxos[0]].concat([mockSlpUtxos[1]]), // mockSlpUtxos[0] 400 + mockSlpUtxos[1] 6500 ); expect(tokenInputObj.remainderTokenValue).toStrictEqual( new BN(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 () => { let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); const tokenId = mockSlpUtxos[0].tokenId; const tokenInputObj = generateTokenTxInput( 'BURN', mockNonSlpUtxos, mockSlpUtxos, tokenId, new BN(500), // burning 500 of these tokens appConfig.defaultFee, txBuilder, ); expect(tokenInputObj.inputTokenUtxos).toStrictEqual( [mockSlpUtxos[0]].concat([mockSlpUtxos[1]]), // mockSlpUtxos[0] 400 + mockSlpUtxos[1] 6500 ); expect(tokenInputObj.remainderTokenValue).toStrictEqual( new BN(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 () => { let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); const { configObj, wallet } = createTokenMock; const tokenSenderCashAddress = wallet.Path1899.cashAddress; const tokenOutputObj = generateTokenTxOutput( txBuilder, 'GENESIS', tokenSenderCashAddress, null, // optional, for SEND or BURN amount new BN(500), // remainder XEC value configObj, ); expect(tokenOutputObj.toString()).toStrictEqual( mockCreateTokenOutputsTxBuilderObj.toString(), ); }); it(`generateTokenTxOutput() returns a valid object for a valid send token tx`, async () => { let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); const { wallet } = createTokenMock; const tokenSenderCashAddress = wallet.Path1899.cashAddress; const tokenRecipientTokenAddress = wallet.Path1899.cashAddress; const tokenOutputObj = generateTokenTxOutput( txBuilder, 'SEND', tokenSenderCashAddress, mockSlpUtxos, new BN(500), // remainder XEC value null, // only for genesis tx tokenRecipientTokenAddress, // recipient token address new BN(50), ); expect(tokenOutputObj.toString()).toStrictEqual( mockSendTokenOutputsTxBuilderObj.toString(), ); }); it(`generateTokenTxOutput() returns a valid object for a valid burn token tx`, async () => { let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); const { wallet } = createTokenMock; const tokenSenderCashAddress = wallet.Path1899.cashAddress; const tokenOutputObj = generateTokenTxOutput( txBuilder, 'BURN', tokenSenderCashAddress, mockSlpUtxos, new BN(500), // remainder XEC value null, // only for genesis tx null, // no token recipients for burn tx new BN(50), ); expect(tokenOutputObj.toString()).toStrictEqual( mockBurnTokenOutputsTxBuilderObj.toString(), ); }); it(`generateTxInput() returns an input object for a valid one to one XEC tx`, async () => { const isOneToMany = false; const utxos = mockNonSlpUtxos; let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); const destinationAddressAndValueArray = null; const satoshisToSend = new BN(2184); const feeInSatsPerByte = appConfig.defaultFee; const inputObj = generateTxInput( isOneToMany, utxos, txBuilder, destinationAddressAndValueArray, satoshisToSend, feeInSatsPerByte, ); expect(inputObj.txBuilder).not.toStrictEqual(null); expect(inputObj.totalInputUtxoValue).toStrictEqual(new BN(700000)); expect(inputObj.txFee).toStrictEqual(455); expect(inputObj.inputUtxos.length).not.toStrictEqual(0); }); it(`generateTxInput() returns an input object for a valid one to many XEC tx`, async () => { const isOneToMany = true; const utxos = mockNonSlpUtxos; let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); const destinationAddressAndValueArray = [ 'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,3000', 'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,3000', 'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,3000', ]; const satoshisToSend = new BN(900000); const feeInSatsPerByte = appConfig.defaultFee; const inputObj = generateTxInput( isOneToMany, utxos, txBuilder, destinationAddressAndValueArray, satoshisToSend, feeInSatsPerByte, ); expect(inputObj.txBuilder).not.toStrictEqual(null); expect(inputObj.totalInputUtxoValue).toStrictEqual(new BN(1400000)); expect(inputObj.txFee).toStrictEqual(889); expect(inputObj.inputUtxos.length).not.toStrictEqual(0); }); it(`generateTxInput() throws error for a one to many XEC tx with invalid destinationAddressAndValueArray input`, async () => { const isOneToMany = true; const utxos = mockNonSlpUtxos; let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); const destinationAddressAndValueArray = null; // invalid since isOneToMany is true const satoshisToSend = new BN(900000); const feeInSatsPerByte = appConfig.defaultFee; let thrownError; try { generateTxInput( 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 isOneToMany = true; const utxos = null; let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); const destinationAddressAndValueArray = [ 'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,3000', 'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,3000', 'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,3000', ]; const satoshisToSend = new BN(900000); const feeInSatsPerByte = appConfig.defaultFee; let thrownError; try { generateTxInput( 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 { 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 BN(singleSendValue)); // for unit test purposes, calculate fee by subtracting satoshisToSend from totalInputUtxoValue // no change output to be subtracted in this tx const txFee = new BN(totalInputUtxoValue).minus(new BN(satoshisToSend)); const destinationAddressAndValueArray = null; let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); const changeAddress = wallet.Path1899.cashAddress; const outputObj = generateTxOutput( 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 { 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 BN( 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 BN(totalInputUtxoValue) .minus(satoshisToSend) .minus( new BN( mockOneToManySendXecTxBuilderObj.transaction.tx.outs[3].value, ), ); // change value const destinationAddressAndValueArray = validAddressArrayInput; let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); const changeAddress = wallet.Path1899.cashAddress; const outputObj = generateTxOutput( 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 { 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 BN(singleSendValue)); // for unit test purposes, calculate fee by subtracting satoshisToSend from totalInputUtxoValue // no change output to be subtracted in this tx const txFee = new BN(totalInputUtxoValue).minus(satoshisToSend); const destinationAddressAndValueArray = null; let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); const changeAddress = wallet.Path1899.cashAddress; let thrownError; try { generateTxOutput( 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 { 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 BN( 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 BN(totalInputUtxoValue) .minus(satoshisToSend) .minus( new BN( mockOneToManySendXecTxBuilderObj.transaction.tx.outs[3].value, ), ); // change value const destinationAddressAndValueArray = null; // invalid as this is mandatory when isOneToMany is true let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); const changeAddress = wallet.Path1899.cashAddress; let thrownError; try { generateTxOutput( 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 let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); // Use legacy sequencing for legacy unit tests txBuilder.DEFAULT_SEQUENCE = 0xffffffff; 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( cashaddr.toLegacy(outputAddressAndValue[0]), // address parseInt(fromXecToSatoshis(new BN(outputAddressAndValue[1]))), // value ); const rawTxHex = signAndBuildTx(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 let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); // Use legacy sequencing for legacy unit tests txBuilder.DEFAULT_SEQUENCE = 0xffffffff; 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( cashaddr.toLegacy(outputAddressAndValue[0]), // address parseInt(fromXecToSatoshis(new BN(outputAddressAndValue[1]))), // value ); } const rawTxHex = signAndBuildTx(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 let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); // Use legacy sequencing for legacy unit tests txBuilder.DEFAULT_SEQUENCE = 0xffffffff; 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( cashaddr.toLegacy(outputAddressAndValue[0]), // address parseInt(fromXecToSatoshis(new BN(outputAddressAndValue[1]))), // value ); const rawTxHex = signAndBuildTx(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 let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); // Use legacy sequencing for legacy unit tests txBuilder.DEFAULT_SEQUENCE = 0xffffffff; 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( cashaddr.toLegacy(outputAddressAndValue[0]), // address parseInt(fromXecToSatoshis(new BN(outputAddressAndValue[1]))), // value ); } const rawTxHex = signAndBuildTx(mockMultipleInputUtxos, txBuilder, wallet); expect(rawTxHex).toStrictEqual( '0200000003582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006a47304402203de4e6a512a6bec1d378b6444008484e1be5a0c621dc4b201d67addefffe864602202daf82e76b7594fe1ab54a49380c6b1226ab65551ae6ab9164216b66266f34a1412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff7313e804af08113dfa290515390a8ec3ac01448118f2eb556ee168a96ee6acdd000000006a473044022029f5fcbc9356beb9eae6b9ff9a479e8c8331b95406b6be456fccf9d90f148ea1022028f4e7fa7234f9429535360c8f5dad303e2c5044431615997861b10f26fa8a88412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff960dd2f0c47e8a3cf1486b046d879f45a047da3b51aedfb5594138ac857214f1000000006a473044022049a67738d99006b3523cff818f3626104cf5106bd463be70d22ad179a8cb403b022025829baf67f964202ea77ea7462a5447e32415e7293cdee382ea7ae9374364e8412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff0326020000000000001976a914f627e51001a51a1a92d8927808701373cf29267f88ac26020000000000001976a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac26020000000000001976a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac00000000', ); }); it(`signAndBuildTx() throws error on an empty inputUtxo param`, () => { // txbuilder output params let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); const { wallet } = sendBCHMock; let thrownError; try { signAndBuildTx([], 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 let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork( utxolib.networks.ecash, ); const inputUtxo = null; // invalid input param const { wallet } = sendBCHMock; let thrownError; try { signAndBuildTx(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 BN(0.01)); }); it(`Correctly converts largest base unit to smallest decimal for cashDecimals = 2`, () => { expect(fromSatoshisToXec(1000000012345678, 2)).toStrictEqual( new BN(10000000123456.78), ); }); it(`Correctly converts smallest base unit to smallest decimal for cashDecimals = 8`, () => { expect(fromSatoshisToXec(1, 8)).toStrictEqual(new BN(0.00000001)); }); it(`Correctly converts largest base unit to smallest decimal for cashDecimals = 8`, () => { expect(fromSatoshisToXec(1000000012345678, 8)).toStrictEqual( new BN(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(`loadStoredWallet accepts undefined wallet state as input and outputs a zero balance wallet state`, () => { expect(loadStoredWallet(undefined)).toStrictEqual({ balances: { totalBalanceInSatoshis: '0', totalBalance: '0', }, }); }); it(`Correctly determines a wallet's balance from its set of non-eToken utxos (nonSlpUtxos)`, () => { expect( getWalletBalanceFromUtxos( validStoredWalletAfter20221123Streamline.state.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.nonSlpUtxos), ).toStrictEqual(utxosLoadedFromCache.balances); }); it(`Recognizes a stored wallet as valid if it has all required fields prior to 20221123 updated format`, () => { expect(isValidStoredWallet(validStoredWallet)).toBe(true); }); it(`Recognizes a stored wallet as valid if it has all required fields in 20221123 updated format`, () => { expect( isValidStoredWallet(validStoredWalletAfter20221123Streamline), ).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(`Recognizes a stored wallet as invalid if it's missing the Path1899 and mnemonic keys`, () => { expect( isValidStoredWallet(invalidStoredWalletMissingPath1899AndMnemonic), ).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 appConfig.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(`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); + expect(isLegacyMigrationRequired(notLegacyWalletWithXecPrefixes)).toBe( + false, + ); + }); + it(`Recognizes a non-eCash prefixed Path245 address and requires migration`, () => { + expect( + isLegacyMigrationRequired(notLegacyWalletWithPath245OnBchPrefix), + ).toBe(true); + }); + it(`Recognizes a non-eCash prefixed Path1899 address and requires migration`, () => { + expect( + isLegacyMigrationRequired(notLegacyWalletWithPath1899OnBchPrefix), + ).toBe(true); + }); + it(`Recognizes a non-eCash prefixed Path145 address and requires migration`, () => { + expect( + isLegacyMigrationRequired(notLegacyWalletWithPath145OnBchPrefix), + ).toBe(true); }); test('toHash160() converts a valid bitcoincash: prefix address to a hash160', () => { const result = toHash160( 'bitcoincash:qq9h6d0a5q65fgywv4ry64x04ep906mdku7ymranw3', ); expect(result).toStrictEqual( '0b7d35fda03544a08e65464d54cfae4257eb6db7', ); }); test('toHash160 throws error if input address is an invalid bitcoincash: address', () => { const address = 'bitcoincash:qqd3qnINVALIDDDDDDDDDza25m'; let errorThrown; try { toHash160(address); } catch (err) { errorThrown = err.message; } expect(errorThrown).toStrictEqual('Invalid address: ' + address + '.'); }); test('toHash160() converts a valid ecash: prefix address to a hash160', () => { const result = toHash160( 'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx', ); expect(result).toStrictEqual( '0b7d35fda03544a08e65464d54cfae4257eb6db7', ); }); test('toHash160 throws error if input address is an invalid ecash address', () => { const address = 'ecash:qqd3qn4zINVALIDDDDDtfza25m'; let errorThrown; try { toHash160(address); } catch (err) { errorThrown = err.message; } expect(errorThrown).toStrictEqual('Invalid address: ' + address + '.'); }); test('toHash160() converts a valid etoken: address to a hash160', () => { const result = toHash160( 'etoken:qq9h6d0a5q65fgywv4ry64x04ep906mdkufhx2swv3', ); expect(result).toStrictEqual( '0b7d35fda03544a08e65464d54cfae4257eb6db7', ); }); test('toHash160 throws error if input address is an invalid etoken: address', () => { const address = 'etoken:qq9h6d0a5INVALIDDDDDDx2swv3'; let errorThrown; try { toHash160(address); } catch (err) { errorThrown = err.message; } expect(errorThrown).toStrictEqual('Invalid address: ' + address + '.'); }); test('toHash160() converts a valid simpleledger: address to a hash160', () => { const result = toHash160( 'simpleledger:qq9h6d0a5q65fgywv4ry64x04ep906mdkujlscgns0', ); expect(result).toStrictEqual( '0b7d35fda03544a08e65464d54cfae4257eb6db7', ); }); test('toHash160 throws error if input address is an invalid simpleledger: address', () => { const address = 'simpleledger:qq9h6d0a5qINVALIDDDjlscgns0'; let errorThrown; try { toHash160(address); } catch (err) { errorThrown = err.message; } expect(errorThrown).toStrictEqual('Invalid address: ' + 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 === opreturnConfig.appPrefixesHex.airdrop, ), ).toStrictEqual(opreturnConfig.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); }); it(`getCashtabByteCount for 2 inputs, 2 outputs returns the same value as BCH.BitcoinCash.getByteCount( { P2PKH: utxos.length }, { P2PKH: p2pkhOutputNumber }, );`, () => { expect(getCashtabByteCount(2, 2)).toBe(374); }); it(`getCashtabByteCount for 1 input, 2 outputs returns the same value as BCH.BitcoinCash.getByteCount( { P2PKH: utxos.length }, { P2PKH: p2pkhOutputNumber }, );`, () => { expect(getCashtabByteCount(1, 2)).toBe(226); }); it(`getCashtabByteCount for 173 input, 1 outputs returns the same value as BCH.BitcoinCash.getByteCount( { P2PKH: utxos.length }, { P2PKH: p2pkhOutputNumber }, );`, () => { expect(getCashtabByteCount(173, 1)).toBe(25648); }); it(`getCashtabByteCount for 1 input, 2000 outputs returns the same value as BCH.BitcoinCash.getByteCount( { P2PKH: utxos.length }, { P2PKH: p2pkhOutputNumber }, );`, () => { expect(getCashtabByteCount(1, 2000)).toBe(68158); }); it('calculates fee correctly for 2 P2PKH outputs', () => { const utxosMock = [{}, {}]; expect(calcFee(utxosMock, 2, appConfig.defaultFee)).toBe(752); }); it(`Converts a hash160 to an ecash address`, () => { expect( hash160ToAddress('76458db0ed96fe9863fc1ccec9fa2cfab884b0f6'), ).toBe('ecash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj'); }); it(`outputScriptToAddress determines P2PKH address type from output script and returns the ecash address`, () => { expect( outputScriptToAddress( '76a914da45fd71b76e34c88e97ccbebb454d7cd395e52c88ac', ), ).toBe('ecash:qrdytlt3kahrfjywjlxtaw69f47d89099s393kne5c'); }); it(`outputScriptToAddress determines P2SH address type from output script and returns the ecash address`, () => { expect( outputScriptToAddress( 'a914c5e60aad8d98f298a76434750630dc1b46a2382187', ), ).toBe('ecash:prz7vz4d3kv09x98vs682p3smsd5dg3cyykjye6grt'); }); it(`outputScriptToAddress throws correct error for an output script that does not parse as P2PKH or P2SH`, () => { let thrownError; try { outputScriptToAddress('notAnOutputScript'); } catch (err) { thrownError = err; } expect(thrownError.message).toBe('Unrecognized outputScript format'); }); it(`outputScriptToAddress throws correct error for an output script that for some reason is bracketed by P2PKH markers but is not a valid hash160`, () => { let thrownError; try { outputScriptToAddress( '76a914da45fd71b76eeeeeeeee34c88e97ccbebb454d7cd395e52c88ac', ); } catch (err) { thrownError = err; } expect(thrownError.message).toBe('Parsed hash160 is incorrect length'); }); it(`toSatoshis returns expected integers for all possible decimal places at extreme limit of XEC supply`, () => { const xecMaxSupply = 21000000000000; const xecMaxSupplyLessOne = xecMaxSupply - 1; for (let i = 1; i < 100; i += 1) { const thisDecimal = parseFloat((i / 100).toFixed(2)); const testValue = xecMaxSupplyLessOne + thisDecimal; const result = toSatoshis(testValue); // Confirm you get an integer expect(Number.isInteger(result)).toBe(true); // Confirm you aren't rounding to the wrong decimal expect(result.toString().slice(-2)).toBe( thisDecimal.toFixed(2).slice(-2), ); } }); }); diff --git a/cashtab/src/utils/cashMethods.js b/cashtab/src/utils/cashMethods.js index 6d953e6d1..439fcb6bd 100644 --- a/cashtab/src/utils/cashMethods.js +++ b/cashtab/src/utils/cashMethods.js @@ -1,1102 +1,1105 @@ import { isValidXecAddress, isValidEtokenAddress, isValidContactList, isValidBchAddress, } from 'utils/validation'; import { BN, TokenType1 } from 'slp-mdm'; import cashaddr from 'ecashaddrjs'; import bs58 from 'bs58'; import * as utxolib from '@bitgo/utxo-lib'; import { opReturn as opreturnConfig } from 'config/opreturn'; import appConfig from 'config/app'; // function is based on BCH-JS' generateBurnOpReturn() however it's been trimmed down for Cashtab use // Reference: https://github.com/Permissionless-Software-Foundation/bch-js/blob/62e56c832b35731880fe448269818b853c76dd80/src/slp/tokentype1.js#L217 export const generateBurnOpReturn = (tokenUtxos, burnQty) => { try { if (!tokenUtxos || !burnQty) { throw new Error('Invalid burn token parameter'); } // sendToken component already prevents burning of a value greater than the token utxo total held by the wallet const tokenId = tokenUtxos[0].tokenId; const decimals = tokenUtxos[0].decimals; // account for token decimals const finalBurnTokenQty = new BN(burnQty).times(10 ** decimals); // Calculate the total amount of tokens owned by the wallet. const totalTokens = tokenUtxos.reduce( (tot, txo) => tot.plus(new BN(txo.tokenQty).times(10 ** decimals)), new BN(0), ); // calculate the token change const tokenChange = totalTokens.minus(finalBurnTokenQty); const tokenChangeStr = tokenChange.toString(); // Generate the burn OP_RETURN as a Buffer // No need for separate .send() calls for change and non-change burns as // nil change values do not generate token outputs as the full balance is burnt const script = TokenType1.send(tokenId, [new BN(tokenChangeStr)]); return script; } catch (err) { console.log('Error in generateBurnOpReturn(): ' + err); throw err; } }; // Function originally based on BCH-JS' generateSendOpReturn function however trimmed down for Cashtab // Reference: https://github.com/Permissionless-Software-Foundation/bch-js/blob/62e56c832b35731880fe448269818b853c76dd80/src/slp/tokentype1.js#L95 export const generateSendOpReturn = (tokenUtxos, sendQty) => { try { if (!tokenUtxos || !sendQty) { throw new Error('Invalid send token parameter'); } const tokenId = tokenUtxos[0].tokenId; const decimals = tokenUtxos[0].decimals; // account for token decimals const finalSendTokenQty = new BN(sendQty).times(10 ** decimals); const finalSendTokenQtyStr = finalSendTokenQty.toString(); // Calculate the total amount of tokens owned by the wallet. const totalTokens = tokenUtxos.reduce( (tot, txo) => tot.plus(new BN(txo.tokenQty).times(10 ** decimals)), new BN(0), ); // calculate token change const tokenChange = totalTokens.minus(finalSendTokenQty); const tokenChangeStr = tokenChange.toString(); // When token change output is required let script, outputs; if (tokenChange > 0) { outputs = 2; // Generate the OP_RETURN as a Buffer. script = TokenType1.send(tokenId, [ new BN(finalSendTokenQtyStr), new BN(tokenChangeStr), ]); } else { // no token change needed outputs = 1; // Generate the OP_RETURN as a Buffer. script = TokenType1.send(tokenId, [new BN(finalSendTokenQtyStr)]); } return { script, outputs }; } catch (err) { console.log('Error in generateSendOpReturn(): ' + err); throw err; } }; // function is based on BCH-JS' generateGenesisOpReturn() however it's been trimmed down for Cashtab use // Reference: https://github.com/Permissionless-Software-Foundation/bch-js/blob/62e56c832b35731880fe448269818b853c76dd80/src/slp/tokentype1.js#L286 export const generateGenesisOpReturn = configObj => { try { if (!configObj) { throw new Error('Invalid token configuration'); } // adjust initial quantity for token decimals const initialQty = new BN(configObj.initialQty) .times(10 ** configObj.decimals) .toString(); const script = TokenType1.genesis( configObj.ticker, configObj.name, configObj.documentUrl, configObj.documentHash, configObj.decimals, configObj.mintBatonVout, new BN(initialQty), ); return script; } catch (err) { console.log('Error in generateGenesisOpReturn(): ' + err); throw err; } }; export const signUtxosByAddress = (inputUtxos, wallet, txBuilder) => { for (let i = 0; i < inputUtxos.length; i++) { const utxo = inputUtxos[i]; const accounts = [wallet.Path245, wallet.Path145, wallet.Path1899]; const wif = accounts .filter(acc => acc.cashAddress === utxo.address) .pop().fundingWif; const utxoECPair = utxolib.ECPair.fromWIF(wif, utxolib.networks.ecash); // Specify hash type // This should be handled at the utxo-lib level, pending latest published version const hashTypes = { SIGHASH_ALL: 0x01, SIGHASH_FORKID: 0x40, }; txBuilder.sign( i, // vin utxoECPair, // keyPair undefined, // redeemScript hashTypes.SIGHASH_ALL | hashTypes.SIGHASH_FORKID, // hashType parseInt(utxo.value), // value ); } return txBuilder; }; export const getCashtabByteCount = (p2pkhInputCount, p2pkhOutputCount) => { // Simplifying bch-js function for P2PKH txs only, as this is all Cashtab supports for now // https://github.com/Permissionless-Software-Foundation/bch-js/blob/master/src/bitcoincash.js#L408 // The below magic numbers refer to: // const types = { // inputs: { // 'P2PKH': 148 * 4, // }, // outputs: { // P2PKH: 34 * 4, // }, // }; const inputCount = new BN(p2pkhInputCount); const outputCount = new BN(p2pkhOutputCount); const inputWeight = new BN(148 * 4); const outputWeight = new BN(34 * 4); const nonSegwitWeightConstant = new BN(10 * 4); let totalWeight = new BN(0); totalWeight = totalWeight .plus(inputCount.times(inputWeight)) .plus(outputCount.times(outputWeight)) .plus(nonSegwitWeightConstant); const byteCount = totalWeight.div(4).integerValue(BN.ROUND_CEIL); return Number(byteCount); }; export const calcFee = ( utxos, p2pkhOutputNumber = 2, satoshisPerByte = appConfig.defaultFee, opReturnByteCount = 0, ) => { const byteCount = getCashtabByteCount(utxos.length, p2pkhOutputNumber); const txFee = Math.ceil(satoshisPerByte * (byteCount + opReturnByteCount)); return txFee; }; export const generateTokenTxOutput = ( txBuilder, tokenAction, legacyCashOriginAddress, tokenUtxosBeingSpent = [], // optional - send or burn tx only remainderXecValue = new BN(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 (!tokenAction || !legacyCashOriginAddress || !txBuilder) { throw new Error('Invalid token tx output parameter'); } let script, opReturnObj, destinationAddress; switch (tokenAction) { case 'GENESIS': script = generateGenesisOpReturn(tokenConfigObj); destinationAddress = legacyCashOriginAddress; break; case 'SEND': opReturnObj = generateSendOpReturn( tokenUtxosBeingSpent, tokenAmount.toString(), ); script = opReturnObj.script; destinationAddress = tokenRecipientAddress; break; case 'BURN': script = generateBurnOpReturn( tokenUtxosBeingSpent, tokenAmount, ); destinationAddress = 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( cashaddr.toLegacy(destinationAddress), parseInt(appConfig.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( cashaddr.toLegacy(tokenUtxosBeingSpent[0].address), // etoken address parseInt(appConfig.etokenSats), ); } // Send xec change to own address if (remainderXecValue.gte(new BN(appConfig.dustSats))) { txBuilder.addOutput( cashaddr.toLegacy(legacyCashOriginAddress), parseInt(remainderXecValue), ); } } catch (err) { console.log(`generateTokenTxOutput() error: ` + err); throw err; } return txBuilder; }; export const generateTxInput = ( isOneToMany, utxos, txBuilder, destinationAddressAndValueArray, satoshisToSend, feeInSatsPerByte, opReturnByteCount, ) => { let txInputObj = {}; const inputUtxos = []; let txFee = 0; let totalInputUtxoValue = new BN(0); try { if ( (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( inputUtxos, txOutputs, feeInSatsPerByte, opReturnByteCount, ); 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 = ( tokenAction, // GENESIS, SEND or BURN totalXecUtxos, totalTokenUtxos, tokenId, tokenAmount, // optional - only for sending or burning feeInSatsPerByte, txBuilder, ) => { let totalXecInputUtxoValue = new BN(0); let remainderXecValue = new BN(0); let remainderTokenValue = new BN(0); let totalXecInputUtxos = []; let txFee = 0; let tokenUtxosBeingSpent = []; try { if ( !tokenAction || !totalXecUtxos || (tokenAction !== 'GENESIS' && !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 BN(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(totalXecInputUtxos, txOutputs, feeInSatsPerByte); remainderXecValue = tokenAction === 'GENESIS' ? totalXecInputUtxoValue .minus(new BN(appConfig.etokenSats)) .minus(new BN(txFee)) : totalXecInputUtxoValue .minus(new BN(appConfig.etokenSats * 2)) // one for token send/burn output, one for token change .minus(new BN(txFee)); if (remainderXecValue.gte(0)) { break; } } if (remainderXecValue.lt(0)) { throw new Error(`Insufficient funds`); } let filteredTokenInputUtxos = []; let finalTokenAmountSpent = new BN(0); let tokenAmountBeingSpent = new BN(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 BN(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 BN(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 = (inputUtxos, wallet) => { if (!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; if ( !isValidXecAddress(changeAddress) && !isValidBchAddress(changeAddress) ) { throw new Error('Invalid change address'); } } catch (err) { throw new Error('Invalid input utxo'); } return changeAddress; }; /** * Get the total XEC amount sent in a one-to-many XEC tx * @param {array} destinationAddressAndValueArray * Array constructed by user input of addresses and values * e.g. [ * "
, ", * "
, " * ] * @returns {number} total value of XEC */ export const sumOneToManyXec = destinationAddressAndValueArray => { return destinationAddressAndValueArray.reduce((prev, curr) => { return parseFloat(prev) + parseFloat(curr.split(',')[1]); }, 0); }; /** * Return an integer that is the given amountXEC in satoshis * @param {Number} amountXec * @returns {Integer} satoshis */ export const toSatoshis = amountXec => { const SATOSHIS_PER_XEC = 100; // Math.round returns the nearest integer value // e.g. in JS, 151.52 * 100 = 15152.000000000002 // We need to return 15152 return Math.round(SATOSHIS_PER_XEC * amountXec); }; /* * Generates an OP_RETURN script for a version 0 alias registration tx * * Returns the final encoded script object ready to be added as a transaction output */ export const generateAliasOpReturnScript = (alias, address) => { // Note: utxolib.script.compile(script) will add pushdata bytes for each buffer // utxolib.script.compile(script) will not add pushdata bytes for raw data // Initialize script array with OP_RETURN byte (6a) as rawdata (i.e. you want compiled result of 6a, not 016a) let script = [opreturnConfig.opReturnPrefixDec]; // Push alias protocol identifier script.push( Buffer.from(opreturnConfig.appPrefixesHex.aliasRegistration, 'hex'), // '.xec' ); // Push alias protocol tx version to stack // Per spec, push this as OP_0 script.push(0); // Push alias to the stack script.push(Buffer.from(alias, 'utf8')); // Get the type and hash of the address in string format const { type, hash } = cashaddr.decode(address, true); // Determine address type and corresponding address version byte let addressVersionByte; // Version bytes per cashaddr spec,https://github.com/bitcoincashorg/bitcoincash.org/blob/master/spec/cashaddr.md if (type === 'p2pkh') { addressVersionByte = '00'; // one byte 0 in hex } else if (type === 'p2sh') { addressVersionByte = '08'; // one byte 8 in hex } else { throw new Error('Unsupported address type'); } // Push and script.push(Buffer.from(`${addressVersionByte}${hash}`, 'hex')); return utxolib.script.compile(script); }; export const generateTxOutput = ( isOneToMany, singleSendValue, satoshisToSend, totalInputUtxoValue, destinationAddress, destinationAddressAndValueArray, changeAddress, txFee, txBuilder, ) => { try { if ( (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 BN(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] .trim(); let outputValue = new BN( destinationAddressAndValueArray[i].split(',')[1], ); txBuilder.addOutput( cashaddr.toLegacy(outputAddress), parseInt(fromXecToSatoshis(outputValue)), ); } } else { // for one to one mode, add output w/ single address and amount to send txBuilder.addOutput( cashaddr.toLegacy(destinationAddress), parseInt(fromXecToSatoshis(singleSendValue)), ); } // if a remainder exists, return to change address as the final output if (remainder.gte(new BN(appConfig.dustSats))) { txBuilder.addOutput( cashaddr.toLegacy(changeAddress), parseInt(remainder), ); } } catch (err) { console.log('Error in generateTxOutput(): ' + err); throw err; } return txBuilder; }; export const signAndBuildTx = (inputUtxos, txBuilder, wallet) => { if (!inputUtxos || inputUtxos.length === 0 || !txBuilder || !wallet) { throw new Error('Invalid buildTx parameter'); } // Sign each XEC UTXO being consumed and refresh transactionBuilder txBuilder = signUtxosByAddress(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) !== opreturnConfig.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 === opreturnConfig.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 === opreturnConfig.appPrefixesHex.eToken) { // add the extracted eToken prefix to array then exit loop resultArray[i] = opreturnConfig.appPrefixesHex.eToken; break; } else if ( i === 0 && message === opreturnConfig.appPrefixesHex.cashtab ) { // add the extracted Cashtab prefix to array resultArray[i] = opreturnConfig.appPrefixesHex.cashtab; } else if ( i === 0 && message === opreturnConfig.appPrefixesHex.cashtabEncrypted ) { // add the Cashtab encryption prefix to array resultArray[i] = opreturnConfig.appPrefixesHex.cashtabEncrypted; } else if ( i === 0 && message === opreturnConfig.appPrefixesHex.airdrop ) { // add the airdrop prefix to array resultArray[i] = opreturnConfig.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 = appConfig.cashDecimals, ) => { // Input 0.00000546 BCH // Output 5.46 XEC or 0.00000546 BCH, depending on appConfig.cashDecimals const amountBig = new BN(amount); const conversionFactor = new BN(10 ** (8 - cashDecimals)); const amountSmallestDenomination = amountBig .times(conversionFactor) .toNumber(); return amountSmallestDenomination; }; export const fromSatoshisToXec = ( amount, cashDecimals = appConfig.cashDecimals, ) => { const amountBig = new BN(amount); const multiplier = new BN(10 ** (-1 * cashDecimals)); const amountInBaseUnits = amountBig.times(multiplier); return amountInBaseUnits; }; export const fromXecToSatoshis = ( sendAmount, cashDecimals = appConfig.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 = BN.isBigNumber(sendAmount) && sendAmount.dp() <= cashDecimals; if (!isValidSendAmount) { return false; } const conversionFactor = new BN(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 = typeof walletStateFromStorage !== 'undefined' ? walletStateFromStorage : {}; const keysInLiveWalletState = Object.keys(liveWalletState); // Newly created wallets may not have a state field // You only need to do this if you are loading a wallet // that hasn't yet saved tokens[i].balance as a string // instead of a BigNumber if (keysInLiveWalletState.includes('tokens')) { const { tokens } = liveWalletState; if ( tokens.length > 0 && tokens[0] && tokens[0].balance && typeof tokens[0].balance !== 'string' ) { for (let i = 0; i < tokens.length; i += 1) { const thisTokenBalance = tokens[i].balance; thisTokenBalance._isBigNumber = true; tokens[i].balance = new BN(thisTokenBalance); } } } // Also confirm balance is correct // Necessary step in case appConfig.decimals changed since last startup let nonSlpUtxosToParseForBalance; let balancesRebased; if (keysInLiveWalletState.length !== 0) { if (keysInLiveWalletState.includes('slpBalancesAndUtxos')) { // If this wallet still includes the wallet.state.slpBalancesAndUtxos field nonSlpUtxosToParseForBalance = liveWalletState.slpBalancesAndUtxos.nonSlpUtxos; } else { nonSlpUtxosToParseForBalance = liveWalletState.nonSlpUtxos; } balancesRebased = getWalletBalanceFromUtxos( nonSlpUtxosToParseForBalance, ); } else { balancesRebased = { totalBalanceInSatoshis: '0', totalBalance: '0', }; } liveWalletState.balances = balancesRebased; return liveWalletState; }; export const getWalletBalanceFromUtxos = nonSlpUtxos => { const totalBalanceInSatoshis = nonSlpUtxos.reduce( (previousBalance, utxo) => previousBalance.plus(new BN(utxo.value)), new BN(0), ); return { totalBalanceInSatoshis: totalBalanceInSatoshis.toString(), totalBalance: fromSatoshisToXec(totalBalanceInSatoshis).toString(), }; }; export const isValidStoredWallet = walletStateFromStorage => { return ( typeof walletStateFromStorage === 'object' && 'state' in walletStateFromStorage && 'mnemonic' in walletStateFromStorage && 'name' in walletStateFromStorage && 'Path245' in walletStateFromStorage && 'Path145' in walletStateFromStorage && 'Path1899' in walletStateFromStorage && typeof walletStateFromStorage.state === 'object' && 'balances' in walletStateFromStorage.state && !('hydratedUtxoDetails' in walletStateFromStorage.state) && ('slpBalancesAndUtxos' in walletStateFromStorage.state || ('slpUtxos' in walletStateFromStorage.state && 'nonSlpUtxos' in walletStateFromStorage.state)) && 'tokens' in walletStateFromStorage.state ); }; export const getWalletState = wallet => { if (!wallet || !wallet.state) { return { balances: { totalBalance: 0, totalBalanceInSatoshis: 0 }, hydratedUtxoDetails: {}, tokens: [], slpUtxos: [], nonSlpUtxos: [], 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; } // converts ecash, etoken, bitcoincash and simpleledger addresses to hash160 export function toHash160(addr) { try { // decode address hash const { hash } = cashaddr.decode(addr); // encode the address hash to legacy format (bitcoin) const legacyAdress = bs58.encode(hash); // convert legacy to hash160 const addrHash160 = Buffer.from(bs58.decode(legacyAdress)).toString( 'hex', ); return addrHash160; } catch (err) { console.log('Error converting address to hash160'); 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 + !wallet.Path245.hash160 || + !wallet.Path1899.cashAddress.startsWith('ecash:') || + !wallet.Path145.cashAddress.startsWith('ecash:') || + !wallet.Path245.cashAddress.startsWith('ecash:') ) { 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 ); }; export const hash160ToAddress = hash160 => { const buffer = Buffer.from(hash160, 'hex'); // Because ecashaddrjs only accepts Uint8Array as input type, convert const hash160ArrayBuffer = new ArrayBuffer(buffer.length); const hash160Uint8Array = new Uint8Array(hash160ArrayBuffer); for (let i = 0; i < hash160Uint8Array.length; i += 1) { hash160Uint8Array[i] = buffer[i]; } // Encode ecash: address const ecashAddr = cashaddr.encode('ecash', 'P2PKH', hash160Uint8Array); return ecashAddr; }; export const outputScriptToAddress = outputScript => { // returns P2SH or P2PKH address // P2PKH addresses are in outputScript of type 76a914...88ac // P2SH addresses are in outputScript of type a914...87 // Return false if cannot determine P2PKH or P2SH address const typeTestSlice = outputScript.slice(0, 4); let addressType; let hash160; switch (typeTestSlice) { case '76a9': addressType = 'P2PKH'; hash160 = outputScript.substring( outputScript.indexOf('76a914') + '76a914'.length, outputScript.lastIndexOf('88ac'), ); break; case 'a914': addressType = 'P2SH'; hash160 = outputScript.substring( outputScript.indexOf('a914') + 'a914'.length, outputScript.lastIndexOf('87'), ); break; default: throw new Error('Unrecognized outputScript format'); } // Test hash160 for correct length if (hash160.length !== 40) { throw new Error('Parsed hash160 is incorrect length'); } const buffer = Buffer.from(hash160, 'hex'); // Because ecashaddrjs only accepts Uint8Array as input type, convert const hash160ArrayBuffer = new ArrayBuffer(buffer.length); const hash160Uint8Array = new Uint8Array(hash160ArrayBuffer); for (let i = 0; i < hash160Uint8Array.length; i += 1) { hash160Uint8Array[i] = buffer[i]; } // Encode ecash: address const ecashAddress = cashaddr.encode( 'ecash', addressType, hash160Uint8Array, ); return ecashAddress; };