@@ -1789,6 +1833,17 @@
}
}
>
+
diff --git a/web/cashtab/src/hooks/__mocks__/sendBCH.js b/web/cashtab/src/hooks/__mocks__/sendBCH.js
--- a/web/cashtab/src/hooks/__mocks__/sendBCH.js
+++ b/web/cashtab/src/hooks/__mocks__/sendBCH.js
@@ -33,6 +33,6 @@
expectedTxId:
'7a39961bbd7e27d804fb3169ef38a83234710fbc53897a4eb0c98454854a26d1',
expectedHex: [
- '02000000016fbde3a1a13bb90e1d939a214d6eb845396e6a07e2c4406c5ba8b554bfb4836e010000006a47304402207f641d1822e2bad12178238155516dc3a00d7aea25ef54fc602cc3390093c6ef022066b0bbbfb931c1c7d3d56b56b51e5ee4e9782ac51a4955bf06e0177533ad0c5c4121032d9ea429b4782e9a2c18a383362c23a44efa2f6d6641d63f53788b4bf45c1decffffffff0226020000000000001976a914d530980e14ee1f2037eaf00164d9c7ccfbddcd3288ac5eff0100000000001976a914c5c649ec64e02a16a5bd7a6c8f1fa5aaa7e488eb88ac00000000',
+ '02000000016fbde3a1a13bb90e1d939a214d6eb845396e6a07e2c4406c5ba8b554bfb4836e010000006a473044022014213502b672599a965f03a91c4aecb789ed15e758ba6594426572ed2ff20ef202201137053f16b9f1b796076ebe6e4755304f3be5df96bb181aaf9f70ad229291bb4121032d9ea429b4782e9a2c18a383362c23a44efa2f6d6641d63f53788b4bf45c1decffffffff0226020000000000001976a914d530980e14ee1f2037eaf00164d9c7ccfbddcd3288ac7cfe0100000000001976a914c5c649ec64e02a16a5bd7a6c8f1fa5aaa7e488eb88ac00000000',
],
};
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
@@ -111,8 +111,8 @@
expect(batchedResult).toStrictEqual(legacyResult);
});
- it('sends BCH correctly', async () => {
- const { sendBch } = useBCH();
+ it('sends XEC correctly', async () => {
+ const { sendXec } = useBCH();
const BCH = new BCHJS();
const {
expectedTxId,
@@ -127,13 +127,16 @@
.fn()
.mockResolvedValue(expectedTxId);
expect(
- await sendBch(
+ await sendXec(
BCH,
wallet,
utxos,
+ currency.defaultFee,
+ '',
+ false,
+ null,
destinationAddress,
sendAmount,
- 1.01,
),
).toBe(`${currency.blockExplorerUrl}/tx/${expectedTxId}`);
expect(BCH.RawTransactions.sendRawTransaction).toHaveBeenCalledWith(
@@ -141,8 +144,43 @@
);
});
+ 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,
+ currency.defaultFee,
+ '',
+ 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;
@@ -157,59 +195,72 @@
.div(10 ** currency.cashDecimals)
.toString();
- const failedSendBch = sendBch(
+ const failedSendBch = sendXec(
BCH,
wallet,
utxos,
+ currency.defaultFee,
+ '',
+ false,
+ null,
destinationAddress,
oneBaseUnitMoreThanBalance,
- 1.01,
+ //1.01,
);
expect(failedSendBch).rejects.toThrow(new Error('Insufficient funds'));
- const nullValuesSendBch = await sendBch(
+ const nullValuesSendBch = await sendXec(
BCH,
wallet,
utxos,
+ currency.defaultFee,
+ '',
+ false,
+ null,
destinationAddress,
null,
- 1.01,
);
expect(nullValuesSendBch).toBe(null);
});
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,
+ currency.defaultFee,
+ '',
+ false,
+ null,
destinationAddress,
new BigNumber(
fromSmallestDenomination(currency.dustSats).toString(),
)
.minus(new BigNumber('0.00000001'))
.toString(),
- 1.01,
);
expect(failedSendBch).rejects.toThrow(new Error('dust'));
- const nullValuesSendBch = await sendBch(
+ const nullValuesSendBch = await sendXec(
BCH,
wallet,
utxos,
+ currency.defaultFee,
+ '',
+ false,
+ null,
destinationAddress,
null,
- 1.01,
);
expect(nullValuesSendBch).toBe(null);
});
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
@@ -217,13 +268,16 @@
.mockImplementation(async () => {
throw new Error('insufficient priority (code 66)');
});
- const insufficientPriority = sendBch(
+ const insufficientPriority = sendXec(
BCH,
wallet,
utxos,
+ currency.defaultFee,
+ '',
+ false,
+ null,
destinationAddress,
sendAmount,
- 1.01,
);
await expect(insufficientPriority).rejects.toThrow(
new Error('insufficient priority (code 66)'),
@@ -234,13 +288,16 @@
.mockImplementation(async () => {
throw new Error('txn-mempool-conflict (code 18)');
});
- const txnMempoolConflict = sendBch(
+ const txnMempoolConflict = sendXec(
BCH,
wallet,
utxos,
+ currency.defaultFee,
+ '',
+ false,
+ null,
destinationAddress,
sendAmount,
- 1.01,
);
await expect(txnMempoolConflict).rejects.toThrow(
new Error('txn-mempool-conflict (code 18)'),
@@ -251,13 +308,16 @@
.mockImplementation(async () => {
throw new Error('Network Error');
});
- const networkError = sendBch(
+ const networkError = sendXec(
BCH,
wallet,
utxos,
+ currency.defaultFee,
+ '',
+ false,
+ null,
destinationAddress,
sendAmount,
- 1.01,
);
await expect(networkError).rejects.toThrow(new Error('Network Error'));
@@ -270,13 +330,16 @@
throw err;
});
- const tooManyAncestorsMempool = sendBch(
+ const tooManyAncestorsMempool = sendXec(
BCH,
wallet,
utxos,
+ currency.defaultFee,
+ '',
+ false,
+ null,
destinationAddress,
sendAmount,
- 1.01,
);
await expect(tooManyAncestorsMempool).rejects.toThrow(
new Error(
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
@@ -959,32 +959,73 @@
}
};
- const sendBch = async (
+ const sendXec = async (
BCH,
wallet,
utxos,
- destinationAddress,
- sendAmount,
feeInSatsPerByte,
optionalOpReturnMsg,
+ isOneToMany,
+ destinationAddressAndValueArray,
+ destinationAddress,
+ sendAmount,
) => {
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 = [];
@@ -1068,11 +1109,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(
@@ -1154,7 +1212,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,83 @@
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 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,35 @@
return fiatBalance;
}
};
+
+export const isValidSendToMany = (addressInfo, valueString, ticker) => {
+ let isValidInput = true;
+
+ 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) {
+ isValidInput = `Invalid ${ticker} address`;
+ // If valid address but token format
+ if (isValidTokenPrefix(address)) {
+ isValidInput = `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
+ isValidInput = `Send amount must be at least 5.5 XEC`;
+ }
+ return isValidInput;
+ } catch (err) {
+ return err;
+ }
+};