Skip to content

Commit

Permalink
web-wallet: Add validation for self-referential transactions
Browse files Browse the repository at this point in the history
Resolves #3099
  • Loading branch information
nortonandreev committed Dec 3, 2024
1 parent b99877f commit 9a0923e
Show file tree
Hide file tree
Showing 8 changed files with 422 additions and 40 deletions.
2 changes: 2 additions & 0 deletions web-wallet/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

<!-- VERSIONS -->

Expand Down
37 changes: 31 additions & 6 deletions web-wallet/src/lib/components/Send/Send.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -158,7 +164,12 @@
isBalanceSufficientForGas
);
$: addressValidationResult = validateAddress(address);
$: addressValidationResult = validateAddress(
sendToAddress,
shieldedAddress,
publicAddress
);
$: isMoonlightTransaction = addressValidationResult.type === "account";
$: if (addressValidationResult.type) {
Expand Down Expand Up @@ -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"}
<Banner
Expand All @@ -226,9 +237,23 @@
bind:this={scanQrComponent}
bind:scanner
on:scan={(event) => {
address = event.detail;
sendToAddress = event.detail;
}}
/>

{#if addressValidationResult.isSelfReferential}
<Banner
variant="warning"
title="Self-referential transaction detected"
>
<p>
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.
</p>
</Banner>
{/if}
</div>
</WizardStep>
<!-- Amount Step -->
Expand Down Expand Up @@ -356,7 +381,7 @@
<span>To:</span>
</dt>
<dd class="operation__review-address">
<span>{address}</span>
<span>{sendToAddress}</span>
</dd>
</dl>

Expand All @@ -367,7 +392,7 @@
<WizardStep step={3} {key} showNavigation={false}>
<OperationResult
errorMessage="Transaction failed"
operation={execute(address, sendAmountInLux, gasPrice, gasLimit)}
operation={execute(sendToAddress, sendAmountInLux, gasPrice, gasLimit)}
pendingMessage="Processing transaction"
successMessage="Transaction created"
>
Expand Down
54 changes: 38 additions & 16 deletions web-wallet/src/lib/components/__tests__/Send.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -36,6 +40,8 @@ describe("Send", () => {
gasLimit: 20000000n,
gasPrice: 1n,
},
publicAddress,
shieldedAddress,
statuses: [
{
label: "Spendable",
Expand All @@ -47,12 +53,6 @@ describe("Send", () => {
const invalidAddress =
"aB5rL7yC2zK9eV3xH1gQ6fP4jD8sM0iU2oX7wG9nZ8lT3hU4jP5mK8nS6qJ3wF4aA9bB2cC5dD8eE7";

const address =
"47jNTgAhzn9KCKF3msCfvKg3k1P1QpPCLZ3HG3AoNp87sQ5WNS3QyjckYHWeuXqW7uvLmbKgejpP8Xkcip89vnMM";

const account =
"zTsZq814KfWUAQujzjBchbMEvqA1FiKBUakMCtAc2zCa74h9YVz4a2roYwS7LHDHeBwS1aap4f3GYhQBrxroYgsBcE4FJdkUbvpSD5LVXY6JRXNgMXgk6ckTPJUFKoHybff";

afterEach(() => {
cleanup();
baseProps.execute.mockClear();
Expand Down Expand Up @@ -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();
});
});
Expand Down Expand Up @@ -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");
Expand All @@ -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();
});
});
Expand All @@ -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");
Expand All @@ -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
Expand All @@ -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");
Expand All @@ -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
Expand All @@ -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");
Expand All @@ -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
Expand Down
Loading

0 comments on commit 9a0923e

Please sign in to comment.