Skip to content

Commit

Permalink
Merge pull request #3413 from dusk-network/feature-3396
Browse files Browse the repository at this point in the history
web-wallet: Decouple cache from the wallet store
  • Loading branch information
ascartabelli authored Jan 24, 2025
2 parents 938c520 + 274069a commit 6fadf10
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 105 deletions.
3 changes: 2 additions & 1 deletion web-wallet/src/lib/__mocks__/mockedWalletStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ const profiles = [
await profileGenerator.next(),
await profileGenerator.next(),
];

const currentProfile = profiles[0];
const shielded = { spendable: 50_000_000_000_000n, value: 2_345_000_000_000n };
const unshielded = { nonce: 1234n, value: shielded.value / 2n };

/** @type {WalletStoreContent} */
/** @type {WalletStoreContent & { currentProfile: NonNullable<WalletStoreContent["currentProfile"]> }} */
const content = {
balance: { shielded, unshielded },
currentProfile,
Expand Down
7 changes: 5 additions & 2 deletions web-wallet/src/lib/dusk/test-helpers/mockReadableStore.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { get, writable } from "svelte/store";

/** @param {*} initialValue */
/**
* @template T
* @param {T} initialValue
*/
function mockReadableStore(initialValue) {
const store = writable(initialValue);
const { set, subscribe } = store;
const getMockedStoreValue = () => get(store);

/** @param {*} value */
/** @param {T} value */
const setMockedStoreValue = (value) => set(value);

return {
Expand Down
105 changes: 47 additions & 58 deletions web-wallet/src/lib/stores/__tests__/walletStore.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { generateMnemonic } from "bip39";

import { stakeInfo } from "$lib/mock-data";

import walletCache from "$lib/wallet-cache";
import WalletTreasury from "$lib/wallet-treasury";
import { getSeedFromMnemonic } from "$lib/wallet";

Expand Down Expand Up @@ -83,16 +82,16 @@ describe("Wallet store", async () => {
.mockImplementation(async () => stakeInfo);

const getCachedBalanceSpy = vi
.spyOn(walletCache, "getBalanceInfo")
.spyOn(WalletTreasury.prototype, "getCachedBalance")
.mockResolvedValue(cachedBalance);
const setCachedBalanceSpy = vi
.spyOn(walletCache, "setBalanceInfo")
.spyOn(WalletTreasury.prototype, "setCachedBalance")
.mockResolvedValue(undefined);
const getCachedStakeInfoSpy = vi
.spyOn(walletCache, "getStakeInfo")
.spyOn(WalletTreasury.prototype, "getCachedStakeInfo")
.mockResolvedValue(cachedStakeInfo);
const setCachedStakeInfoSpy = vi
.spyOn(walletCache, "setStakeInfo")
.spyOn(WalletTreasury.prototype, "setCachedStakeInfo")
.mockResolvedValue(undefined);
const setProfilesSpy = vi.spyOn(WalletTreasury.prototype, "setProfiles");
const treasuryUpdateSpy = vi.spyOn(WalletTreasury.prototype, "update");
Expand Down Expand Up @@ -180,13 +179,9 @@ describe("Wallet store", async () => {
});

expect(getCachedBalanceSpy).toHaveBeenCalledTimes(1);
expect(getCachedBalanceSpy).toHaveBeenCalledWith(
defaultProfile.address.toString()
);
expect(getCachedBalanceSpy).toHaveBeenCalledWith(defaultProfile);
expect(getCachedStakeInfoSpy).toHaveBeenCalledTimes(1);
expect(getCachedStakeInfoSpy).toHaveBeenCalledWith(
defaultProfile.account.toString()
);
expect(getCachedStakeInfoSpy).toHaveBeenCalledWith(defaultProfile);

await vi.advanceTimersByTimeAsync(AUTO_SYNC_INTERVAL - 1);

Expand All @@ -209,16 +204,13 @@ describe("Wallet store", async () => {
treasuryUpdateSpy.mock.invocationCallOrder[0]
);
expect(setCachedBalanceSpy).toHaveBeenCalledTimes(1);
expect(setCachedBalanceSpy).toHaveBeenCalledWith(
defaultProfile.address.toString(),
{
shielded: await balanceSpy.mock.results[0].value,
unshielded: await balanceSpy.mock.results[1].value,
}
);
expect(setCachedBalanceSpy).toHaveBeenCalledWith(defaultProfile, {
shielded: await balanceSpy.mock.results[0].value,
unshielded: await balanceSpy.mock.results[1].value,
});
expect(setCachedStakeInfoSpy).toHaveBeenCalledTimes(1);
expect(setCachedStakeInfoSpy).toHaveBeenCalledWith(
defaultProfile.account.toString(),
defaultProfile,
await stakeInfoSpy.mock.results[0].value
);
expect(setCachedBalanceSpy.mock.invocationCallOrder[0]).toBeGreaterThan(
Expand Down Expand Up @@ -326,7 +318,16 @@ describe("Wallet store", async () => {
const executeSpy = vi
.spyOn(Network.prototype, "execute")
.mockResolvedValue(phoenixTxResult);
const setPendingNotesSpy = vi.spyOn(walletCache, "setPendingNoteInfo");

const updateNonceSpy = vi.spyOn(
WalletTreasury.prototype,
"updateCachedNonce"
);

const updateCachedPendingNotesSpy = vi.spyOn(
WalletTreasury.prototype,
"updateCachedPendingNotes"
);

/**
* @typedef { "claimRewards" | "shield" | "stake" | "transfer" | "unshield" | "unstake" } TransferMethod
Expand All @@ -347,9 +348,10 @@ describe("Wallet store", async () => {
clearTimeoutSpy.mockRestore();
vi.useRealTimers();

const currentlyCachedBalance = await walletCache.getBalanceInfo(
defaultProfile.address.toString()
);
const currentlyCachedBalance =
await new WalletTreasury().getCachedBalance(
defaultProfile.address.toString()
);
const newNonce = currentlyCachedBalance.unshielded.nonce + 1n;

let expectedTx;
Expand Down Expand Up @@ -400,26 +402,17 @@ describe("Wallet store", async () => {
);

if (isPhoenixTransfer) {
expect(setCachedBalanceSpy).not.toHaveBeenCalled();
expect(setPendingNotesSpy).toHaveBeenCalledTimes(1);
expect(setPendingNotesSpy).toHaveBeenCalledWith(
expect(updateNonceSpy).not.toHaveBeenCalled();
expect(updateCachedPendingNotesSpy).toHaveBeenCalledTimes(1);
expect(updateCachedPendingNotesSpy).toHaveBeenCalledWith(
phoenixTxResult.nullifiers,
phoenixTxResult.hash
);
setPendingNotesSpy.mockClear();
updateCachedPendingNotesSpy.mockClear();
} else {
expect(setCachedBalanceSpy).toHaveBeenCalledTimes(1);
expect(setCachedBalanceSpy).toHaveBeenCalledWith(
defaultProfile.address.toString(),
{
...currentlyCachedBalance,
unshielded: {
...currentlyCachedBalance.unshielded,
nonce: newNonce,
},
}
);
expect(setPendingNotesSpy).not.toHaveBeenCalled();
expect(updateNonceSpy).toHaveBeenCalledTimes(1);
expect(updateNonceSpy).toHaveBeenCalledWith(defaultProfile, newNonce);
expect(updateCachedPendingNotesSpy).not.toHaveBeenCalled();
setCachedBalanceSpy.mockClear();
}

Expand Down Expand Up @@ -464,16 +457,13 @@ describe("Wallet store", async () => {
treasuryUpdateSpy.mock.invocationCallOrder[1]
);
expect(setCachedBalanceSpy).toHaveBeenCalledTimes(1);
expect(setCachedBalanceSpy).toHaveBeenCalledWith(
defaultProfile.address.toString(),
{
shielded: await balanceSpy.mock.results[0].value,
unshielded: await balanceSpy.mock.results[1].value,
}
);
expect(setCachedBalanceSpy).toHaveBeenCalledWith(defaultProfile, {
shielded: await balanceSpy.mock.results[0].value,
unshielded: await balanceSpy.mock.results[1].value,
});
expect(setCachedStakeInfoSpy).toHaveBeenCalledTimes(1);
expect(setCachedStakeInfoSpy).toHaveBeenCalledWith(
defaultProfile.account.toString(),
defaultProfile,
await stakeInfoSpy.mock.results[0].value
);
expect(setCachedBalanceSpy.mock.invocationCallOrder[0]).toBeGreaterThan(
Expand Down Expand Up @@ -512,12 +502,14 @@ describe("Wallet store", async () => {

afterEach(async () => {
executeSpy.mockClear();
setPendingNotesSpy.mockClear();
updateNonceSpy.mockClear();
updateCachedPendingNotesSpy.mockClear();
});

afterAll(() => {
executeSpy.mockRestore();
setPendingNotesSpy.mockRestore();
updateNonceSpy.mockRestore();
updateCachedPendingNotesSpy.mockRestore();
});

it("should expose a method to claim the rewards", async () => {
Expand Down Expand Up @@ -550,7 +542,7 @@ describe("Wallet store", async () => {
});

describe("Wallet store services", () => {
const cacheClearSpy = vi.spyOn(walletCache, "clear");
const cacheClearSpy = vi.spyOn(WalletTreasury.prototype, "clearCache");

beforeEach(async () => {
walletStore.reset();
Expand Down Expand Up @@ -651,16 +643,13 @@ describe("Wallet store", async () => {
expect(stakeInfoSpy).toHaveBeenCalledTimes(1);
expect(stakeInfoSpy).toHaveBeenCalledWith(fakeExtraProfile.account);
expect(setCachedBalanceSpy).toHaveBeenCalledTimes(1);
expect(setCachedBalanceSpy).toHaveBeenCalledWith(
fakeExtraProfile.address.toString(),
{
shielded: await balanceSpy.mock.results[0].value,
unshielded: await balanceSpy.mock.results[1].value,
}
);
expect(setCachedBalanceSpy).toHaveBeenCalledWith(fakeExtraProfile, {
shielded: await balanceSpy.mock.results[0].value,
unshielded: await balanceSpy.mock.results[1].value,
});
expect(setCachedStakeInfoSpy).toHaveBeenCalledTimes(1);
expect(setCachedStakeInfoSpy).toHaveBeenCalledWith(
fakeExtraProfile.account.toString(),
fakeExtraProfile,
await stakeInfoSpy.mock.results[0].value
);
expect(setCachedBalanceSpy.mock.invocationCallOrder[0]).toBeGreaterThan(
Expand Down
55 changes: 22 additions & 33 deletions web-wallet/src/lib/stores/walletStore.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { get, writable } from "svelte/store";
import { setKey, setPathIn } from "lamb";
import { setKey } from "lamb";
import {
Bookkeeper,
Bookmark,
ProfileGenerator,
} from "$lib/vendor/w3sper.js/src/mod";

import walletCache from "$lib/wallet-cache";
import WalletTreasury from "$lib/wallet-treasury";

import { transactions } from "$lib/mock-data";
Expand Down Expand Up @@ -90,46 +89,40 @@ const updateCacheAfterTransaction = async (txInfo) => {
* writing the pending notes info, as we'll
* change soon how they are handled (probably by w3sper directly).
*/
await walletCache
.setPendingNoteInfo(txInfo.nullifiers, txInfo.hash)
await treasury
.updateCachedPendingNotes(txInfo.nullifiers, txInfo.hash)
.catch(() => {});
} else {
const address = String(getCurrentProfile()?.address);
const currentBalance = await walletCache.getBalanceInfo(address);
const profile = getCurrentProfile();

/**
* We update the stored `nonce` so that if a transaction is made
* before the sync gives us an updated one, the transaction
* won't be rejected by reusing the old value.
*/
await walletCache.setBalanceInfo(
address,
setPathIn(currentBalance, "unshielded.nonce", txInfo.nonce)
);
profile && (await treasury.updateCachedNonce(profile, txInfo.nonce));
}

return txInfo;
};

/** @type {() => Promise<void>} */
const updateBalance = async () => {
const { currentProfile } = get(walletStore);
const profile = getCurrentProfile();

if (!currentProfile) {
if (!profile) {
return;
}

const shielded = await bookkeeper.balance(currentProfile.address);
const unshielded = await bookkeeper.balance(currentProfile.account);
const shielded = await bookkeeper.balance(profile.address);
const unshielded = await bookkeeper.balance(profile.account);
const balance = { shielded, unshielded };

/**
* We ignore the error as the cached balance is only
* a nice to have for the user.
*/
await walletCache
.setBalanceInfo(currentProfile.address.toString(), balance)
.catch(() => {});
await treasury.setCachedBalance(profile, balance).catch(() => {});

update((currentStore) => ({
...currentStore,
Expand All @@ -139,21 +132,20 @@ const updateBalance = async () => {

/** @type {() => Promise<void>} */
const updateStakeInfo = async () => {
const { currentProfile } = get(walletStore);
const profile = getCurrentProfile();

if (!currentProfile) {
if (!profile) {
return;
}

const stakeInfo = await bookkeeper.stakeInfo(currentProfile.account);
/** @type {StakeInfo} */
const stakeInfo = await bookkeeper.stakeInfo(profile.account);

/**
* We ignore the error as the cached stake info is only
* a nice to have for the user.
*/
await walletCache
.setStakeInfo(currentProfile.account.toString(), stakeInfo)
.catch(() => {});
await treasury.setCachedStakeInfo(profile, stakeInfo).catch(() => {});

update((currentStore) => ({
...currentStore,
Expand All @@ -172,10 +164,10 @@ const abortSync = () => {
};

/** @type {WalletStoreServices["clearLocalData"]} */
const clearLocalData = () => {
const clearLocalData = async () => {
abortSync();

return walletCache.clear();
await treasury.clearCache();
};

/** @type {WalletStoreServices["clearLocalDataAndInit"]} */
Expand All @@ -202,11 +194,8 @@ const getTransactionsHistory = async () => transactions;
/** @type {WalletStoreServices["init"]} */
async function init(profileGenerator, syncFromBlock) {
const currentProfile = await profileGenerator.default;
const currentAddress = currentProfile.address.toString();
const cachedBalance = await walletCache.getBalanceInfo(currentAddress);
const cachedStakeInfo = await walletCache.getStakeInfo(
currentProfile.account.toString()
);
const cachedBalance = await treasury.getCachedBalance(currentProfile);
const cachedStakeInfo = await treasury.getCachedStakeInfo(currentProfile);
const minimumStake = await bookkeeper.minimumStake;

treasury.setProfiles([currentProfile]);
Expand All @@ -223,7 +212,7 @@ async function init(profileGenerator, syncFromBlock) {

sync(syncFromBlock)
.then(() => {
settingsStore.update(setKey("userId", currentAddress));
settingsStore.update(setKey("userId", currentProfile.address.toString()));
})
.finally(updateStaticInfo);
}
Expand Down Expand Up @@ -297,7 +286,7 @@ async function sync(fromBlock) {
syncController = new AbortController();

const { block, bookmark, lastFinalizedBlockHeight } =
await walletCache.getSyncInfo();
await treasury.getCachedSyncInfo();

/** @type {bigint | Bookmark} */
let from;
Expand All @@ -324,7 +313,7 @@ async function sync(fromBlock) {
}

if (from === 0n) {
await walletCache.clear();
await treasury.clearCache();
}

update((currentStore) => ({
Expand Down
Loading

0 comments on commit 6fadf10

Please sign in to comment.