diff --git a/web-wallet/.eslintignore b/web-wallet/.eslintignore index 38972655f..da81b5c35 100644 --- a/web-wallet/.eslintignore +++ b/web-wallet/.eslintignore @@ -4,10 +4,11 @@ node_modules /.svelte-kit /package .env -.env.* +.env.\* !.env.example # Ignore files for PNPM, NPM and YARN + pnpm-lock.yaml package-lock.json yarn.lock diff --git a/web-wallet/CHANGELOG.md b/web-wallet/CHANGELOG.md index f25308d9c..bf490f79c 100644 --- a/web-wallet/CHANGELOG.md +++ b/web-wallet/CHANGELOG.md @@ -9,12 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add option to sync from a custom block height on Wallet Restoration [#1568] +- Show current block height on Wallet Creation [#1561] - Added gas settings validation on Unstake / Widthdraw Rewards flows [#2000] - Added allocation (shield/unshield) page and UI [#2196] - Added token migration contract bindings [#2014] ### Changed +- Newly created Wallet does not sync from genesis [#1567] - Update font-display to swap for custom fonts to improve performance [#2026] - Update anchor colors to ensure better accessibility [#1765] - Update Transactions list design [#1922] @@ -229,7 +232,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#1545]: https://github.com/dusk-network/rusk/issues/1545 [#1547]: https://github.com/dusk-network/rusk/issues/1547 [#1552]: https://github.com/dusk-network/rusk/issues/1552 +[#1561]: https://github.com/dusk-network/rusk/issues/1561 [#1565]: https://github.com/dusk-network/rusk/issues/1565 +[#1568]: https://github.com/dusk-network/rusk/issues/1568 +[#1567]: https://github.com/dusk-network/rusk/issues/1567 [#1576]: https://github.com/dusk-network/rusk/issues/1576 [#1591]: https://github.com/dusk-network/rusk/issues/1591 [#1598]: https://github.com/dusk-network/rusk/issues/1598 diff --git a/web-wallet/__mocks__/Wallet.js b/web-wallet/__mocks__/Wallet.js index f465ef3a9..5dd33981e 100644 --- a/web-wallet/__mocks__/Wallet.js +++ b/web-wallet/__mocks__/Wallet.js @@ -1,13 +1,13 @@ class Wallet { - constructor(seed, gasLimit = 2900000000, gasPrice = 1) { - this.gasLimit = gasLimit; - this.gasPrice = gasPrice; + constructor(seed) { this.seed = seed; this.wasm = {}; } - gasLimit; - gasPrice; + static get networkBlockHeight() { + return Promise.resolve(0); + } + seed; wasm; diff --git a/web-wallet/package-lock.json b/web-wallet/package-lock.json index b1791d995..c5186cbbf 100644 --- a/web-wallet/package-lock.json +++ b/web-wallet/package-lock.json @@ -9,7 +9,7 @@ "version": "0.5.0", "license": "MPL-2.0", "dependencies": { - "@dusk-network/dusk-wallet-js": "0.4.2", + "@dusk-network/dusk-wallet-js": "0.5.3", "@floating-ui/dom": "1.6.5", "@mdi/js": "7.4.47", "@wagmi/connectors": "5.1.6", @@ -2223,12 +2223,12 @@ } }, "node_modules/@dusk-network/dusk-wallet-js": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@dusk-network/dusk-wallet-js/-/dusk-wallet-js-0.4.2.tgz", - "integrity": "sha512-9Jy/Amm4oKkPvjw9/xo7/RRwm9U6fRobkFArVcPXwN5sL2uz6/UPd7b6w4X5bBTrdEyCZHNBwtslmwAHrwV6dA==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@dusk-network/dusk-wallet-js/-/dusk-wallet-js-0.5.3.tgz", + "integrity": "sha512-HXjAewHCYL6zg65V0QaUY/xC9kkQhUs5mu+d+/3ljsRfz4mXu1fGY7pb46Kbd7lbfd7LFyBguLaQeF4ZexXfKQ==", "license": "MPL", "dependencies": { - "dexie": "3.2.4", + "dexie": "3.2.7", "fake-indexeddb": "5.0.1" } }, @@ -9880,9 +9880,9 @@ "license": "MIT" }, "node_modules/dexie": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/dexie/-/dexie-3.2.4.tgz", - "integrity": "sha512-VKoTQRSv7+RnffpOJ3Dh6ozknBqzWw/F3iqMdsZg958R0AS8AnY9x9d1lbwENr0gzeGJHXKcGhAMRaqys6SxqA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-3.2.7.tgz", + "integrity": "sha512-2a+BXvVhY5op+smDRLxeBAivE7YcYaneXJ1la3HOkUfX9zKkE/AJ8CNgjiXbtXepFyFmJNGSbmjOwqbT749r/w==", "license": "Apache-2.0", "engines": { "node": ">=6.0" diff --git a/web-wallet/package.json b/web-wallet/package.json index 677807431..f363750a7 100644 --- a/web-wallet/package.json +++ b/web-wallet/package.json @@ -35,7 +35,7 @@ "type": "module", "version": "0.5.0", "dependencies": { - "@dusk-network/dusk-wallet-js": "0.4.2", + "@dusk-network/dusk-wallet-js": "0.5.3", "@floating-ui/dom": "1.6.5", "@mdi/js": "7.4.47", "@wagmi/connectors": "5.1.6", diff --git a/web-wallet/src/__mocks__/mockedWalletStore.js b/web-wallet/src/__mocks__/mockedWalletStore.js index bd2c41e7b..53d16880a 100644 --- a/web-wallet/src/__mocks__/mockedWalletStore.js +++ b/web-wallet/src/__mocks__/mockedWalletStore.js @@ -9,9 +9,8 @@ const content = { addresses, balance, currentAddress, - error: null, initialized: true, - isSyncing: false, + syncStatus: { current: 0, error: null, isInProgress: false, last: 0 }, }; const mockedWalletStore = mockReadableStore(content); diff --git a/web-wallet/src/lib/components/CopyField/CopyField.css b/web-wallet/src/lib/components/CopyField/CopyField.css new file mode 100644 index 000000000..f2abfc791 --- /dev/null +++ b/web-wallet/src/lib/components/CopyField/CopyField.css @@ -0,0 +1,9 @@ +.copy-field { + display: flex; + align-items: center; + gap: var(--default-gap); +} + +.copy-field__content { + flex: 1; +} diff --git a/web-wallet/src/lib/components/CopyField/CopyField.svelte b/web-wallet/src/lib/components/CopyField/CopyField.svelte new file mode 100644 index 000000000..975817453 --- /dev/null +++ b/web-wallet/src/lib/components/CopyField/CopyField.svelte @@ -0,0 +1,55 @@ + + + + +
+ +
diff --git a/web-wallet/src/lib/components/__tests__/CopyField.spec.js b/web-wallet/src/lib/components/__tests__/CopyField.spec.js new file mode 100644 index 000000000..9ec8b76d6 --- /dev/null +++ b/web-wallet/src/lib/components/__tests__/CopyField.spec.js @@ -0,0 +1,54 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { cleanup, fireEvent, render } from "@testing-library/svelte"; +import { CopyField } from ".."; + +Object.assign(navigator, { + clipboard: { + writeText: vi.fn().mockResolvedValue(""), + }, +}); + +describe("CopyField", () => { + const baseProps = { + disabled: false, + displayValue: "1,234,567", + name: "Sample Information", + rawValue: "1234567", + }; + + const baseOptions = { + props: baseProps, + target: document.body, + }; + + afterEach(cleanup); + + it("renders the CopyField component", () => { + const { container } = render(CopyField, baseOptions); + + expect(container.firstChild).toMatchSnapshot(); + }); + + it("renders the CopyField component with the copy button disabled", () => { + const { container, getByRole } = render(CopyField, { + ...baseOptions, + props: { ...baseProps, disabled: true }, + }); + + const copyButton = getByRole("button"); + + expect(copyButton).toBeDisabled(); + + expect(container.firstChild).toMatchSnapshot(); + }); + + it("copies the raw value on pressing the copy button", async () => { + const { getByRole } = render(CopyField, baseOptions); + + const copyButton = getByRole("button"); + + await fireEvent.click(copyButton); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith("1234567"); + }); +}); diff --git a/web-wallet/src/lib/components/__tests__/__snapshots__/CopyField.spec.js.snap b/web-wallet/src/lib/components/__tests__/__snapshots__/CopyField.spec.js.snap new file mode 100644 index 000000000..d56203ae2 --- /dev/null +++ b/web-wallet/src/lib/components/__tests__/__snapshots__/CopyField.spec.js.snap @@ -0,0 +1,66 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`CopyField > renders the CopyField component 1`] = ` +
+ + + + +
+`; + +exports[`CopyField > renders the CopyField component with the copy button disabled 1`] = ` +
+ + + + +
+`; diff --git a/web-wallet/src/lib/components/index.js b/web-wallet/src/lib/components/index.js index 6671565e6..9082364a2 100644 --- a/web-wallet/src/lib/components/index.js +++ b/web-wallet/src/lib/components/index.js @@ -7,6 +7,7 @@ export { default as ApproveMigration } from "./ApproveMigration/ApproveMigration export { default as Balance } from "./Balance/Balance.svelte"; export { default as ContractOperations } from "./ContractOperations/ContractOperations.svelte"; export { default as ContractStatusesList } from "./ContractStatusesList/ContractStatusesList.svelte"; +export { default as CopyField } from "./CopyField/CopyField.svelte"; export { default as DashboardNav } from "./DashboardNav/DashboardNav.svelte"; export { default as ExecuteMigration } from "./ExecuteMigration/ExecuteMigration.svelte"; export { default as ExistingWalletNotice } from "./ExistingWalletNotice/ExistingWalletNotice.svelte"; diff --git a/web-wallet/src/lib/containers/Cards/IconHeadingCard.svelte b/web-wallet/src/lib/containers/Cards/IconHeadingCard.svelte index 82e6dc4cd..da5b43120 100644 --- a/web-wallet/src/lib/containers/Cards/IconHeadingCard.svelte +++ b/web-wallet/src/lib/containers/Cards/IconHeadingCard.svelte @@ -9,6 +9,9 @@ /** @type {string} */ export let heading; + /** @type {string | Undefined} */ + export let className = undefined; + /** @type {CardGap} */ export let gap = "default"; @@ -19,7 +22,7 @@ export let reverse = false; - +
import { Card, Icon, Switch } from "$lib/dusk/components"; + import { createEventDispatcher } from "svelte"; import "./Card.css"; @@ -17,6 +18,12 @@ /** @type {boolean} */ export let isToggled = false; + + const dispatch = createEventDispatcher(); + + function dispatchToggleEvent() { + dispatch("toggle", { isToggled }); + } @@ -27,7 +34,7 @@ {/if}

{heading}

- +
{#if isToggled} diff --git a/web-wallet/src/lib/containers/StakeContract/StakeContract.svelte b/web-wallet/src/lib/containers/StakeContract/StakeContract.svelte index 92e74a7e2..763dd597f 100644 --- a/web-wallet/src/lib/containers/StakeContract/StakeContract.svelte +++ b/web-wallet/src/lib/containers/StakeContract/StakeContract.svelte @@ -57,13 +57,15 @@ const executeOperations = { stake: (amount, gasPrice, gasLimit) => walletStore - .stake(amount, gasPrice, gasLimit) + .stake(amount, { limit: gasLimit, price: gasPrice }) .then(getLastTransactionHash), unstake: (gasPrice, gasLimit) => - walletStore.unstake(gasPrice, gasLimit).then(getLastTransactionHash), + walletStore + .unstake({ limit: gasLimit, price: gasPrice }) + .then(getLastTransactionHash), "withdraw-rewards": (gasPrice, gasLimit) => walletStore - .withdrawReward(gasPrice, gasLimit) + .withdrawReward({ limit: gasLimit, price: gasPrice }) .then(getLastTransactionHash), }; @@ -124,8 +126,8 @@ $: ({ currentOperation } = $operationsStore); $: [gasSettings, language, minAllowedStake] = collectSettings($settingsStore); const { hideStakingNotice } = $settingsStore; - $: ({ balance, error, isSyncing } = $walletStore); - $: isSyncOK = !(isSyncing || !!error); + $: ({ balance, syncStatus } = $walletStore); + $: isSyncOK = !(syncStatus.isInProgress || !!syncStatus.error); $: duskFormatter = createCurrencyFormatter(language, "DUSK", 9); @@ -137,7 +139,7 @@ waitFor={walletStore.getStakeInfo()} > - {#if !isSyncing && !error} + {#if !syncStatus.isInProgress && !syncStatus.error} {:else}

Data will load after a successful sync.

diff --git a/web-wallet/src/lib/contracts/__tests__/executeSend.spec.js b/web-wallet/src/lib/contracts/__tests__/executeSend.spec.js index f6588c1f6..ddcc08531 100644 --- a/web-wallet/src/lib/contracts/__tests__/executeSend.spec.js +++ b/web-wallet/src/lib/contracts/__tests__/executeSend.spec.js @@ -43,7 +43,10 @@ describe("executeSend", () => { await executeSend(...args); expect(walletStore.transfer).toHaveBeenCalledTimes(1); - expect(walletStore.transfer).toHaveBeenCalledWith(...args); + expect(walletStore.transfer).toHaveBeenCalledWith("abc", 1000, { + limit: 2, + price: 1, + }); expect(getLastTransactionHash).toHaveBeenCalledTimes(1); }); diff --git a/web-wallet/src/lib/contracts/executeSend.js b/web-wallet/src/lib/contracts/executeSend.js index 6bc3e4cc6..d9756ca92 100644 --- a/web-wallet/src/lib/contracts/executeSend.js +++ b/web-wallet/src/lib/contracts/executeSend.js @@ -4,7 +4,7 @@ import { walletStore } from "$lib/stores"; /** @type {(to: string, amount: number, gasPrice:number, gasLimit:number) => Promise} */ const executeSend = (to, amount, gasPrice, gasLimit) => walletStore - .transfer(to, amount, gasPrice, gasLimit) + .transfer(to, amount, { limit: gasLimit, price: gasPrice }) .then(getLastTransactionHash); export default executeSend; diff --git a/web-wallet/src/lib/dusk/components/ProgressBar/ProgressBar.svelte b/web-wallet/src/lib/dusk/components/ProgressBar/ProgressBar.svelte index 24f1672ab..ca65708a2 100644 --- a/web-wallet/src/lib/dusk/components/ProgressBar/ProgressBar.svelte +++ b/web-wallet/src/lib/dusk/components/ProgressBar/ProgressBar.svelte @@ -1,13 +1,33 @@ + + -
+
{ + return { + tweened: vi.fn((initialValue) => { + // Return a mock store that immediately sets the value for testing purposes + let value = initialValue; + const { set, subscribe } = writable(value); + return { + set: (/** @type {any} */ newValue) => { + // Simulate the tweening effect by setting the value immediately + set(newValue); + value = newValue; + }, + subscribe, + update: (/** @type {(arg0: any) => any} */ fn) => set(fn(value)), + }; + }), + }; +}); + describe("ProgressBar", () => { afterEach(cleanup); @@ -12,7 +32,7 @@ describe("ProgressBar", () => { expect(container.firstChild).toMatchSnapshot(); }); - it("renders the Stepper component with current percentage set as zero", () => { + it("renders the ProgressBar component with current percentage set as zero", () => { const { container } = render(ProgressBar, { props: { currentPercentage: 0 }, }); @@ -20,7 +40,7 @@ describe("ProgressBar", () => { expect(container.firstChild).toMatchSnapshot(); }); - it("re-renders the Stepper component when the current percentage property changes", async () => { + it("re-renders the ProgressBar component when the current percentage property changes", async () => { const { container, rerender } = render(ProgressBar, { props: { currentPercentage: 0 }, }); diff --git a/web-wallet/src/lib/dusk/components/__tests__/__snapshots__/ProgressBar.spec.js.snap b/web-wallet/src/lib/dusk/components/__tests__/__snapshots__/ProgressBar.spec.js.snap index 2cd3e49bf..b566a6455 100644 --- a/web-wallet/src/lib/dusk/components/__tests__/__snapshots__/ProgressBar.spec.js.snap +++ b/web-wallet/src/lib/dusk/components/__tests__/__snapshots__/ProgressBar.spec.js.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`ProgressBar > re-renders the Stepper component when the current percentage property changes 1`] = ` +exports[`ProgressBar > re-renders the ProgressBar component when the current percentage property changes 1`] = `
re-renders the Stepper component when the current percent
`; -exports[`ProgressBar > re-renders the Stepper component when the current percentage property changes 2`] = ` +exports[`ProgressBar > re-renders the ProgressBar component when the current percentage property changes 2`] = `
re-renders the Stepper component when the current percent
`; -exports[`ProgressBar > renders the ProgressBar component with no current percentage set 1`] = ` +exports[`ProgressBar > renders the ProgressBar component with current percentage set as zero 1`] = `
`; -exports[`ProgressBar > renders the Stepper component with current percentage set as zero 1`] = ` +exports[`ProgressBar > renders the ProgressBar component with no current percentage set 1`] = `
`; diff --git a/web-wallet/src/lib/dusk/number/createNumberFormatter.js b/web-wallet/src/lib/dusk/number/createNumberFormatter.js new file mode 100644 index 000000000..ce0b837bd --- /dev/null +++ b/web-wallet/src/lib/dusk/number/createNumberFormatter.js @@ -0,0 +1,17 @@ +/** + * Creates a locale aware number formatter + * + * @param {String} locale A BCP 47 language tag + * @param {Number|Undefined} digits The maximum fraction digits that should display + * @returns {(value: number | bigint) => string} + */ +const createNumberFormatter = (locale, digits = undefined) => { + const formatter = new Intl.NumberFormat(locale, { + maximumFractionDigits: digits, + style: "decimal", + }); + + return (value) => formatter.format(value); +}; + +export default createNumberFormatter; diff --git a/web-wallet/src/lib/dusk/number/index.js b/web-wallet/src/lib/dusk/number/index.js new file mode 100644 index 000000000..deba02192 --- /dev/null +++ b/web-wallet/src/lib/dusk/number/index.js @@ -0,0 +1 @@ +export { default as createNumberFormatter } from "./createNumberFormatter"; diff --git a/web-wallet/src/lib/services/wallet/__tests__/getWallet.spec.js b/web-wallet/src/lib/services/wallet/__tests__/getWallet.spec.js index a220c097f..4cbc08a4d 100644 --- a/web-wallet/src/lib/services/wallet/__tests__/getWallet.spec.js +++ b/web-wallet/src/lib/services/wallet/__tests__/getWallet.spec.js @@ -21,8 +21,6 @@ describe("getWallet", () => { [ "wasm", "seed", - "gasLimit", - "gasPrice", "constructor", "getBalance", "getPsks", diff --git a/web-wallet/src/lib/services/wallet/getWallet.js b/web-wallet/src/lib/services/wallet/getWallet.js index 49016adde..554efc586 100644 --- a/web-wallet/src/lib/services/wallet/getWallet.js +++ b/web-wallet/src/lib/services/wallet/getWallet.js @@ -3,12 +3,9 @@ import { Wallet } from "@dusk-network/dusk-wallet-js"; /** * Gets a `Wallet` instance. * @param {Uint8Array} seed - * @param {Number} [gasLimit=2900000000] - * @param {Number} [gasPrice=1] * @returns {Wallet} */ -const getWallet = (seed, gasLimit, gasPrice) => - new Wallet(Array.from(seed), gasLimit, gasPrice); +const getWallet = (seed) => new Wallet(Array.from(seed)); export default getWallet; diff --git a/web-wallet/src/lib/stores/__tests__/walletStore.spec.js b/web-wallet/src/lib/stores/__tests__/walletStore.spec.js index 467f28910..8a4d2c6d5 100644 --- a/web-wallet/src/lib/stores/__tests__/walletStore.spec.js +++ b/web-wallet/src/lib/stores/__tests__/walletStore.spec.js @@ -1,7 +1,7 @@ import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import { get } from "svelte/store"; import { keys } from "lamb"; -import { Wallet } from "@dusk-network/dusk-wallet-js"; +import { Gas, Wallet } from "@dusk-network/dusk-wallet-js"; import { addresses, transactions } from "$lib/mock-data"; import { rejectAfter, resolveAfter } from "$lib/dusk/test-helpers"; @@ -17,7 +17,16 @@ describe("walletStore", async () => { const balance = { maximum: 100, value: 1 }; const wallet = new Wallet([]); + const defaultSyncOptions = { + from: undefined, + onblock: expect.any(Function), + signal: expect.any(AbortSignal), + }; + const abortControllerSpy = vi.spyOn(AbortController.prototype, "abort"); + const blockHeightSpy = vi + .spyOn(Wallet, "networkBlockHeight", "get") + .mockResolvedValue(1536); const getBalanceSpy = vi .spyOn(Wallet.prototype, "getBalance") .mockResolvedValue(balance); @@ -54,9 +63,8 @@ describe("walletStore", async () => { value: 0, }, currentAddress: "", - error: null, initialized: false, - isSyncing: false, + syncStatus: { current: 0, error: null, isInProgress: false, last: 0 }, }; const initializedStore = { ...initialState, @@ -65,13 +73,14 @@ describe("walletStore", async () => { currentAddress: addresses[0], initialized: true, }; - const gas = { + const gasSettings = { limit: 30000000, price: 1, }; beforeEach(() => { abortControllerSpy.mockClear(); + blockHeightSpy.mockClear(); getBalanceSpy.mockClear(); getPsksSpy.mockClear(); historySpy.mockClear(); @@ -89,6 +98,7 @@ describe("walletStore", async () => { afterAll(() => { abortControllerSpy.mockRestore(); + blockHeightSpy.mockRestore(); getBalanceSpy.mockRestore(); getPsksSpy.mockRestore(); historySpy.mockRestore(); @@ -131,7 +141,12 @@ describe("walletStore", async () => { expect(get(walletStore)).toStrictEqual({ ...initializedStore, balance: initialState.balance, - error: new Error("Synchronization aborted"), + syncStatus: { + current: 0, + error: new Error("Synchronization aborted"), + isInProgress: false, + last: 0, + }, }); }); @@ -142,14 +157,13 @@ describe("walletStore", async () => { ...initialState, addresses: addresses, currentAddress: addresses[0], - error: null, initialized: true, - isSyncing: true, + syncStatus: { current: 0, error: null, isInProgress: true, last: 0 }, }); expect(getPsksSpy).toHaveBeenCalledTimes(1); expect(syncSpy).toHaveBeenCalledTimes(1); - expect(syncSpy).toHaveBeenCalledWith({ signal: expect.any(AbortSignal) }); + expect(syncSpy).toHaveBeenCalledWith(defaultSyncOptions); await vi.advanceTimersToNextTimerAsync(); @@ -161,14 +175,38 @@ describe("walletStore", async () => { expect(get(walletStore)).toStrictEqual(initializedStore); }); + it("should allow to start the sync from a specific block height after initializing the wallet", async () => { + const from = 9999; + + await walletStore.init(wallet, from); + + expect(get(walletStore)).toStrictEqual({ + ...initialState, + addresses: addresses, + currentAddress: addresses[0], + initialized: true, + syncStatus: { current: 0, error: null, isInProgress: true, last: 0 }, + }); + + expect(getPsksSpy).toHaveBeenCalledTimes(1); + expect(getBalanceSpy).not.toHaveBeenCalled(); + + await vi.advanceTimersToNextTimerAsync(); + + expect(syncSpy).toHaveBeenCalledTimes(1); + expect(syncSpy).toHaveBeenCalledWith({ ...defaultSyncOptions, from }); + expect(getBalanceSpy).toHaveBeenCalledTimes(1); + expect(getBalanceSpy).toHaveBeenCalledWith(addresses[0]); + expect(get(walletStore)).toStrictEqual(initializedStore); + }); + it("should set the sync error in the store if the sync fails", async () => { const storeWhileLoading = { ...initialState, addresses: addresses, currentAddress: addresses[0], - error: null, initialized: true, - isSyncing: true, + syncStatus: { current: 0, error: null, isInProgress: true, last: 0 }, }; const error = new Error("sync failed"); @@ -183,12 +221,11 @@ describe("walletStore", async () => { await vi.advanceTimersByTimeAsync(settleTime); expect(syncSpy).toHaveBeenCalledTimes(1); - expect(syncSpy).toHaveBeenCalledWith({ signal: expect.any(AbortSignal) }); + expect(syncSpy).toHaveBeenCalledWith(defaultSyncOptions); expect(getBalanceSpy).not.toHaveBeenCalled(); expect(get(walletStore)).toStrictEqual({ ...storeWhileLoading, - error, - isSyncing: false, + syncStatus: { current: 0, error, isInProgress: false, last: 0 }, }); }); @@ -201,7 +238,7 @@ describe("walletStore", async () => { await vi.advanceTimersToNextTimerAsync(); expect(syncSpy).toHaveBeenCalledTimes(1); - expect(syncSpy).toHaveBeenCalledWith({ signal: expect.any(AbortSignal) }); + expect(syncSpy).toHaveBeenCalledWith(defaultSyncOptions); syncSpy.mockClear(); @@ -215,7 +252,7 @@ describe("walletStore", async () => { await syncPromise1; expect(syncSpy).toHaveBeenCalledTimes(1); - expect(syncSpy).toHaveBeenCalledWith({ signal: expect.any(AbortSignal) }); + expect(syncSpy).toHaveBeenCalledWith(defaultSyncOptions); const syncPromise4 = walletStore.sync(); @@ -231,7 +268,7 @@ describe("walletStore", async () => { await walletStore.init(wallet); expect(syncSpy).toHaveBeenCalledTimes(1); - expect(syncSpy).toHaveBeenCalledWith({ signal: expect.any(AbortSignal) }); + expect(syncSpy).toHaveBeenCalledWith(defaultSyncOptions); walletStore.abortSync(); @@ -242,7 +279,7 @@ describe("walletStore", async () => { await walletStore.init(wallet); expect(syncSpy).toHaveBeenCalledTimes(1); - expect(syncSpy).toHaveBeenCalledWith({ signal: expect.any(AbortSignal) }); + expect(syncSpy).toHaveBeenCalledWith(defaultSyncOptions); walletStore.abortSync(); @@ -251,14 +288,14 @@ describe("walletStore", async () => { walletStore.sync(); expect(syncSpy).toHaveBeenCalledTimes(2); - expect(syncSpy).toHaveBeenCalledWith({ signal: expect.any(AbortSignal) }); + expect(syncSpy).toHaveBeenCalledWith(defaultSyncOptions); }); it("should do nothing if there is no sync in progress", async () => { await walletStore.init(wallet); expect(syncSpy).toHaveBeenCalledTimes(1); - expect(syncSpy).toHaveBeenCalledWith({ signal: expect.any(AbortSignal) }); + expect(syncSpy).toHaveBeenCalledWith(defaultSyncOptions); await vi.advanceTimersToNextTimerAsync(); @@ -299,14 +336,14 @@ describe("walletStore", async () => { ...initialState, addresses: addresses, currentAddress: addresses[0], - error: null, initialized: true, - isSyncing: true, + syncStatus: { current: 0, error: null, isInProgress: true, last: 0 }, }); expect(getPsksSpy).toHaveBeenCalledTimes(1); expect(getBalanceSpy).not.toHaveBeenCalled(); expect(syncSpy).toHaveBeenCalledTimes(1); + expect(syncSpy).toHaveBeenCalledWith(defaultSyncOptions); await vi.advanceTimersByTimeAsync(settleTime); @@ -315,10 +352,58 @@ describe("walletStore", async () => { expect(get(walletStore)).toStrictEqual(initializedStore); }); + it("should allow to start the sync from a specific block height after clearing and initializing the wallet", async () => { + getPsksSpy.mockClear(); + getBalanceSpy.mockClear(); + syncSpy.mockClear(); + walletStore.reset(); + + const from = 4276; + + await walletStore.clearLocalDataAndInit(wallet, from); + + expect(get(walletStore)).toStrictEqual({ + ...initialState, + addresses: addresses, + currentAddress: addresses[0], + initialized: true, + syncStatus: { + ...initialState.syncStatus, + error: null, + isInProgress: true, + }, + }); + + await vi.advanceTimersToNextTimerAsync(); + + expect(getPsksSpy).toHaveBeenCalledTimes(1); + expect(getBalanceSpy).toHaveBeenCalledTimes(1); + expect(getBalanceSpy).toHaveBeenCalledWith(addresses[0]); + expect(syncSpy).toHaveBeenCalledTimes(1); + expect(syncSpy).toHaveBeenCalledWith({ + ...defaultSyncOptions, + from, + }); + + await vi.advanceTimersToNextTimerAsync(); + + expect(get(walletStore)).toStrictEqual(initializedStore); + }); + + it("should expose a method to retrieve the current block height", async () => { + // This method needs to work even without a wallet instance + walletStore.reset(); + + await walletStore.getCurrentBlockHeight(); + + expect(blockHeightSpy).toHaveBeenCalledTimes(1); + }); + it("should expose a method to retrieve the stake info", async () => { await walletStore.getStakeInfo(); expect(syncSpy).toHaveBeenCalledTimes(1); + expect(syncSpy).toHaveBeenCalledWith(defaultSyncOptions); expect(stakeInfoSpy).toHaveBeenCalledTimes(1); expect(stakeInfoSpy).toHaveBeenCalledWith(currentAddress); expect(syncSpy.mock.invocationCallOrder[0]).toBeLessThan( @@ -331,7 +416,7 @@ describe("walletStore", async () => { /* eslint-disable camelcase */ has_key: false, has_staked: false, - /* eslint-enable camelcase */ + /* eslint-disable camelcase */ }); const expected = { @@ -345,6 +430,7 @@ describe("walletStore", async () => { const result = await walletStore.getStakeInfo(); expect(syncSpy).toHaveBeenCalledTimes(1); + expect(syncSpy).toHaveBeenCalledWith(defaultSyncOptions); expect(stakeInfoSpy).toHaveBeenCalledTimes(1); expect(stakeInfoSpy).toHaveBeenCalledWith(currentAddress); expect(syncSpy.mock.invocationCallOrder[0]).toBeLessThan( @@ -357,6 +443,7 @@ describe("walletStore", async () => { await walletStore.getTransactionsHistory(); expect(syncSpy).toHaveBeenCalledTimes(1); + expect(syncSpy).toHaveBeenCalledWith(defaultSyncOptions); expect(historySpy).toHaveBeenCalledTimes(1); expect(historySpy).toHaveBeenCalledWith(currentAddress); expect(syncSpy.mock.invocationCallOrder[0]).toBeLessThan( @@ -378,6 +465,7 @@ describe("walletStore", async () => { await walletStore.setCurrentAddress(addresses[1]); expect(syncSpy).toHaveBeenCalledTimes(1); + expect(syncSpy).toHaveBeenCalledWith(defaultSyncOptions); expect(get(walletStore).currentAddress).toBe(addresses[1]); expect(setCurrentAddressSpy.mock.invocationCallOrder[0]).toBeLessThan( syncSpy.mock.invocationCallOrder[0] @@ -394,14 +482,14 @@ describe("walletStore", async () => { }); it("should expose a method to allow to stake an amount of Dusk", async () => { - await walletStore.stake(10, gas.price, gas.limit); - - expect(wallet.gasLimit).toBe(gas.limit); - expect(wallet.gasPrice).toBe(gas.price); + await walletStore.stake(10, gasSettings); expect(syncSpy).toHaveBeenCalledTimes(2); + expect(syncSpy).toHaveBeenNthCalledWith(1, defaultSyncOptions); + expect(syncSpy).toHaveBeenNthCalledWith(2, defaultSyncOptions); expect(stakeSpy).toHaveBeenCalledTimes(1); - expect(stakeSpy).toHaveBeenCalledWith(currentAddress, 10); + expect(stakeSpy).toHaveBeenCalledWith(currentAddress, 10, gasSettings); + expect(stakeSpy.mock.calls[0][2]).toBeInstanceOf(Gas); expect(syncSpy.mock.invocationCallOrder[0]).toBeLessThan( stakeSpy.mock.invocationCallOrder[0] ); @@ -410,19 +498,39 @@ describe("walletStore", async () => { ); }); - it("should expose a method to allow to transfer an amount of Dusk", async () => { - await walletStore.transfer(addresses[1], 10, gas.price, gas.limit); + it("should expose a method to manually start a synchronization", async () => { + await walletStore.sync(); + + expect(syncSpy).toHaveBeenCalledTimes(1); + expect(syncSpy).toHaveBeenCalledWith(defaultSyncOptions); + }); + + it("should allow to start a synchronization from a specific block height", async () => { + const from = 7654; + + await walletStore.sync(from); + + expect(syncSpy).toHaveBeenCalledTimes(1); + expect(syncSpy).toHaveBeenCalledWith({ + ...defaultSyncOptions, + from, + }); + }); - expect(wallet.gasLimit).toBe(gas.limit); - expect(wallet.gasPrice).toBe(gas.price); + it("should expose a method to allow to transfer an amount of Dusk", async () => { + await walletStore.transfer(addresses[1], 10, gasSettings); expect(syncSpy).toHaveBeenCalledTimes(2); + expect(syncSpy).toHaveBeenNthCalledWith(1, defaultSyncOptions); + expect(syncSpy).toHaveBeenNthCalledWith(2, defaultSyncOptions); expect(transferSpy).toHaveBeenCalledTimes(1); expect(transferSpy).toHaveBeenCalledWith( currentAddress, addresses[1], - 10 + 10, + gasSettings ); + expect(transferSpy.mock.calls[0][3]).toBeInstanceOf(Gas); expect(syncSpy.mock.invocationCallOrder[0]).toBeLessThan( transferSpy.mock.invocationCallOrder[0] ); @@ -432,14 +540,14 @@ describe("walletStore", async () => { }); it("should expose a method to allow to unstake the current address", async () => { - await walletStore.unstake(gas.price, gas.limit); - - expect(wallet.gasLimit).toBe(gas.limit); - expect(wallet.gasPrice).toBe(gas.price); + await walletStore.unstake(gasSettings); expect(syncSpy).toHaveBeenCalledTimes(2); + expect(syncSpy).toHaveBeenNthCalledWith(1, defaultSyncOptions); + expect(syncSpy).toHaveBeenNthCalledWith(2, defaultSyncOptions); expect(unstakeSpy).toHaveBeenCalledTimes(1); - expect(unstakeSpy).toHaveBeenCalledWith(currentAddress); + expect(unstakeSpy).toHaveBeenCalledWith(currentAddress, gasSettings); + expect(unstakeSpy.mock.calls[0][1]).toBeInstanceOf(Gas); expect(syncSpy.mock.invocationCallOrder[0]).toBeLessThan( unstakeSpy.mock.invocationCallOrder[0] ); @@ -449,14 +557,19 @@ describe("walletStore", async () => { }); it("should expose a method to allow to withdraw a reward", async () => { - await walletStore.withdrawReward(gas.price, gas.limit); - - expect(wallet.gasLimit).toBe(gas.limit); - expect(wallet.gasPrice).toBe(gas.price); + await walletStore.withdrawReward(gasSettings); expect(syncSpy).toHaveBeenCalledTimes(2); + expect(syncSpy).toHaveBeenNthCalledWith(1, defaultSyncOptions); + expect(syncSpy).toHaveBeenNthCalledWith(2, { + ...defaultSyncOptions, + }); expect(withdrawRewardSpy).toHaveBeenCalledTimes(1); - expect(withdrawRewardSpy).toHaveBeenCalledWith(currentAddress); + expect(withdrawRewardSpy).toHaveBeenCalledWith( + currentAddress, + gasSettings + ); + expect(withdrawRewardSpy.mock.calls[0][1]).toBeInstanceOf(Gas); expect(syncSpy.mock.invocationCallOrder[0]).toBeLessThan( withdrawRewardSpy.mock.invocationCallOrder[0] ); @@ -482,7 +595,7 @@ describe("walletStore", async () => { keys(operationsMap).forEach((operation) => { const spy = operationsMap[operation]; - it("should return a resolved promise with the operation result if an operation succeeds", async () => { + it("should return a resolved promise with the operation result if an operation succeeds even if the last sync fails", async () => { await walletStore.init(wallet); await vi.advanceTimersToNextTimerAsync(); @@ -491,11 +604,15 @@ describe("walletStore", async () => { .mockRejectedValueOnce(fakeSyncError); spy.mockResolvedValueOnce(fakeSuccess); - expect(get(walletStore).error).toBe(null); + expect(get(walletStore).syncStatus.error).toBe(null); // @ts-ignore it's a mock and we don't care to pass the correct arguments expect(await walletStore[operation]()).toBe(fakeSuccess); - expect(get(walletStore).error).toBe(fakeSyncError); + expect(get(walletStore).syncStatus.error).toBe(fakeSyncError); + expect(syncSpy).toHaveBeenCalledTimes(3); + expect(syncSpy).toHaveBeenNthCalledWith(1, defaultSyncOptions); + expect(syncSpy).toHaveBeenNthCalledWith(2, defaultSyncOptions); + expect(syncSpy).toHaveBeenNthCalledWith(3, defaultSyncOptions); walletStore.reset(); }); @@ -509,14 +626,18 @@ describe("walletStore", async () => { .mockRejectedValueOnce(fakeSyncError); spy.mockRejectedValueOnce(fakeFailure); - expect(get(walletStore).error).toBe(null); + expect(get(walletStore).syncStatus.error).toBe(null); // @ts-ignore it's a mock and we don't care to pass the correct arguments expect(walletStore[operation]()).rejects.toThrowError(fakeFailure); await vi.advanceTimersToNextTimerAsync(); - expect(get(walletStore).error).toBe(fakeSyncError); + expect(get(walletStore).syncStatus.error).toBe(fakeSyncError); + expect(syncSpy).toHaveBeenCalledTimes(3); + expect(syncSpy).toHaveBeenNthCalledWith(1, defaultSyncOptions); + expect(syncSpy).toHaveBeenNthCalledWith(2, defaultSyncOptions); + expect(syncSpy).toHaveBeenNthCalledWith(3, defaultSyncOptions); }); }); }); diff --git a/web-wallet/src/lib/stores/stores.d.ts b/web-wallet/src/lib/stores/stores.d.ts index 4f9abe431..30f0eb2a3 100644 --- a/web-wallet/src/lib/stores/stores.d.ts +++ b/web-wallet/src/lib/stores/stores.d.ts @@ -27,6 +27,11 @@ type SettingsStoreContent = { type SettingsStore = Writable & { reset: () => void }; +type GasSettings = { + limit: number; + price: number; +}; + type TransactionsStoreContent = { transactions: Transaction[] }; type TransactionsStore = Readable; @@ -40,11 +45,15 @@ type WalletStoreContent = { maximum: number; value: number; }; + syncStatus: { + isInProgress: boolean; + current: number; + last: number; + error: Error | null; + }; currentAddress: string; - error: Error | null; initialized: boolean; addresses: string[]; - isSyncing: boolean; }; type WalletStoreServices = { @@ -52,14 +61,19 @@ type WalletStoreServices = { clearLocalData: () => Promise; - clearLocalDataAndInit: (wallet: Wallet) => Promise; + clearLocalDataAndInit: ( + wallet: Wallet, + syncFromBlock?: number + ) => Promise; + + getCurrentBlockHeight: () => Promise; getStakeInfo: () => Promise & ReturnType; // The return type apparently is not in a promise here getTransactionsHistory: () => Promise>; - init: (wallet: Wallet) => Promise; + init: (wallet: Wallet, syncFromBlock?: number) => Promise; reset: () => void; @@ -67,27 +81,23 @@ type WalletStoreServices = { stake: ( amount: number, - gasPrice: number, - gasLimit: number + gasSettings: GasSettings ) => Promise & ReturnType; - sync: () => Promise; + sync: (from?: number) => Promise; transfer: ( to: string, amount: number, - gasPrice: number, - gasLimit: number + gasSettings: GasSettings ) => Promise & ReturnType; unstake: ( - gasPrice: number, - gasLimit: number + gasSettings: GasSettings ) => Promise & ReturnType; withdrawReward: ( - gasPrice: number, - gasLimit: number + gasSettings: GasSettings ) => Promise & ReturnType; }; diff --git a/web-wallet/src/lib/stores/walletStore.js b/web-wallet/src/lib/stores/walletStore.js index 3539574f4..371a1c52b 100644 --- a/web-wallet/src/lib/stores/walletStore.js +++ b/web-wallet/src/lib/stores/walletStore.js @@ -1,13 +1,7 @@ import { get, writable } from "svelte/store"; -import { getKey, uniquesBy } from "lamb"; - -/** - * @typedef {import("@dusk-network/dusk-wallet-js").Wallet} Wallet - */ - -/** - * @typedef {WalletStoreServices["getTransactionsHistory"]} GetTransactionsHistory - */ +import { getKey, setKey, uniquesBy } from "lamb"; +import { Gas, Wallet } from "@dusk-network/dusk-wallet-js"; +import settingsStore from "./settingsStore"; /** @type {AbortController} */ let syncController; @@ -28,9 +22,8 @@ const initialState = { value: 0, }, currentAddress: "", - error: null, initialized: false, - isSyncing: false, + syncStatus: { current: 0, error: null, isInProgress: false, last: 0 }, }; const walletStore = writable(initialState); @@ -55,17 +48,33 @@ const getCurrentAddress = () => get(walletStore).currentAddress; /** @type {(action: (...args: any[]) => Promise) => Promise} */ const syncedAction = (action) => sync().then(action).finally(sync); +async function updateAfterSync() { + const store = get(walletStore); + + // @ts-expect-error + const balance = await walletInstance.getBalance(store.currentAddress); + + set({ + ...store, + balance, + syncStatus: initialState.syncStatus, + }); +} + const abortSync = () => { syncPromise && syncController?.abort(); syncPromise = null; }; -/** @type {() => Promise} */ +/** @type {WalletStoreServices["clearLocalData"]} */ const clearLocalData = async () => walletInstance?.reset(); -/** @type {(wallet: Wallet) => Promise} */ -const clearLocalDataAndInit = (wallet) => - wallet.reset().then(() => init(wallet)); +/** @type {WalletStoreServices["clearLocalDataAndInit"]} */ +const clearLocalDataAndInit = (wallet, syncFromBlock) => + wallet.reset().then(() => init(wallet, syncFromBlock)); + +/** @type {WalletStoreServices["getCurrentBlockHeight"]} */ +const getCurrentBlockHeight = async () => Wallet.networkBlockHeight; /** @type {WalletStoreServices["getStakeInfo"]} */ const getStakeInfo = async () => @@ -74,35 +83,22 @@ const getStakeInfo = async () => .then(() => walletInstance.stakeInfo(getCurrentAddress())) .then(fixStakeInfo); -/** @type {GetTransactionsHistory} */ - +/** @type {WalletStoreServices["getTransactionsHistory"]} */ const getTransactionsHistory = async () => sync() // @ts-expect-error .then(() => walletInstance.history(getCurrentAddress())) .then(uniquesById); +/** @type {WalletStoreServices["reset"]} */ function reset() { abortSync(); walletInstance = null; set(initialState); } -async function updateAfterSync() { - const store = get(walletStore); - - // @ts-expect-error - const balance = await walletInstance.getBalance(store.currentAddress); - - set({ - ...store, - balance, - isSyncing: false, - }); -} - -/** @param {Wallet} wallet */ -async function init(wallet) { +/** @type {WalletStoreServices["init"]} */ +async function init(wallet, syncFromBlock) { walletInstance = wallet; const addresses = await walletInstance.getPsks(); @@ -114,7 +110,10 @@ async function init(wallet) { currentAddress, initialized: true, }); - sync(); + + sync(syncFromBlock).then(() => { + settingsStore.update(setKey("userId", currentAddress)); + }); } /** @type {WalletStoreServices["setCurrentAddress"]} */ @@ -122,26 +121,25 @@ async function setCurrentAddress(address) { const store = get(walletStore); return store.addresses.includes(address) - ? Promise.resolve(set({ ...store, currentAddress: address })).then(sync) + ? Promise.resolve(set({ ...store, currentAddress: address })).then(() => + sync() + ) : Promise.reject(new Error("The received address is not in the list")); } /** @type {WalletStoreServices["stake"]} */ - -const stake = async (amount, gasPrice, gasLimit) => +const stake = async (amount, gasSettings) => syncedAction(() => { // @ts-expect-error - walletInstance.gasLimit = gasLimit; - - // @ts-expect-error - walletInstance.gasPrice = gasPrice; - - // @ts-expect-error - return walletInstance.stake(getCurrentAddress(), amount); + return walletInstance.stake( + getCurrentAddress(), + amount, + new Gas(gasSettings) + ); }); /** @type {WalletStoreServices["sync"]} */ -function sync() { +function sync(from) { if (!walletInstance) { throw new Error("No wallet instance to sync"); } @@ -149,11 +147,37 @@ function sync() { if (!syncPromise) { const store = get(walletStore); - set({ ...store, error: null, isSyncing: true }); + set({ + ...store, + syncStatus: { + current: 0, + error: null, + isInProgress: true, + last: 0, + }, + }); syncController = new AbortController(); + + const syncOptions = { + from, + /** @type {(current: number, last: number) => void} */ + onblock: (current, last) => { + set({ + ...store, + syncStatus: { + current, + error: null, + isInProgress: true, + last, + }, + }); + }, + signal: syncController.signal, + }; + syncPromise = walletInstance - .sync({ signal: syncController.signal }) + .sync(syncOptions) .then(() => { if (syncController.signal.aborted) { throw new Error("Synchronization aborted"); @@ -161,7 +185,15 @@ function sync() { }) .then(updateAfterSync) .catch((error) => { - set({ ...store, error, isSyncing: false }); + set({ + ...store, + syncStatus: { + current: 0, + error, + isInProgress: false, + last: 0, + }, + }); }) .finally(() => { syncPromise = null; @@ -172,42 +204,32 @@ function sync() { } /** @type {WalletStoreServices["transfer"]} */ -const transfer = async (to, amount, gasPrice, gasLimit) => +const transfer = async (to, amount, gasSettings) => syncedAction(() => { // @ts-expect-error - walletInstance.gasLimit = gasLimit; - - // @ts-expect-error - walletInstance.gasPrice = gasPrice; - - // @ts-expect-error - return walletInstance.transfer(getCurrentAddress(), to, amount); + return walletInstance.transfer( + getCurrentAddress(), + to, + amount, + new Gas(gasSettings) + ); }); /** @type {WalletStoreServices["unstake"]} */ -const unstake = async (gasPrice, gasLimit) => +const unstake = async (gasSettings) => syncedAction(() => { // @ts-expect-error - walletInstance.gasLimit = gasLimit; - - // @ts-expect-error - walletInstance.gasPrice = gasPrice; - - // @ts-expect-error - return walletInstance.unstake(getCurrentAddress()); + return walletInstance.unstake(getCurrentAddress(), new Gas(gasSettings)); }); /** @type {WalletStoreServices["withdrawReward"]} */ -const withdrawReward = async (gasPrice, gasLimit) => +const withdrawReward = async (gasSettings) => syncedAction(() => { // @ts-expect-error - walletInstance.gasLimit = gasLimit; - - // @ts-expect-error - walletInstance.gasPrice = gasPrice; - - // @ts-expect-error - return walletInstance.withdrawReward(getCurrentAddress()); + return walletInstance.withdrawReward( + getCurrentAddress(), + new Gas(gasSettings) + ); }); /** @type {WalletStore} */ @@ -215,6 +237,7 @@ export default { abortSync, clearLocalData, clearLocalDataAndInit, + getCurrentBlockHeight, getStakeInfo, getTransactionsHistory, init, diff --git a/web-wallet/src/lib/wallet/initializeWallet.js b/web-wallet/src/lib/wallet/initializeWallet.js index 35395e05c..12749cd15 100644 --- a/web-wallet/src/lib/wallet/initializeWallet.js +++ b/web-wallet/src/lib/wallet/initializeWallet.js @@ -1,20 +1,18 @@ import { settingsStore, walletStore } from "$lib/stores"; import { getSeedFromMnemonic } from "$lib/wallet"; import { getWallet } from "$lib/services/wallet"; -import { setKey } from "lamb"; -/** @param {string[]} mnemonicPhrase */ -async function initializeWallet(mnemonicPhrase) { +/** + * @param {string[]} mnemonicPhrase + * @param {number | undefined} syncFrom + */ +async function initializeWallet(mnemonicPhrase, syncFrom = undefined) { settingsStore.reset(); const mnemonic = mnemonicPhrase.join(" "); const seed = getSeedFromMnemonic(mnemonic); const wallet = getWallet(seed); - const defaultAddress = (await wallet.getPsks())[0]; - - await walletStore.clearLocalDataAndInit(wallet); - - settingsStore.update(setKey("userId", defaultAddress)); + walletStore.clearLocalDataAndInit(wallet, syncFrom); } export default initializeWallet; diff --git a/web-wallet/src/routes/(app)/__tests__/layout.spec.js b/web-wallet/src/routes/(app)/__tests__/layout.spec.js index 7e40f7efb..f53c9d210 100644 --- a/web-wallet/src/routes/(app)/__tests__/layout.spec.js +++ b/web-wallet/src/routes/(app)/__tests__/layout.spec.js @@ -42,7 +42,7 @@ describe("App layout.js", () => { }); it("should do nothing otherwise", async () => { - await walletStore.init(new Wallet([], 0, 0)); + await walletStore.init(new Wallet([])); // @ts-ignore await expect(load()).resolves.toBe(void 0); diff --git a/web-wallet/src/routes/(app)/dashboard/+layout.svelte b/web-wallet/src/routes/(app)/dashboard/+layout.svelte index 439990dc0..19a4c5c1c 100644 --- a/web-wallet/src/routes/(app)/dashboard/+layout.svelte +++ b/web-wallet/src/routes/(app)/dashboard/+layout.svelte @@ -19,7 +19,7 @@ export let data; /** @type {string} */ - let syncStatus = ""; + let syncStatusLabel = ""; /** @type {string} */ let networkStatusIconPath = ""; @@ -37,19 +37,19 @@ const { currency, language } = $settingsStore; $: ({ network } = $settingsStore); - $: ({ balance, currentAddress, addresses, isSyncing, error } = $walletStore); - $: if (isSyncing) { + $: ({ balance, currentAddress, addresses, syncStatus } = $walletStore); + $: if (syncStatus.isInProgress) { iconVariant = "warning"; networkStatusIconPath = mdiTimerSand; - syncStatus = `Syncing with Dusk ${network}`; - } else if (error) { + syncStatusLabel = ""; + } else if (syncStatus.error) { iconVariant = "error"; networkStatusIconPath = mdiAlertOutline; - syncStatus = `Dusk ${network} - Sync Failed`; + syncStatusLabel = `Dusk ${network} - Sync Failed`; } else { iconVariant = "success"; networkStatusIconPath = mdiLink; - syncStatus = `Dusk ${network}`; + syncStatusLabel = `Dusk ${network}`; } @@ -83,22 +83,36 @@ `; diff --git a/web-wallet/src/routes/(welcome)/setup/restore/__tests__/page.spec.js b/web-wallet/src/routes/(welcome)/setup/restore/__tests__/page.spec.js index da9104b98..d86d8c0ec 100644 --- a/web-wallet/src/routes/(welcome)/setup/restore/__tests__/page.spec.js +++ b/web-wallet/src/routes/(welcome)/setup/restore/__tests__/page.spec.js @@ -152,7 +152,13 @@ describe("Restore", async () => { await fireEvent.click(getByRole("button", { name: "Next" })); expect(loginInfoStorage.get()).toBeNull(); - // Restore Wallet step + // Block Height Step + await fireEvent.click(getByRole("button", { name: "Next" })); + + // Syncing Step + await fireEvent.click(getByRole("button", { name: "Next" })); + + // All Done Step await fireEvent.click(getByRole("button", { name: "Next" })); await vi.waitUntil(() => gotoSpy.mock.calls.length > 0); @@ -161,7 +167,7 @@ describe("Restore", async () => { expect(getWalletSpy).toHaveBeenCalledTimes(1); expect(getWalletSpy).toHaveBeenCalledWith(seed); expect(clearAndInitSpy).toHaveBeenCalledTimes(1); - expect(clearAndInitSpy).toHaveBeenCalledWith(expect.any(Wallet)); + expect(clearAndInitSpy).toHaveBeenCalledWith(expect.any(Wallet), 0); expect(gotoSpy).toHaveBeenCalledTimes(1); expect(gotoSpy).toHaveBeenCalledWith("/dashboard"); }); @@ -197,15 +203,22 @@ describe("Restore", async () => { expect(loginInfoStorage.get()).not.toBeNull(); }); - // Restore Wallet step + // Block Height step await fireEvent.click(getByRole("button", { name: "Next" })); + + // Network Sync step + await fireEvent.click(getByRole("button", { name: "Next" })); + + // All Done step + await fireEvent.click(getByRole("button", { name: "Next" })); + await vi.waitUntil(() => gotoSpy.mock.calls.length > 0); expect(settingsResetSpy).toHaveBeenCalledTimes(1); expect(getWalletSpy).toHaveBeenCalledTimes(1); expect(getWalletSpy).toHaveBeenCalledWith(seed); expect(clearAndInitSpy).toHaveBeenCalledTimes(1); - expect(clearAndInitSpy).toHaveBeenCalledWith(expect.any(Wallet)); + expect(clearAndInitSpy).toHaveBeenCalledWith(expect.any(Wallet), 0); expect(gotoSpy).toHaveBeenCalledTimes(1); expect(gotoSpy).toHaveBeenCalledWith("/dashboard"); }); diff --git a/web-wallet/src/routes/components-showcase/+page.svelte b/web-wallet/src/routes/components-showcase/+page.svelte index bdb4f14a1..874475173 100644 --- a/web-wallet/src/routes/components-showcase/+page.svelte +++ b/web-wallet/src/routes/components-showcase/+page.svelte @@ -8,6 +8,7 @@ import Buttons from "./Buttons.svelte"; import Cards from "./Cards.svelte"; import Checkboxes from "./Checkboxes.svelte"; + import CopyField from "./CopyField.svelte"; import DashboardNavs from "./DashboardNavs.svelte"; import ExclusiveChoices from "./ExclusiveChoices.svelte"; import MigrateContract from "./MigrateContract.svelte"; @@ -30,6 +31,7 @@ Buttons: Buttons, Cards: Cards, Checkboxes: Checkboxes, + "Copy Field": CopyField, "Dashboard Navs": DashboardNavs, "Exclusive Choices": ExclusiveChoices, "Migrate Contract": MigrateContract, diff --git a/web-wallet/src/routes/components-showcase/CopyField.svelte b/web-wallet/src/routes/components-showcase/CopyField.svelte new file mode 100644 index 000000000..d1de2d270 --- /dev/null +++ b/web-wallet/src/routes/components-showcase/CopyField.svelte @@ -0,0 +1,19 @@ + + +
+ + + +
diff --git a/web-wallet/src/style/dusk/language.css b/web-wallet/src/style/dusk/language.css index 821f6e5fd..3e2115e67 100644 --- a/web-wallet/src/style/dusk/language.css +++ b/web-wallet/src/style/dusk/language.css @@ -88,9 +88,9 @@ /* Progress bars */ - --progress-bar-height: 0.3125em; + --progress-bar-height: 0.6125em; --progress-bg-color: var(--primary-color); - --progress-filler-color: var(--success-color); + --progress-filler-color: var(--secondary-color); /* Steppers */ diff --git a/web-wallet/vite-setup.js b/web-wallet/vite-setup.js index efdcc94ba..30bb82df0 100644 --- a/web-wallet/vite-setup.js +++ b/web-wallet/vite-setup.js @@ -27,7 +27,10 @@ Object.defineProperty(window, "litIssuedWarnings", { }); // Mocking the Wallet -vi.doMock("@dusk-network/dusk-wallet-js", () => ({ Wallet })); +vi.doMock("@dusk-network/dusk-wallet-js", async (importOriginal) => ({ + ...(await importOriginal()), + Wallet, +})); /* * Mocking deprecated `atob` and `btoa` functions in Node.