@@ -1585,6 +1629,17 @@
}
}
>
+
diff --git a/web/cashtab/src/hooks/__tests__/useBCH.test.js b/web/cashtab/src/hooks/__tests__/useBCH.test.js
--- a/web/cashtab/src/hooks/__tests__/useBCH.test.js
+++ b/web/cashtab/src/hooks/__tests__/useBCH.test.js
@@ -103,8 +103,8 @@
expect(batchedResult).toStrictEqual(legacyResult);
});
- it('sends BCH correctly', async () => {
- const { sendBch } = useBCH();
+ it('sends one to one XEC correctly', async () => {
+ const { sendXec } = useBCH();
const BCH = new BCHJS();
const {
expectedTxId,
@@ -119,7 +119,7 @@
.fn()
.mockResolvedValue(expectedTxId);
expect(
- await sendBch(
+ await sendXec(
BCH,
wallet,
utxos,
@@ -133,8 +133,44 @@
);
});
+ it('sends one to many XEC correctly', async () => {
+ const { sendXec } = useBCH();
+ const BCH = new BCHJS();
+ const {
+ expectedTxId,
+ expectedHex,
+ utxos,
+ wallet,
+ destinationAddress,
+ sendAmount,
+ } = sendBCHMock;
+
+ const addressAndValueArray = [
+ 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05,6',
+ 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05,6.8',
+ 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05,7',
+ 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05,6',
+ ];
+
+ BCH.RawTransactions.sendRawTransaction = jest
+ .fn()
+ .mockResolvedValue(expectedTxId);
+ expect(
+ await sendXec(
+ BCH,
+ wallet,
+ utxos,
+ null,
+ null,
+ 1.01,
+ true,
+ addressAndValueArray,
+ ),
+ ).toBe(`${currency.blockExplorerUrl}/tx/${expectedTxId}`);
+ });
+
it(`Throws error if called trying to send one base unit ${currency.ticker} more than available in utxo set`, async () => {
- const { sendBch } = useBCH();
+ const { sendXec } = useBCH();
const BCH = new BCHJS();
const { expectedTxId, utxos, wallet, destinationAddress } = sendBCHMock;
@@ -149,7 +185,7 @@
.div(10 ** currency.cashDecimals)
.toString();
- const failedSendBch = sendBch(
+ const failedSendBch = sendXec(
BCH,
wallet,
utxos,
@@ -158,7 +194,7 @@
1.01,
);
expect(failedSendBch).rejects.toThrow(new Error('Insufficient funds'));
- const nullValuesSendBch = await sendBch(
+ const nullValuesSendBch = await sendXec(
BCH,
wallet,
utxos,
@@ -170,13 +206,13 @@
});
it('Throws error on attempt to send one satoshi less than backend dust limit', async () => {
- const { sendBch } = useBCH();
+ const { sendXec } = useBCH();
const BCH = new BCHJS();
const { expectedTxId, utxos, wallet, destinationAddress } = sendBCHMock;
BCH.RawTransactions.sendRawTransaction = jest
.fn()
.mockResolvedValue(expectedTxId);
- const failedSendBch = sendBch(
+ const failedSendBch = sendXec(
BCH,
wallet,
utxos,
@@ -189,7 +225,7 @@
1.01,
);
expect(failedSendBch).rejects.toThrow(new Error('dust'));
- const nullValuesSendBch = await sendBch(
+ const nullValuesSendBch = await sendXec(
BCH,
wallet,
utxos,
@@ -201,7 +237,7 @@
});
it('receives errors from the network and parses it', async () => {
- const { sendBch } = useBCH();
+ const { sendXec } = useBCH();
const BCH = new BCHJS();
const { sendAmount, utxos, wallet, destinationAddress } = sendBCHMock;
BCH.RawTransactions.sendRawTransaction = jest
@@ -209,7 +245,7 @@
.mockImplementation(async () => {
throw new Error('insufficient priority (code 66)');
});
- const insufficientPriority = sendBch(
+ const insufficientPriority = sendXec(
BCH,
wallet,
utxos,
@@ -226,7 +262,7 @@
.mockImplementation(async () => {
throw new Error('txn-mempool-conflict (code 18)');
});
- const txnMempoolConflict = sendBch(
+ const txnMempoolConflict = sendXec(
BCH,
wallet,
utxos,
@@ -243,7 +279,7 @@
.mockImplementation(async () => {
throw new Error('Network Error');
});
- const networkError = sendBch(
+ const networkError = sendXec(
BCH,
wallet,
utxos,
@@ -262,7 +298,7 @@
throw err;
});
- const tooManyAncestorsMempool = sendBch(
+ const tooManyAncestorsMempool = sendXec(
BCH,
wallet,
utxos,
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
@@ -873,31 +873,70 @@
}
};
- const sendBch = async (
+ const sendXec = async (
BCH,
wallet,
utxos,
destinationAddress,
sendAmount,
feeInSatsPerByte,
+ isOneToMany,
+ destinationAddressAndValueArray,
) => {
try {
- if (!sendAmount) {
- return null;
- }
+ let value = 0;
- const value = new BigNumber(sendAmount);
+ if (isOneToMany) {
+ // this is a one to many XEC transaction
+ if (
+ !destinationAddressAndValueArray ||
+ !destinationAddressAndValueArray.length
+ ) {
+ throw new Error('Invalid destinationAddressAndValueArray');
+ }
+ const arrayLength = destinationAddressAndValueArray.length;
+ for (let i = 0; i < arrayLength; i++) {
+ // add the total value being sent in this array of recipients
+ value += Number(
+ destinationAddressAndValueArray[i].split(',')[1],
+ );
+ }
- // 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');
+ value = new BigNumber(value);
+ // If user is attempting to send an aggregate value that is less than minimum accepted by the backend
+ if (
+ value.lt(
+ new BigNumber(
+ fromSmallestDenomination(
+ currency.dustSats,
+ ).toString(),
+ ),
+ )
+ ) {
+ // Throw the same error given by the backend attempting to broadcast such a tx
+ throw new Error('dust');
+ }
+ } else {
+ // this is a one to one XEC transaction then check sendAmount
+ // note: one to many transactions won't be sending a single sendAmount
+ if (!sendAmount) {
+ return null;
+ }
+
+ value = new BigNumber(sendAmount);
+ // 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');
+ }
}
const inputUtxos = [];
@@ -917,6 +956,7 @@
);
throw error;
}
+
let originalAmount = new BigNumber(0);
let txFee = 0;
for (let i = 0; i < utxos.length; i++) {
@@ -924,9 +964,9 @@
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);
@@ -961,11 +1001,28 @@
throw error;
}
- // add output w/ address and amount to send
- transactionBuilder.addOutput(
- BCH.Address.toCashAddress(destinationAddress),
- parseInt(toSmallestDenomination(value)),
- );
+ if (isOneToMany) {
+ // for one to many mode, add the multiple outputs from the array
+ let arrayLength = destinationAddressAndValueArray.length;
+ for (let i = 0; i < arrayLength; i++) {
+ // add each send tx from the array as an output
+ let outputAddress =
+ destinationAddressAndValueArray[i].split(',')[0];
+ let outputValue = new BigNumber(
+ destinationAddressAndValueArray[i].split(',')[1],
+ );
+ transactionBuilder.addOutput(
+ BCH.Address.toCashAddress(outputAddress),
+ parseInt(toSmallestDenomination(outputValue)),
+ );
+ }
+ } else {
+ // for one to one mode, add output w/ single address and amount to send
+ transactionBuilder.addOutput(
+ BCH.Address.toCashAddress(destinationAddress),
+ parseInt(toSmallestDenomination(value)),
+ );
+ }
if (remainder.gte(new BigNumber(currency.dustSats))) {
transactionBuilder.addOutput(
@@ -1047,7 +1104,7 @@
getTxData,
getRestUrl,
signPkMessage,
- sendBch,
+ sendXec,
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;
+ }
+};