From 9a0923e6edce90e8b7b06376c685e5834a3ca335 Mon Sep 17 00:00:00 2001 From: Norton Andreev Date: Tue, 3 Dec 2024 12:26:34 +0200 Subject: [PATCH] web-wallet: Add validation for self-referential transactions Resolves #3099 --- web-wallet/CHANGELOG.md | 2 + .../src/lib/components/Send/Send.svelte | 37 ++- .../src/lib/components/__tests__/Send.spec.js | 54 +++- .../__tests__/__snapshots__/Send.spec.js.snap | 296 ++++++++++++++++++ .../string/__tests__/validateAddress.spec.js | 40 ++- .../src/lib/dusk/string/validateAddress.js | 18 +- .../routes/(app)/dashboard/send/+page.svelte | 14 +- .../__tests__/__snapshots__/page.spec.js.snap | 1 + 8 files changed, 422 insertions(+), 40 deletions(-) diff --git a/web-wallet/CHANGELOG.md b/web-wallet/CHANGELOG.md index 0a8866c477..7c78ca0f7c 100644 --- a/web-wallet/CHANGELOG.md +++ b/web-wallet/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add "Support" section under Settings [#3071] - Add user feedback for "Send" flow validation [#3098] +- Add validation for self-referential transactions [#3099] ### Changed @@ -407,6 +408,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#3076]: https://github.com/dusk-network/rusk/issues/3076 [#3081]: https://github.com/dusk-network/rusk/issues/3081 [#3098]: https://github.com/dusk-network/rusk/issues/3098 +[#3099]: https://github.com/dusk-network/rusk/issues/3099 diff --git a/web-wallet/src/lib/components/Send/Send.svelte b/web-wallet/src/lib/components/Send/Send.svelte index c539e9a258..5586698cb0 100644 --- a/web-wallet/src/lib/components/Send/Send.svelte +++ b/web-wallet/src/lib/components/Send/Send.svelte @@ -54,11 +54,17 @@ /** @type {boolean} */ export let enableMoonlightTransactions = false; + /** @type {string} */ + export let shieldedAddress; + + /** @type {string} */ + export let publicAddress; + /** @type {number} */ let sendAmount = 1; /** @type {string} */ - let address = ""; + let sendToAddress = ""; /** @type {import("qr-scanner").default} */ let scanner; @@ -158,7 +164,12 @@ isBalanceSufficientForGas ); - $: addressValidationResult = validateAddress(address); + $: addressValidationResult = validateAddress( + sendToAddress, + shieldedAddress, + publicAddress + ); + $: isMoonlightTransaction = addressValidationResult.type === "account"; $: if (addressValidationResult.type) { @@ -204,7 +215,7 @@ required className={`operation__send-address ${!addressValidationResult.isValid ? "operation__send-address--invalid" : ""}`} type="multiline" - bind:value={address} + bind:value={sendToAddress} /> {#if addressValidationResult.type === "account"} { - address = event.detail; + sendToAddress = event.detail; }} /> + + {#if addressValidationResult.isSelfReferential} + +

+ You are attempting to initiate a transaction with your own wallet + address as both the sender and the receiver. Self-referential + transactions may not have meaningful purpose and will incur gas + fees. +

+
+ {/if} @@ -356,7 +381,7 @@ To:
- {address} + {sendToAddress}
@@ -367,7 +392,7 @@ diff --git a/web-wallet/src/lib/components/__tests__/Send.spec.js b/web-wallet/src/lib/components/__tests__/Send.spec.js index 7ec888d276..4d351a86bd 100644 --- a/web-wallet/src/lib/components/__tests__/Send.spec.js +++ b/web-wallet/src/lib/components/__tests__/Send.spec.js @@ -23,6 +23,10 @@ vi.mock("$lib/dusk/string", async (importOriginal) => { describe("Send", () => { const formatter = createCurrencyFormatter("en", "DUSK", 9); const lastTxId = "some-id"; + const publicAddress = + "zTsZq814KfWUAQujzjBchbMEvqA1FiKBUakMCtAc2zCa74h9YVz4a2roYwS7LHDHeBwS1aap4f3GYhQBrxroYgsBcE4FJdkUbvpSD5LVXY6JRXNgMXgk6ckTPJUFKoHybff"; + const shieldedAddress = + "47jNTgAhzn9KCKF3msCfvKg3k1P1QpPCLZ3HG3AoNp87sQ5WNS3QyjckYHWeuXqW7uvLmbKgejpP8Xkcip89vnMM"; const baseProps = { availableBalance: 1_000_000_000_000n, execute: vi.fn().mockResolvedValue(lastTxId), @@ -36,6 +40,8 @@ describe("Send", () => { gasLimit: 20000000n, gasPrice: 1n, }, + publicAddress, + shieldedAddress, statuses: [ { label: "Spendable", @@ -47,12 +53,6 @@ describe("Send", () => { const invalidAddress = "aB5rL7yC2zK9eV3xH1gQ6fP4jD8sM0iU2oX7wG9nZ8lT3hU4jP5mK8nS6qJ3wF4aA9bB2cC5dD8eE7"; - const address = - "47jNTgAhzn9KCKF3msCfvKg3k1P1QpPCLZ3HG3AoNp87sQ5WNS3QyjckYHWeuXqW7uvLmbKgejpP8Xkcip89vnMM"; - - const account = - "zTsZq814KfWUAQujzjBchbMEvqA1FiKBUakMCtAc2zCa74h9YVz4a2roYwS7LHDHeBwS1aap4f3GYhQBrxroYgsBcE4FJdkUbvpSD5LVXY6JRXNgMXgk6ckTPJUFKoHybff"; - afterEach(() => { cleanup(); baseProps.execute.mockClear(); @@ -89,13 +89,27 @@ describe("Send", () => { it("should display a warning if the address input is a public account", async () => { const { container, getByRole } = render(Send, baseProps); + const sendToAddress = + "aTsZq814KfWUAQujzjBchbMEvqA1FiKBUakMCtAc2zCa74h9YVz4a2roYwS7LHDHeBwS1aap4f3GYhQBrxroYgsBcE5FJdkUbvpSD5LVXY6JRXNgMXgk6ckTPJUFKoHybff"; const addressInput = getByRole("textbox"); await fireEvent.input(addressInput, { - target: { value: account }, + target: { value: sendToAddress }, }); - expect(addressInput).toHaveValue(account); + expect(addressInput).toHaveValue(sendToAddress); + expect(container.firstChild).toMatchSnapshot(); + }); + + it("should display a warning if the address input is self-referential", async () => { + const { container, getByRole } = render(Send, baseProps); + const addressInput = getByRole("textbox"); + + await fireEvent.input(addressInput, { + target: { value: publicAddress }, + }); + + expect(addressInput).toHaveValue(publicAddress); expect(container.firstChild).toMatchSnapshot(); }); }); @@ -214,7 +228,9 @@ describe("Send", () => { const { container, getByRole } = render(Send, baseProps); const addressInput = getByRole("textbox"); - await fireEvent.input(addressInput, { target: { value: address } }); + await fireEvent.input(addressInput, { + target: { value: shieldedAddress }, + }); await fireEvent.click(getByRole("button", { name: "Next" })); const amountInput = getByRole("spinbutton"); @@ -232,7 +248,7 @@ describe("Send", () => { ); expect(value.textContent).toBe(baseProps.formatter(amount)); - expect(key.textContent).toBe(address); + expect(key.textContent).toBe(shieldedAddress); expect(container.firstChild).toMatchSnapshot(); }); }); @@ -251,7 +267,9 @@ describe("Send", () => { const { getByRole, getByText } = render(Send, baseProps); const addressInput = getByRole("textbox"); - await fireEvent.input(addressInput, { target: { value: address } }); + await fireEvent.input(addressInput, { + target: { value: shieldedAddress }, + }); await fireEvent.click(getByRole("button", { name: "Next" })); const amountInput = getByRole("spinbutton"); @@ -264,7 +282,7 @@ describe("Send", () => { expect(baseProps.execute).toHaveBeenCalledTimes(1); expect(baseProps.execute).toHaveBeenCalledWith( - address, + shieldedAddress, duskToLux(amount), baseProps.gasSettings.gasPrice, baseProps.gasSettings.gasLimit @@ -285,7 +303,9 @@ describe("Send", () => { const { getByRole, getByText } = render(Send, baseProps); const addressInput = getByRole("textbox"); - await fireEvent.input(addressInput, { target: { value: address } }); + await fireEvent.input(addressInput, { + target: { value: shieldedAddress }, + }); await fireEvent.click(getByRole("button", { name: "Next" })); const amountInput = getByRole("spinbutton"); @@ -297,7 +317,7 @@ describe("Send", () => { expect(baseProps.execute).toHaveBeenCalledTimes(1); expect(baseProps.execute).toHaveBeenCalledWith( - address, + shieldedAddress, duskToLux(amount), baseProps.gasSettings.gasPrice, baseProps.gasSettings.gasLimit @@ -312,7 +332,9 @@ describe("Send", () => { const { getByRole, getByText } = render(Send, baseProps); const addressInput = getByRole("textbox"); - await fireEvent.input(addressInput, { target: { value: address } }); + await fireEvent.input(addressInput, { + target: { value: shieldedAddress }, + }); await fireEvent.click(getByRole("button", { name: "Next" })); const amountInput = getByRole("spinbutton"); @@ -324,7 +346,7 @@ describe("Send", () => { expect(baseProps.execute).toHaveBeenCalledTimes(1); expect(baseProps.execute).toHaveBeenCalledWith( - address, + shieldedAddress, duskToLux(amount), baseProps.gasSettings.gasPrice, baseProps.gasSettings.gasLimit 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 35ec1de9cd..dcf2c1364b 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 @@ -198,6 +198,301 @@ exports[`Send > Address step > should display a warning if the address input is + + + +
+ + + + + + + + Back + + + + + + + +
+ + + + + + + + + + +`; + +exports[`Send > Address step > should display a warning if the address input is self-referential 1`] = ` +
+
+
+
+ + 1 + + + + Address + + + + 2 + + + + Amount + + + + 3 + + + + Review + + + + 4 + + + + Done + + + +
+ +
+ + + +
+
+
+
+ Spendable + +
+
+ + 1,000.000000000 + + + + + + + +
+
+ +
+ +
+

+ Address: +

+ + +
+ +