diff --git a/web-wallet/CHANGELOG.md b/web-wallet/CHANGELOG.md
index 0cfc824459..dc9199e960 100644
--- a/web-wallet/CHANGELOG.md
+++ b/web-wallet/CHANGELOG.md
@@ -10,16 +10,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Add visible version, commit hash and build date [#1441]
+- Add Address validation (Transfer flow) [#1377]
### Changed
+- Change Get Quote API Endpoint to env variable [#1311]
+
### Removed
- Remove the use of `checkValidity()` in Send and Stake flow amounts validity checks [#1391]
### Fixed
-- Fix typo in routes/welcome/__tests__/page.spec.js [#1445]
+- Fix typo in routes/welcome/tests/page.spec.js [#1445]
## [0.2.1] - 2024-02-20
@@ -50,7 +53,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
-- Change Get Quote API Endpoint to env variable [#1311]
- Change Holdings component design [#1361]
- Change `fiatCurrency`, `locale`, `tokenCurrency`, `token` to required properties in Balance component [#1323]
- Change `package.json` fields to reflect repo change [#1367]
@@ -96,6 +98,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#1367]: https://github.com/dusk-network/rusk/issues/1367
[#1353]: https://github.com/dusk-network/rusk/issues/1353
[#1343]: https://github.com/dusk-network/rusk/issues/1343
+[#1377]: https://github.com/dusk-network/rusk/issues/1377
[#1403]: https://github.com/dusk-network/rusk/issues/1403
[#1412]: https://github.com/dusk-network/rusk/issues/1412
[#1410]: https://github.com/dusk-network/rusk/issues/1410
@@ -107,11 +110,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#1423]: https://github.com/dusk-network/rusk/issues/1423
[#1391]: https://github.com/dusk-network/rusk/issues/1391
[#1417]: https://github.com/dusk-network/rusk/issues/1417
-<<<<<<< HEAD
[#1445]: https://github.com/dusk-network/rusk/issues/1445
-=======
[#1441]: https://github.com/dusk-network/rusk/issues/1441
->>>>>>> e0c2a641 (web-wallet: Add visible version, commit hash and build date)
[Unreleased]: https://github.com/dusk-network/rusk/tree/master/web-wallet
diff --git a/web-wallet/src/lib/components/Send/Send.svelte b/web-wallet/src/lib/components/Send/Send.svelte
index 1b380f121d..c76653b803 100644
--- a/web-wallet/src/lib/components/Send/Send.svelte
+++ b/web-wallet/src/lib/components/Send/Send.svelte
@@ -7,6 +7,7 @@
import { deductLuxFeeFrom } from "$lib/contracts";
import { duskToLux, luxToDusk } from "$lib/dusk/currency";
+ import { validateAddress } from "$lib/dusk/string";
import { logo } from "$lib/dusk/icons";
import {
AnchorButton,
@@ -79,6 +80,8 @@
$: totalLuxFee = luxFee + duskToLux(amount);
$: isFeeWithinLimit = totalLuxFee <= duskToLux(spendable);
$: isNextButtonDisabled = !(isAmountValid && isValidGas && isFeeWithinLimit);
+
+ $: addressValidationResult = validateAddress(address);
@@ -148,7 +151,7 @@
@@ -166,7 +169,8 @@
/>
@@ -326,4 +330,8 @@
.review-transaction__value {
font-weight: bold;
}
+
+ :global(.operation__send-address--invalid) {
+ color: var(--error-color);
+ }
diff --git a/web-wallet/src/lib/components/__tests__/Send.spec.js b/web-wallet/src/lib/components/__tests__/Send.spec.js
index 6c3a22f1d0..4fbaa37886 100644
--- a/web-wallet/src/lib/components/__tests__/Send.spec.js
+++ b/web-wallet/src/lib/components/__tests__/Send.spec.js
@@ -34,9 +34,13 @@ describe("Send", () => {
value: "1,000.000000000"
}]
};
- const address =
+
+ const invalidAddress =
"aB5rL7yC2zK9eV3xH1gQ6fP4jD8sM0iU2oX7wG9nZ8lT3hU4jP5mK8nS6qJ3wF4aA9bB2cC5dD8eE7";
+ const address =
+ "47jNTgAhzn9KCKF3msCfvKg3k1P1QpPCLZ3HG3AoNp87sQ5WNS3QyjckYHWeuXqW7uvLmbKgejpP8Xkcip89vnMM";
+
afterEach(() => {
cleanup();
baseProps.execute.mockClear();
@@ -117,7 +121,22 @@ describe("Send", () => {
expect(nextButton).toBeDisabled();
});
- it("should enable the next button if the user inputs an address", async () => {
+ it("should disable the next button if the address is invalid empty", async () => {
+ const { getByRole } = render(Send, baseProps);
+
+ await fireEvent.click(getByRole("button", { name: "Next" }));
+
+ const nextButton = getByRole("button", { name: "Next" });
+ const addressInput = getByRole("textbox");
+
+ await fireEvent.input(addressInput, { target: { value: invalidAddress } });
+
+ expect(addressInput).toHaveValue(invalidAddress);
+
+ expect(nextButton).toBeDisabled();
+ });
+
+ it("should enable the next button if the user inputs a valid address", async () => {
const { getByRole } = render(Send, baseProps);
await fireEvent.click(getByRole("button", { name: "Next" }));
diff --git a/web-wallet/src/lib/components/__tests__/__snapshots__/Send.spec.js.snap b/web-wallet/src/lib/components/__tests__/__snapshots__/Send.spec.js.snap
index 50697c438a..b606845fa5 100644
--- a/web-wallet/src/lib/components/__tests__/__snapshots__/Send.spec.js.snap
+++ b/web-wallet/src/lib/components/__tests__/__snapshots__/Send.spec.js.snap
@@ -77,7 +77,8 @@ exports[`Send > Address step > should render the Send component Address step 1`]
@@ -560,7 +561,7 @@ exports[`Send > Review step > should render the Send component Review step 1`] =
- aB5rL7yC2zK9eV3xH1gQ6fP4jD8sM0iU2oX7wG9nZ8lT3hU4jP5mK8nS6qJ3wF4aA9bB2cC5dD8eE7
+ 47jNTgAhzn9KCKF3msCfvKg3k1P1QpPCLZ3HG3AoNp87sQ5WNS3QyjckYHWeuXqW7uvLmbKgejpP8Xkcip89vnMM
diff --git a/web-wallet/src/lib/dusk/components/Textbox/Textbox.svelte b/web-wallet/src/lib/dusk/components/Textbox/Textbox.svelte
index c89da730f9..58066fa0f7 100644
--- a/web-wallet/src/lib/dusk/components/Textbox/Textbox.svelte
+++ b/web-wallet/src/lib/dusk/components/Textbox/Textbox.svelte
@@ -25,7 +25,7 @@
inputElement?.select();
}
- const classes = makeClassName([
+ $: classes = makeClassName([
"dusk-textbox",
`dusk-textbox-${type}`,
className
diff --git a/web-wallet/src/lib/dusk/string/__tests__/validateAddress.spec.js b/web-wallet/src/lib/dusk/string/__tests__/validateAddress.spec.js
new file mode 100644
index 0000000000..24eadd3532
--- /dev/null
+++ b/web-wallet/src/lib/dusk/string/__tests__/validateAddress.spec.js
@@ -0,0 +1,63 @@
+import { validateAddress } from "..";
+import { describe, expect, it } from "vitest";
+
+describe("validateAddress", () => {
+ const validAddresses = [
+ "47jNTgAhzn9KCKF3msCfvKg3k1P1QpPCLZ3HG3AoNp87sQ5WNS3QyjckYHWeuXqW7uvLmbKgejpP8Xkcip89vnMM",
+ "4xwKPC9UMvketmoNkDvyaJufcTZWmNn8giB8xWTf3Qk8nFkRW81nTVwSdGPcbomzHThPuoXsdFzrzwiMar6BEfdw",
+ "5kB6VBePF8eFhFVjLwM1xrEL6yGBm1uDsoWyRjdqDQ2nNz8nECAsRh3MZiM6uEo6WmukqyKzzCK9B5rcPTnjZQgt",
+ "4LaS4bWzFQtvxZ7frUaXbfm3xsbnHHYwNkGnLqqpmWPYQeSfbAPy7N4Md8gk5gHn9f4wxNSNyFJuyxcnXPSWTRMd",
+ "gMxrVEH5aW7XuQiXN2Pm2YRLHyCNmokmBb1VzjcmcQg7gzmxstPnozdt7SvvMKLP71BadPsa5jmoWFc2WzWDYPo"
+ ];
+
+ const invalidAddresses = [
+ // Invalid Base58
+ "InvalidKey12345",
+
+ // Too short
+ "4LaS4bWzFQtvxZ7frUaXbfm3xsbnHHYwNkGnLqqpmWPY",
+
+ // Too long
+ "5kB6VBePF8eFhFVjLwM1xrEL6yGBm1uDsoWyRjdqDQ2nNz8nECAsRh3MZiM6uEo6WmukqyKzzCK9B5rcPTnjZQgtXXXXXXXX",
+
+ // Empty string
+ "",
+
+ // Contains an invalid character (!)
+ "47jNTgAhzn9KCKF3msCfvKg3k1P1QpPCLZ3HG3AoNp87sQ5WNS3QyjckYHWeuXqW7uvLmbKgejpP8Xkcip89vnM!",
+
+ // Contains an invalid character (_)
+ "47jNTgAhzn9_CKF3msCfvKg3k1P1QpPCLZ3HG3AoNp87sQ5WNS3QyjckYHWeuXqW7uvLmbKgejpP8Xkcip89vnM",
+
+ // Contains an invalid character ( )
+ "47jNTgAhzn9 CKF3msCfvKg3k1P1QpPCLZ3HG3AoNp87sQ5WNS3QyjckYHWeuXqW7uvLmbKgejpP8Xkcip89vnM",
+
+ // Contains an invalid character (0)
+ "47jNTgAhzn0KCKF3msCfvKg3k1P1QpPCLZ3HG3AoNp87sQ5WNS3QyjckYHWeuXqW7uvLmbKgejpP8Xkcip89vnMM",
+
+ // Contains an invalid character (O)
+ "47jNTgAhznOKCKF3msCfvKg3k1P1QpPCLZ3HG3AoNp87sQ5WNS3QyjckYHWeuXqW7uvLmbKgejpP8Xkcip89vnMM",
+
+ // Contains an invalid character (l)
+ "47jNTgAhznlKCKF3msCfvKg3k1P1QpPCLZ3HG3AoNp87sQ5WNS3QyjckYHWeuXqW7uvLmbKgejpP8Xkcip89vnMM",
+
+ // Contains an invalid character (l)
+ "47jNTgAhznIKCKF3msCfvKg3k1P1QpPCLZ3HG3AoNp87sQ5WNS3QyjckYHWeuXqW7uvLmbKgejpP8Xkcip89vnMM"
+ ];
+
+ it("passes when supplied with a valid address", () => {
+ for (const address of validAddresses) {
+ const result = validateAddress(address);
+
+ expect(result.isValid).toBe(true);
+ }
+ });
+
+ it("fails when supplied with an invalid address", () => {
+ for (const address of invalidAddresses) {
+ const result = validateAddress(address);
+
+ expect(result.isValid).toBe(false);
+ }
+ });
+});
diff --git a/web-wallet/src/lib/dusk/string/index.js b/web-wallet/src/lib/dusk/string/index.js
index b56b48dd15..73ed19da2e 100644
--- a/web-wallet/src/lib/dusk/string/index.js
+++ b/web-wallet/src/lib/dusk/string/index.js
@@ -2,3 +2,4 @@ export { default as calculateAdaptiveCharCount } from "./calculateAdaptiveCharCo
export { default as hexStringToBytes } from "./hexStringToBytes";
export { default as makeClassName } from "./makeClassName";
export { default as middleEllipsis } from "./middleEllipsis";
+export { default as validateAddress } from "./validateAddress";
diff --git a/web-wallet/src/lib/dusk/string/validateAddress.js b/web-wallet/src/lib/dusk/string/validateAddress.js
new file mode 100644
index 0000000000..f9415c03ba
--- /dev/null
+++ b/web-wallet/src/lib/dusk/string/validateAddress.js
@@ -0,0 +1,20 @@
+/**
+ * Validates a Dusk address, with feedback on failure or success.
+ * @param {String} address The public spent key to validate.
+ * @returns {{isValid: boolean, reason: string}} An object with two keys:
+ * - `isValid` {Boolean} - true if the address is valid, false if invalid.
+ * - `reason` {String} - describes why the address is invalid or confirms if it is valid.
+ */
+export default function validateAddress (address) {
+ const regex = /[\W_0OIl]/;
+
+ if (address.length < 87 || address.length > 88) {
+ return { isValid: false, reason: "Invalid length. Addresses must be 87 or 88 characters long." };
+ }
+
+ if (address.search(regex) !== -1) {
+ return { isValid: false, reason: "Invalid character set. Address contains forbidden characters." };
+ }
+
+ return { isValid: true, reason: "Valid address." };
+}