@@ -1585,6 +1629,17 @@
}
}
>
+
diff --git a/web/cashtab/src/hooks/useBCH.js b/web/cashtab/src/hooks/useBCH.js
--- a/web/cashtab/src/hooks/useBCH.js
+++ b/web/cashtab/src/hooks/useBCH.js
@@ -1024,6 +1024,174 @@
}
};
+ const sendMultiBch = async (
+ BCH,
+ wallet,
+ utxos,
+ destinationAddressAndValueArray,
+ feeInSatsPerByte,
+ ) => {
+ try {
+ if (!destinationAddressAndValueArray) {
+ return null;
+ }
+
+ const inputUtxos = [];
+ let transactionBuilder;
+
+ // instance of transaction builder
+ if (process.env.REACT_APP_NETWORK === `mainnet`)
+ transactionBuilder = new BCH.TransactionBuilder();
+ else transactionBuilder = new BCH.TransactionBuilder('testnet');
+
+ let originalAmount = new BigNumber(0);
+ let txFee = 0;
+ let totalSatoshisToSend = 0;
+
+ // parse UTXOs
+ for (let i = 0; i < utxos.length; i++) {
+ const utxo = utxos[i];
+ originalAmount = originalAmount.plus(utxo.value);
+ const vout = utxo.vout;
+ const txid = utxo.txid;
+ // add input with txid and index of vout
+ transactionBuilder.addInput(txid, vout);
+
+ inputUtxos.push(utxo);
+ txFee += calcFee(BCH, inputUtxos, 2, feeInSatsPerByte);
+ }
+
+ var arrayLength = destinationAddressAndValueArray.length;
+ for (var i = 0; i < arrayLength; i++) {
+ // set the values from before and after the comma from each TextArea input line
+ let destinationAddress =
+ destinationAddressAndValueArray[i].split(',')[0];
+ let outputValue =
+ destinationAddressAndValueArray[i].split(',')[1];
+
+ const value = new BigNumber(outputValue);
+
+ // If user is attempting to send less than minimum accepted by the backend
+ if (
+ value.lt(
+ new BigNumber(
+ fromSmallestDenomination(
+ currency.dustSats,
+ ).toString(),
+ ),
+ )
+ ) {
+ // Throw the same error given by the backend attempting to broadcast such a tx
+ throw new Error('dust');
+ }
+
+ // logic to check for smallest denomination
+ const satoshisToSend = toSmallestDenomination(value);
+
+ // track the total send value throughout this loop
+ totalSatoshisToSend =
+ Number(totalSatoshisToSend) + Number(satoshisToSend);
+
+ // Throw validation error if toSmallestDenomination returns false
+ if (!satoshisToSend) {
+ const error = new Error(
+ `Invalid decimal places for send amount`,
+ );
+ throw error;
+ }
+
+ // add output w/ address and amount to send
+ transactionBuilder.addOutput(
+ BCH.Address.toCashAddress(destinationAddress),
+ parseInt(toSmallestDenomination(value)),
+ );
+ }
+
+ // Get change address from sending utxos
+ // fall back to what is stored in wallet
+ let REMAINDER_ADDR;
+
+ // Validate address
+ let isValidChangeAddress;
+ try {
+ REMAINDER_ADDR = inputUtxos[0].address;
+ isValidChangeAddress =
+ BCH.Address.isCashAddress(REMAINDER_ADDR);
+ } catch (err) {
+ isValidChangeAddress = false;
+ }
+ if (!isValidChangeAddress) {
+ REMAINDER_ADDR = wallet.Path1899.cashAddress;
+ }
+
+ // amount to send back to the remainder address.
+ const remainder = originalAmount
+ .minus(totalSatoshisToSend)
+ .minus(txFee);
+
+ if (remainder.lt(0)) {
+ const error = new Error(`Insufficient funds`);
+ error.code = SEND_BCH_ERRORS.INSUFFICIENT_FUNDS;
+ throw error;
+ }
+
+ if (remainder.gte(new BigNumber(currency.dustSats))) {
+ transactionBuilder.addOutput(
+ REMAINDER_ADDR,
+ parseInt(remainder),
+ );
+ }
+
+ // Sign the transactions with the HD node.
+ for (let i = 0; i < inputUtxos.length; i++) {
+ const utxo = inputUtxos[i];
+ transactionBuilder.sign(
+ i,
+ BCH.ECPair.fromWIF(utxo.wif),
+ undefined,
+ transactionBuilder.hashTypes.SIGHASH_ALL,
+ utxo.value,
+ );
+ }
+
+ // build tx
+ const tx = transactionBuilder.build();
+ // output rawhex
+ const hex = tx.toHex();
+
+ // Broadcast transaction to the network
+ const txidStr = await BCH.RawTransactions.sendRawTransaction([hex]);
+
+ if (txidStr && txidStr[0]) {
+ console.log(`${currency.ticker} txid`, txidStr[0]);
+ }
+ let link;
+ if (process.env.REACT_APP_NETWORK === `mainnet`) {
+ link = `${currency.blockExplorerUrl}/tx/${txidStr}`;
+ } else {
+ link = `${currency.blockExplorerUrlTestnet}/tx/${txidStr}`;
+ }
+ //console.log(`link`, link);
+
+ return link;
+ } catch (err) {
+ if (err.error === 'insufficient priority (code 66)') {
+ err.code = SEND_BCH_ERRORS.INSUFFICIENT_PRIORITY;
+ } else if (err.error === 'txn-mempool-conflict (code 18)') {
+ err.code = SEND_BCH_ERRORS.DOUBLE_SPENDING;
+ } else if (err.error === 'Network Error') {
+ err.code = SEND_BCH_ERRORS.NETWORK_ERROR;
+ } else if (
+ err.error ===
+ 'too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)'
+ ) {
+ err.code = SEND_BCH_ERRORS.MAX_UNCONFIRMED_TXS;
+ }
+ console.log(`error: `, err);
+ throw err;
+ }
+ };
+
const getBCH = (apiIndex = 0) => {
let ConstructedSlpWallet;
@@ -1048,6 +1216,7 @@
getRestUrl,
signPkMessage,
sendBch,
+ sendMultiBch,
sendToken,
createToken,
getTokenStats,
diff --git a/web/cashtab/src/utils/__tests__/validation.test.js b/web/cashtab/src/utils/__tests__/validation.test.js
--- a/web/cashtab/src/utils/__tests__/validation.test.js
+++ b/web/cashtab/src/utils/__tests__/validation.test.js
@@ -10,6 +10,7 @@
isValidCashtabSettings,
formatSavedBalance,
formatFiatBalance,
+ isValidSendToMany,
} from '../validation';
import { currency } from '@components/Common/Ticker.js';
import { fromSmallestDenomination } from '@utils/cashMethods';
@@ -281,4 +282,94 @@
it(`test formatFiatBalance with undefined input`, () => {
expect(formatFiatBalance(undefined, 'en-US')).toBe(undefined);
});
+ it(`test isValidSendToMany with null inputs`, () => {
+ expect(isValidSendToMany(null)).toBe('invalid address input');
+ });
+ it(`test isValidSendToMany with null addressInfo and valid valueString and ticker`, () => {
+ expect(isValidSendToMany(null, '233.32', 'XEC')).toBe(
+ 'invalid address input',
+ );
+ });
+ it(`test isValidSendToMany with valid addressInfo and ticker, null valueString`, () => {
+ const validAddressInfo = {
+ address: 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8',
+ isValid: true,
+ queryString: null,
+ amount: null,
+ };
+ expect(isValidSendToMany(validAddressInfo, null, 'XEC')).toBe(
+ 'invalid value input',
+ );
+ });
+ it(`test isValidSendToMany with valid addressInfo and valueString, null ticker`, () => {
+ const validAddressInfo = {
+ address: 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8',
+ isValid: true,
+ queryString: null,
+ amount: null,
+ };
+ expect(isValidSendToMany(validAddressInfo, '234.32', null)).toBe(
+ 'invalid ticker input',
+ );
+ });
+ it(`test isValidSendToMany with undefined addressInfo and valid valueString and ticker`, () => {
+ expect(isValidSendToMany(undefined, '233.32', 'XEC')).toBe(
+ 'invalid address input',
+ );
+ });
+ it(`test isValidSendToMany with valid addressInfo and ticker, undefined valueString`, () => {
+ const validAddressInfo = {
+ address: 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8',
+ isValid: true,
+ queryString: null,
+ amount: null,
+ };
+ expect(isValidSendToMany(validAddressInfo, undefined, 'XEC')).toBe(
+ 'invalid value input',
+ );
+ });
+ it(`test isValidSendToMany with valid addressInfo and valueString, undefined ticker`, () => {
+ const validAddressInfo = {
+ address: 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8',
+ isValid: true,
+ queryString: null,
+ amount: null,
+ };
+ expect(isValidSendToMany(validAddressInfo, '234.32', undefined)).toBe(
+ 'invalid ticker input',
+ );
+ });
+ it(`test isValidSendToMany with valid addressInfo and valueString, invalid ticker`, () => {
+ const validAddressInfo = {
+ address: 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8',
+ isValid: true,
+ queryString: null,
+ amount: null,
+ };
+ expect(isValidSendToMany(validAddressInfo, '234.32', 'FOOBAR')).toBe(
+ false,
+ );
+ });
+ it(`test isValidSendToMany with invalid addressInfo and valid valueString and ticker`, () => {
+ const validAddressInfo = {
+ address: 'ecash:qpatqlfooobarrrrrrrrrrkpyrlwu8',
+ isValid: false,
+ queryString: null,
+ amount: null,
+ };
+ expect(isValidSendToMany(validAddressInfo, '234.32', 'XEC')).toBe(
+ 'Invalid XEC address',
+ );
+ });
+ it(`test isValidSendToMany with valid addressInfo and ticker, less than minimal valueString`, () => {
+ const validAddressInfo = {
+ address: 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8',
+ isValid: true,
+ queryString: null,
+ amount: null,
+ };
+ expect(isValidSendToMany(validAddressInfo, '2.548', 'XEC')).toBe(
+ 'Send amount must be at least 5.5 XEC',
+ );
+ });
});
diff --git a/web/cashtab/src/utils/validation.js b/web/cashtab/src/utils/validation.js
--- a/web/cashtab/src/utils/validation.js
+++ b/web/cashtab/src/utils/validation.js
@@ -1,5 +1,5 @@
import BigNumber from 'bignumber.js';
-import { currency } from '@components/Common/Ticker.js';
+import { currency, isValidTokenPrefix } from '@components/Common/Ticker.js';
import { fromSmallestDenomination } from '@utils/cashMethods';
// Validate cash amount
@@ -157,3 +157,34 @@
return fiatBalance;
}
};
+
+export const isValidSendToMany = (addressInfo, valueString, ticker) => {
+ let error = false;
+ try {
+ if (addressInfo === null || addressInfo === undefined) {
+ return 'invalid address input';
+ } else if (valueString === null || valueString === undefined) {
+ return 'invalid value input';
+ } else if (ticker === null || ticker === undefined) {
+ return 'invalid ticker input';
+ }
+
+ const { address, isValid, queryString, amount } = addressInfo;
+
+ // Is this valid address?
+ if (!isValid) {
+ error = `Invalid ${ticker} address`;
+ // If valid address but token format
+ if (isValidTokenPrefix(address)) {
+ error = `Token addresses are not supported for ${ticker} sends`;
+ }
+ // Is this send value above minimum
+ } else if (valueString < 5.5) {
+ // value can only be XEC ticker in multi recipient mode
+ error = `Send amount must be at least 5.5 XEC`;
+ }
+ return error;
+ } catch (err) {
+ return err;
+ }
+};