Create a Token
@@ -554,7 +554,7 @@
className="sc-kEYyzF gZfJyA"
>
Create a Token
diff --git a/web/cashtab/src/utils/__mocks__/mockXecAirdropRecipients.js b/web/cashtab/src/utils/__mocks__/mockXecAirdropRecipients.js
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/utils/__mocks__/mockXecAirdropRecipients.js
@@ -0,0 +1,27 @@
+export const validXecAirdropList =
+ 'ecash:qrqgwxnaxlfagezvr2zj4s9yee6rrs96dyguh7zsvk,7\n' +
+ 'ecash:qzsha6zk9m0f3hlfe5q007zdwnzvn3vwuuzel2lfzv,67\n' +
+ 'ecash:qqlkyzmeupf7q8t2ttf2u8xgyk286pg4wyz0v403dj,4376\n' +
+ 'ecash:qz2taa43tljkvnvkeqv9pyx337hmg0zclqfqjrqst4,673728\n' +
+ 'ecash:qp0hlj26nwjpk9c3f0umjz7qmwpzfh0fhckq4zj9s6,23673\n';
+
+export const invalidXecAirdropList =
+ 'ecash:qrqgwxnaxlfagezvr2zj4s9yee6rrs96dyguh7zsvk,7\n' +
+ 'ecash:qzsha6zk9m0f3hlfe5q007zdwnzvn3vwuuzel2lfzv,3\n' +
+ 'ecash:qqlkyzmeupf7q8t2ttf2u8xgyk286pg4wyz0v403dj,4376\n' +
+ 'ecash:qz2taa43tljkvnvkeqv9pyx337hmg0zclqfqjrqst4,673728\n' +
+ 'ecash:qp0hlj26nwjpk9c3f0umjz7qmwpzfh0fhckq4zj9s6,23673\n';
+
+export const invalidXecAirdropListMultipleInvalidValues =
+ 'ecash:qrqgwxnaxlfagezvr2zj4s9yee6rrs96dyguh7zsvk,7\n' +
+ 'ecash:qzsha6zk9m0f3hlfe5q007zdwnzvn3vwuuzel2lfzv,3,3,4\n' +
+ 'ecash:qqlkyzmeupf7q8t2ttf2u8xgyk286pg4wyz0v403dj,4,1,2\n' +
+ 'ecash:qz2taa43tljkvnvkeqv9pyx337hmg0zclqfqjrqst4,673728\n' +
+ 'ecash:qp0hlj26nwjpk9c3f0umjz7qmwpzfh0fhckq4zj9s6,23673\n';
+
+export const invalidXecAirdropListMultipleValidValues =
+ 'ecash:qrqgwxnaxlfagezvr2zj4s9yee6rrs96dyguh7zsvk,7\n' +
+ 'ecash:qzsha6zk9m0f3hlfe5q007zdwnzvn3vwuuzel2lfzv,8,9,44\n' +
+ 'ecash:qqlkyzmeupf7q8t2ttf2u8xgyk286pg4wyz0v403dj,4376,43,1212\n' +
+ 'ecash:qz2taa43tljkvnvkeqv9pyx337hmg0zclqfqjrqst4,673728\n' +
+ 'ecash:qp0hlj26nwjpk9c3f0umjz7qmwpzfh0fhckq4zj9s6,23673\n';
diff --git a/web/cashtab/src/utils/__tests__/cashMethods.test.js b/web/cashtab/src/utils/__tests__/cashMethods.test.js
--- a/web/cashtab/src/utils/__tests__/cashMethods.test.js
+++ b/web/cashtab/src/utils/__tests__/cashMethods.test.js
@@ -13,6 +13,7 @@
toLegacyCash,
toLegacyToken,
toLegacyCashArray,
+ convertEtokenToEcashAddr,
parseOpReturn,
isExcludedUtxo,
whichUtxosWereAdded,
@@ -714,4 +715,59 @@
),
).toBe(false);
});
+ 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',
+ ),
+ );
+ });
});
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
@@ -15,6 +15,9 @@
isValidUtxo,
isValidBchApiUtxoObject,
isValidEtokenBurnAmount,
+ isValidTokenId,
+ isValidXecAirdrop,
+ isValidAirdropOutputsArray,
} from '../validation';
import { currency } from '@components/Common/Ticker.js';
import { fromSmallestDenomination } from '@utils/cashMethods';
@@ -24,6 +27,12 @@
noCovidStatsInvalid,
cGenStatsValid,
} from '../__mocks__/mockTokenStats';
+import {
+ validXecAirdropList,
+ invalidXecAirdropList,
+ invalidXecAirdropListMultipleInvalidValues,
+ invalidXecAirdropListMultipleValidValues,
+} from '../__mocks__/mockXecAirdropRecipients';
import {
validUtxo,
@@ -474,4 +483,114 @@
const testEtokenBurnAmount = 100;
expect(isValidEtokenBurnAmount(testEtokenBurnAmount, 100)).toBe(true);
});
+ it(`isValidTokenId accepts valid token ID that is 64 chars in length`, () => {
+ const testValidTokenId =
+ '1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e';
+ expect(isValidTokenId(testValidTokenId)).toBe(true);
+ });
+ it(`isValidTokenId rejects a token ID that is less than 64 chars in length`, () => {
+ const testValidTokenId = '111111thisisaninvalidtokenid';
+ expect(isValidTokenId(testValidTokenId)).toBe(false);
+ });
+ it(`isValidTokenId rejects a token ID that is more than 64 chars in length`, () => {
+ const testValidTokenId =
+ '111111111c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e';
+ expect(isValidTokenId(testValidTokenId)).toBe(false);
+ });
+ it(`isValidTokenId rejects a token ID number that is 64 digits in length`, () => {
+ const testValidTokenId = 8912345678912345678912345678912345678912345678912345678912345679;
+ expect(isValidTokenId(testValidTokenId)).toBe(false);
+ });
+ it(`isValidTokenId rejects null`, () => {
+ const testValidTokenId = null;
+ expect(isValidTokenId(testValidTokenId)).toBe(false);
+ });
+ it(`isValidTokenId rejects undefined`, () => {
+ const testValidTokenId = undefined;
+ expect(isValidTokenId(testValidTokenId)).toBe(false);
+ });
+ it(`isValidTokenId rejects empty string`, () => {
+ const testValidTokenId = '';
+ expect(isValidTokenId(testValidTokenId)).toBe(false);
+ });
+ it(`isValidTokenId rejects special character input`, () => {
+ const testValidTokenId = '^&$%@&^$@&%$!';
+ expect(isValidTokenId(testValidTokenId)).toBe(false);
+ });
+ it(`isValidTokenId rejects non-alphanumeric input`, () => {
+ const testValidTokenId = 99999999999;
+ expect(isValidTokenId(testValidTokenId)).toBe(false);
+ });
+ it(`isValidXecAirdrop accepts valid Total Airdrop Amount`, () => {
+ const testAirdropTotal = '1000000';
+ expect(isValidXecAirdrop(testAirdropTotal)).toBe(true);
+ });
+ it(`isValidXecAirdrop rejects null`, () => {
+ const testAirdropTotal = null;
+ expect(isValidXecAirdrop(testAirdropTotal)).toBe(false);
+ });
+ it(`isValidXecAirdrop rejects undefined`, () => {
+ const testAirdropTotal = undefined;
+ expect(isValidXecAirdrop(testAirdropTotal)).toBe(false);
+ });
+ it(`isValidXecAirdrop rejects empty string`, () => {
+ const testAirdropTotal = '';
+ expect(isValidXecAirdrop(testAirdropTotal)).toBe(false);
+ });
+ it(`isValidTokenId rejects an alphanumeric input`, () => {
+ const testAirdropTotal = 'a73hsyujs3737';
+ expect(isValidXecAirdrop(testAirdropTotal)).toBe(false);
+ });
+ it(`isValidTokenId rejects a number !> 0 in string format`, () => {
+ const testAirdropTotal = '0';
+ expect(isValidXecAirdrop(testAirdropTotal)).toBe(false);
+ });
+ it(`isValidAirdropOutputsArray accepts an airdrop list with valid XEC values`, () => {
+ // Tools.js logic removes the EOF newline before validation
+ const outputArray = validXecAirdropList.substring(
+ 0,
+ validXecAirdropList.length - 1,
+ );
+ expect(isValidAirdropOutputsArray(outputArray)).toBe(true);
+ });
+ it(`isValidAirdropOutputsArray rejects an airdrop list with invalid XEC values`, () => {
+ // Tools.js logic removes the EOF newline before validation
+ const outputArray = invalidXecAirdropList.substring(
+ 0,
+ invalidXecAirdropList.length - 1,
+ );
+ expect(isValidAirdropOutputsArray(outputArray)).toBe(false);
+ });
+ it(`isValidAirdropOutputsArray rejects null`, () => {
+ const testAirdropListValues = null;
+ expect(isValidAirdropOutputsArray(testAirdropListValues)).toBe(false);
+ });
+ it(`isValidAirdropOutputsArray rejects undefined`, () => {
+ const testAirdropListValues = undefined;
+ expect(isValidAirdropOutputsArray(testAirdropListValues)).toBe(false);
+ });
+ it(`isValidAirdropOutputsArray rejects empty string`, () => {
+ const testAirdropListValues = '';
+ expect(isValidAirdropOutputsArray(testAirdropListValues)).toBe(false);
+ });
+ it(`isValidAirdropOutputsArray rejects an airdrop list with multiple invalid XEC values per row`, () => {
+ // Tools.js logic removes the EOF newline before validation
+ const addressStringArray =
+ invalidXecAirdropListMultipleInvalidValues.substring(
+ 0,
+ invalidXecAirdropListMultipleInvalidValues.length - 1,
+ );
+
+ expect(isValidAirdropOutputsArray(addressStringArray)).toBe(false);
+ });
+ it(`isValidAirdropOutputsArray rejects an airdrop list with multiple valid XEC values per row`, () => {
+ // Tools.js logic removes the EOF newline before validation
+ const addressStringArray =
+ invalidXecAirdropListMultipleValidValues.substring(
+ 0,
+ invalidXecAirdropListMultipleValidValues.length - 1,
+ );
+
+ expect(isValidAirdropOutputsArray(addressStringArray)).toBe(false);
+ });
});
diff --git a/web/cashtab/src/utils/cashMethods.js b/web/cashtab/src/utils/cashMethods.js
--- a/web/cashtab/src/utils/cashMethods.js
+++ b/web/cashtab/src/utils/cashMethods.js
@@ -234,6 +234,40 @@
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.
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
@@ -316,3 +316,52 @@
new BigNumber(tokenBurnAmount).lte(maxAmount)
);
};
+
+// XEC airdrop field validations
+export const isValidTokenId = tokenId => {
+ const format = /[ `!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~]/;
+ const specialCharCheck = format.test(tokenId);
+
+ return (
+ typeof tokenId === 'string' &&
+ tokenId.length === 64 &&
+ tokenId.trim() != '' &&
+ !specialCharCheck
+ );
+};
+
+export const isValidXecAirdrop = xecAirdrop => {
+ return (
+ typeof xecAirdrop === 'string' &&
+ xecAirdrop.length > 0 &&
+ xecAirdrop.trim() != '' &&
+ new BigNumber(xecAirdrop).gt(0)
+ );
+};
+
+export const isValidAirdropOutputsArray = airdropOutputsArray => {
+ if (!airdropOutputsArray) {
+ return false;
+ }
+
+ let isValid = true;
+
+ // split by individual rows
+ const addressStringArray = airdropOutputsArray.split('\n');
+
+ for (let i = 0; i < addressStringArray.length; i++) {
+ const substring = addressStringArray[i].split(',');
+ let valueString = substring[1];
+ // if the XEC being sent is less than dust sats or contains extra values per line
+ if (
+ new BigNumber(valueString).lt(
+ fromSmallestDenomination(currency.dustSats),
+ ) ||
+ substring.length !== 2
+ ) {
+ isValid = false;
+ }
+ }
+
+ return isValid;
+};