Skip to content

Commit

Permalink
Merge pull request #3170 from dusk-network/feature-3156
Browse files Browse the repository at this point in the history
web-wallet: Update sync and cache to handle cases of rejected blocks
  • Loading branch information
ascartabelli authored Dec 16, 2024
2 parents 8c4ec10 + cb6a3ff commit 6c28bcb
Show file tree
Hide file tree
Showing 16 changed files with 423 additions and 163 deletions.
2 changes: 2 additions & 0 deletions web-wallet/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Add notice for stake maturity [#2981]
- Add capability to maintain cache consistency in case of rejected blocks [#3156]

### Changed

Expand Down Expand Up @@ -431,6 +432,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#3099]: https://github.com/dusk-network/rusk/issues/3099
[#3113]: https://github.com/dusk-network/rusk/issues/3113
[#3129]: https://github.com/dusk-network/rusk/issues/3129
[#3156]: https://github.com/dusk-network/rusk/issues/3156
[#3160]: https://github.com/dusk-network/rusk/issues/3160

<!-- VERSIONS -->
Expand Down
8 changes: 4 additions & 4 deletions web-wallet/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion web-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"fake-indexeddb": "6.0.0",
"jsdom": "24.1.0",
"jsdom-worker": "0.3.0",
"lamb-types": "0.61.11",
"lamb-types": "0.61.12",
"postcss-nested": "6.0.1",
"prettier": "3.3.2",
"prettier-plugin-svelte": "3.2.5",
Expand Down
2 changes: 1 addition & 1 deletion web-wallet/src/__mocks__/AddressSyncer.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class AddressSyncerMock extends AddressSyncer {
return;
}

/** @type {WalletCacheSyncInfo} */
/** @type {{ blockHeight: bigint, bookmark: bigint }} */
const syncInfo = {
blockHeight: 50n * BigInt(currentChunk),
bookmark: 100n * BigInt(currentChunk),
Expand Down
11 changes: 10 additions & 1 deletion web-wallet/src/lib/mock-data/cache-sync-info.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
/** @type {WalletCacheSyncInfo[]} */
export default [{ blockHeight: 12486n, bookmark: 35n }];
export default [
{
block: {
hash: "0c23c8e3a6532f8b3d1f409e156123d793e17f4377544a1c3bfd12c0be30cd6f",
height: 12486n,
},
bookmark: 35n,
lastFinalizedBlockHeight: 10435n,
},
];
156 changes: 108 additions & 48 deletions web-wallet/src/lib/stores/__tests__/networkStore.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import {
afterAll,
afterEach,
beforeEach,
describe,
expect,
it,
vi,
} from "vitest";
import { get } from "svelte/store";

import {
Expand All @@ -14,16 +22,19 @@ describe("Network store", async () => {
const blockHeightSpy = vi
.spyOn(Network.prototype, "blockHeight", "get")
.mockResolvedValue(blockHeight);
const networkQuerySpy = vi.spyOn(Network.prototype, "query");

afterEach(() => {
connectSpy.mockClear();
disconnectSpy.mockClear();
networkQuerySpy.mockClear();
});

afterAll(() => {
connectSpy.mockRestore();
disconnectSpy.mockRestore();
blockHeightSpy.mockRestore();
networkQuerySpy.mockRestore();
});

it("should build the network with the correct URL and expose a name for it", async () => {
Expand Down Expand Up @@ -69,77 +80,126 @@ describe("Network store", async () => {
expect(network).toBeInstanceOf(Network);
});

it("should expose a method to disconnect from the network and update the store's connection status", async () => {
const store = (await import("..")).networkStore;
describe("Connection and disconnection", () => {
it("should expose a method to disconnect from the network and update the store's connection status", async () => {
const store = (await import("..")).networkStore;

await store.connect();
await store.connect();

expect(get(store).connected).toBe(true);
expect(get(store).connected).toBe(true);

await store.disconnect();
await store.disconnect();

expect(disconnectSpy).toHaveBeenCalledTimes(1);
expect(get(store).connected).toBe(false);
});
expect(disconnectSpy).toHaveBeenCalledTimes(1);
expect(get(store).connected).toBe(false);
});

it("should not try to connect again to the network if it's already connected", async () => {
const store = (await import("..")).networkStore;
it("should not try to connect again to the network if it's already connected", async () => {
const store = (await import("..")).networkStore;

const network = await store.connect();
const network = await store.connect();

expect(connectSpy).toHaveBeenCalledTimes(1);
expect(get(store).connected).toBe(true);
expect(connectSpy).toHaveBeenCalledTimes(1);
expect(get(store).connected).toBe(true);

connectSpy.mockClear();
connectSpy.mockClear();

const network2 = await store.connect();
const network2 = await store.connect();

expect(network2).toBe(network);
expect(connectSpy).not.toHaveBeenCalled();
expect(get(store).connected).toBe(true);
expect(network2).toBe(network);
expect(connectSpy).not.toHaveBeenCalled();
expect(get(store).connected).toBe(true);
});
});

it("should expose a service method to retrieve the current block height", async () => {
const store = (await import("..")).networkStore;
describe("Service methods", () => {
/** @type {NetworkStore} */
let store;

await expect(store.getCurrentBlockHeight()).resolves.toBe(blockHeight);
});
beforeEach(async () => {
store = (await import("..")).networkStore;

it("should expose a service method to retrieve a `AccountSyncer` for the network", async () => {
const store = (await import("..")).networkStore;
// we check that every service method takes
// care of connecting to the network when necessary
await store.disconnect();

await store.disconnect();
expect(get(store).connected).toBe(false);
expect(get(store).connected).toBe(false);
});

connectSpy.mockClear();
it("should expose a service method to check if a block with the given height and hash exists on the network", async () => {
networkQuerySpy
.mockResolvedValueOnce({ checkBlock: true })
.mockResolvedValueOnce({ checkBlock: false });

const syncer = await store.getAccountSyncer();
await expect(store.checkBlock(12n, "some-hash")).resolves.toBe(true);
await expect(store.checkBlock(12n, "some-hash")).resolves.toBe(false);
});

expect(connectSpy).toHaveBeenCalledTimes(1);
expect(syncer).toBeInstanceOf(AccountSyncer);
it("should expose a service method to retrieve a `AccountSyncer` for the network", async () => {
connectSpy.mockClear();

// check that the cached network is used
await store.getAccountSyncer();
expect(connectSpy).toHaveBeenCalledTimes(1);
expect(syncer).toBeInstanceOf(AccountSyncer);
});
const syncer = await store.getAccountSyncer();

it("should expose a service method to retrieve a `AddressSyncer` for the network", async () => {
const store = (await import("..")).networkStore;
expect(connectSpy).toHaveBeenCalledTimes(1);
expect(syncer).toBeInstanceOf(AccountSyncer);

await store.disconnect();
expect(get(store).connected).toBe(false);
// check that the cached network is used
await store.getAccountSyncer();
expect(connectSpy).toHaveBeenCalledTimes(1);
expect(syncer).toBeInstanceOf(AccountSyncer);
});

connectSpy.mockClear();
it("should expose a service method to retrieve a `AddressSyncer` for the network", async () => {
connectSpy.mockClear();

const syncer = await store.getAddressSyncer();
const syncer = await store.getAddressSyncer();

expect(connectSpy).toHaveBeenCalledTimes(1);
expect(syncer).toBeInstanceOf(AddressSyncer);
expect(connectSpy).toHaveBeenCalledTimes(1);
expect(syncer).toBeInstanceOf(AddressSyncer);

// check that the cached network is used
await store.getAddressSyncer();
expect(connectSpy).toHaveBeenCalledTimes(1);
expect(syncer).toBeInstanceOf(AddressSyncer);
// check that the cached network is used
await store.getAddressSyncer();
expect(connectSpy).toHaveBeenCalledTimes(1);
expect(syncer).toBeInstanceOf(AddressSyncer);
});

it("should expose a method to retrieve a block hash by its height and return an empty string if the block is not found", async () => {
const expectedHash = "some-block-hash";

networkQuerySpy.mockResolvedValueOnce({
block: { header: { hash: expectedHash } },
});

await expect(store.getBlockHashByHeight(123n)).resolves.toStrictEqual(
expectedHash
);

networkQuerySpy.mockResolvedValueOnce({ block: null });

await expect(store.getBlockHashByHeight(123n)).resolves.toBe("");
});

it("should expose a service method to retrieve the current block height", async () => {
await expect(store.getCurrentBlockHeight()).resolves.toBe(blockHeight);
});

it("should expose a method to retrieve the last finalized block height and return `0n` if the block is not found", async () => {
const height = 123;

networkQuerySpy.mockResolvedValueOnce({
lastBlockPair: {
// eslint-disable-next-line camelcase
json: { last_finalized_block: [height, "some-block-hash"] },
},
});

await expect(store.getLastFinalizedBlockHeight()).resolves.toStrictEqual(
BigInt(height)
);

networkQuerySpy.mockResolvedValueOnce({ lastBlockPair: null });

await expect(store.getLastFinalizedBlockHeight()).resolves.toBe(0n);
});
});
});
22 changes: 10 additions & 12 deletions web-wallet/src/lib/stores/__tests__/walletStore.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import walletCache from "$lib/wallet-cache";
import WalletTreasury from "$lib/wallet-treasury";
import { getSeedFromMnemonic } from "$lib/wallet";

import { walletStore } from "..";
import { networkStore, walletStore } from "..";

describe("Wallet store", async () => {
vi.useFakeTimers();
Expand Down Expand Up @@ -94,10 +94,15 @@ describe("Wallet store", async () => {
const setCachedStakeInfoSpy = vi
.spyOn(walletCache, "setStakeInfo")
.mockResolvedValue(undefined);
const setLastBlockHeightSpy = vi.spyOn(walletCache, "setLastBlockHeight");
const setProfilesSpy = vi.spyOn(WalletTreasury.prototype, "setProfiles");
const treasuryUpdateSpy = vi.spyOn(WalletTreasury.prototype, "update");

vi.spyOn(networkStore, "checkBlock").mockResolvedValue(true);
vi.spyOn(networkStore, "getBlockHashByHeight").mockResolvedValue(
"some-block-hash"
);
vi.spyOn(networkStore, "getLastFinalizedBlockHeight").mockResolvedValue(121n);

const seed = getSeedFromMnemonic(generateMnemonic());
const profileGenerator = new ProfileGenerator(async () => seed);
const defaultProfile = await profileGenerator.default;
Expand Down Expand Up @@ -192,16 +197,11 @@ describe("Wallet store", async () => {
treasuryUpdateSpy.mock.invocationCallOrder[0]
);
expect(treasuryUpdateSpy).toHaveBeenCalledTimes(1);
expect(setLastBlockHeightSpy).toHaveBeenCalledTimes(1);
expect(balanceSpy).toHaveBeenCalledTimes(2);
expect(balanceSpy).toHaveBeenNthCalledWith(1, defaultProfile.address);
expect(balanceSpy).toHaveBeenNthCalledWith(2, defaultProfile.account);
expect(stakeInfoSpy).toHaveBeenCalledTimes(1);
expect(stakeInfoSpy).toHaveBeenCalledWith(defaultProfile.account);
expect(setLastBlockHeightSpy).toHaveBeenCalledWith(expect.any(BigInt));
expect(setLastBlockHeightSpy.mock.invocationCallOrder[0]).toBeGreaterThan(
treasuryUpdateSpy.mock.invocationCallOrder[0]
);
expect(balanceSpy.mock.invocationCallOrder[0]).toBeGreaterThan(
treasuryUpdateSpy.mock.invocationCallOrder[0]
);
Expand All @@ -227,6 +227,7 @@ describe("Wallet store", async () => {
expect(setCachedStakeInfoSpy.mock.invocationCallOrder[0]).toBeGreaterThan(
stakeInfoSpy.mock.invocationCallOrder[0]
);

expect(clearTimeoutSpy).toHaveBeenCalledTimes(1);
expect(setTimeoutSpy).toHaveBeenCalledTimes(1);
expect(clearTimeoutSpy.mock.invocationCallOrder[0]).toBeLessThan(
Expand Down Expand Up @@ -276,8 +277,8 @@ describe("Wallet store", async () => {

walletStore.abortSync();

expect(abortControllerSpy).toHaveBeenCalledTimes(1);
expect(clearTimeoutSpy).toHaveBeenCalledTimes(1);
expect(abortControllerSpy).toHaveBeenCalledTimes(1);

await vi.runAllTimersAsync();

Expand Down Expand Up @@ -564,15 +565,12 @@ describe("Wallet store", async () => {

treasuryUpdateSpy.mockClear();
balanceSpy.mockClear();
cacheClearSpy.mockClear();
stakeInfoSpy.mockClear();
setCachedBalanceSpy.mockClear();
setCachedStakeInfoSpy.mockClear();
});

afterEach(async () => {
cacheClearSpy.mockClear();
});

afterAll(() => {
cacheClearSpy.mockRestore();
});
Expand Down
Loading

0 comments on commit 6c28bcb

Please sign in to comment.