Skip to content

Commit

Permalink
refactor: wallet management now scoped in wallet model (except index …
Browse files Browse the repository at this point in the history
…db write) (#2632)
  • Loading branch information
johnthecat authored Nov 12, 2024
1 parent 3b7cfdb commit 0c1aa31
Show file tree
Hide file tree
Showing 36 changed files with 329 additions and 350 deletions.
15 changes: 11 additions & 4 deletions src/renderer/entities/wallet/lib/model-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type BaseAccount, type ChainAccount } from '@/shared/core';
import { type BaseAccount, type ChainAccount, type ShardAccount } from '@/shared/core';

import { accountUtils } from './account-utils';

Expand All @@ -9,9 +9,10 @@ export const modelUtils = {
type AccountsGroup = {
base: BaseAccount[];
chains: ChainAccount[][];
shards: ShardAccount[][];
};
function groupAccounts(accounts: Omit<BaseAccount | ChainAccount, 'id' | 'walletId'>[]): AccountsGroup {
return accounts.reduce<{ base: BaseAccount[]; chains: ChainAccount[][] }>(
function groupAccounts(accounts: Omit<BaseAccount | ChainAccount | ShardAccount, 'id' | 'walletId'>[]) {
return accounts.reduce<AccountsGroup>(
(acc, account) => {
const lastBaseIndex = acc.base.length - 1;

Expand All @@ -24,9 +25,15 @@ function groupAccounts(accounts: Omit<BaseAccount | ChainAccount, 'id' | 'wallet
}
acc.chains[lastBaseIndex].push(account);
}
if (accountUtils.isShardAccount(account)) {
if (!acc.shards[lastBaseIndex]) {
acc.shards[lastBaseIndex] = [];
}
acc.shards[lastBaseIndex].push(account);
}

return acc;
},
{ base: [], chains: [] },
{ base: [], chains: [], shards: [] },
);
}
93 changes: 5 additions & 88 deletions src/renderer/entities/wallet/model/__tests__/wallet-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,50 +25,6 @@ describe('entities/wallet/model/wallet-model', () => {
expect(scope.getState(walletModel.$activeWallet)).toEqual(wallets[0]);
});

test('should update $allWallets on watchOnlyCreated', async () => {
const wallets = walletMock.getWallets(0);
const { newAccounts, newWallet } = walletMock;

jest.spyOn(storageService.wallets, 'create').mockResolvedValue(newWallet);
jest.spyOn(storageService.accounts, 'createAll').mockResolvedValue([newAccounts[0]]);
jest.spyOn(storageService.wallets, 'update').mockResolvedValue(3);

const scope = fork({
values: new Map().set(walletModel.$allWallets, wallets),
});

await allSettled(walletModel.events.watchOnlyCreated, {
scope,
params: { wallet: newWallet, accounts: [newAccounts[0]] as any[] },
});

expect(scope.getState(walletModel.$allWallets)).toEqual(
wallets.concat({ ...newWallet, accounts: [newAccounts[0]] }),
);
});

test('should update $allWallets on multishardCreated', async () => {
const wallets = walletMock.getWallets(0);
const { newAccounts, newWallet } = walletMock;

jest.spyOn(storageService.wallets, 'create').mockResolvedValue(newWallet);
jest.spyOn(storageService.accounts, 'create').mockResolvedValue(newAccounts[0]);
jest.spyOn(storageService.accounts, 'createAll').mockResolvedValue([newAccounts[1]]);
jest.spyOn(storageService.wallets, 'update').mockResolvedValue(3);

const scope = fork({
values: new Map().set(walletModel.$allWallets, wallets),
});

expect(scope.getState(walletModel.$allWallets)).toHaveLength(wallets.length);
await allSettled(walletModel.events.multishardCreated, {
scope,
params: { wallet: newWallet, accounts: newAccounts as any[] },
});

expect(scope.getState(walletModel.$allWallets)).toEqual(wallets.concat({ ...newWallet, accounts: newAccounts }));
});

test('should update $allWallets on walletRemoved', async () => {
const wallets = walletMock.getWallets(0);
const [removedWallet, ...remainingWallets] = wallets;
Expand All @@ -79,13 +35,13 @@ describe('entities/wallet/model/wallet-model', () => {
const deleteAccountsSpy = jest.spyOn(storageService.accounts, 'deleteAll').mockResolvedValue([1, 2, 3]);

const scope = fork({
values: new Map().set(walletModel.$allWallets, wallets),
values: new Map().set(walletModel._test.$allWallets, wallets),
});

await allSettled(walletModel.events.walletRemoved, { scope, params: removedWallet.id });

expect(deleteAccountsSpy).toHaveBeenCalledWith(removedAccounts.map((a) => a.id));
expect(scope.getState(walletModel.$allWallets)).toEqual(remainingWallets);
expect(scope.getState(walletModel._test.$allWallets)).toEqual(remainingWallets);
});

test('should update $allWallets on walletsRemoved', async () => {
Expand All @@ -98,52 +54,13 @@ describe('entities/wallet/model/wallet-model', () => {
const deleteAccountsSpy = jest.spyOn(storageService.accounts, 'deleteAll').mockResolvedValue([1, 2, 3]);

const scope = fork({
values: new Map().set(walletModel.$allWallets, wallets),
values: new Map().set(walletModel._test.$allWallets, wallets),
});

await allSettled(walletModel.events.walletsRemoved, { scope, params: [removedWallet.id] });

expect(deleteAccountsSpy).toHaveBeenCalledWith(removedAccounts.map((a) => a.id));
expect(scope.getState(walletModel.$allWallets)).toEqual(remainingWallets);
});

test('should update $allWallets on walletHidden', async () => {
const wallets = walletMock.getWallets(0);
const hiddenWallet = wallets[0];

const updateSpy = jest.spyOn(storageService.wallets, 'update').mockResolvedValue(1);

const scope = fork({
values: new Map().set(walletModel.$allWallets, wallets),
});

await allSettled(walletModel.events.walletHidden, { scope, params: hiddenWallet });

expect(updateSpy).toHaveBeenCalledWith(1, { isHidden: true });
expect(scope.getState(walletModel.$allWallets)).toEqual([{ ...hiddenWallet, isHidden: true }, ...wallets.slice(1)]);
});

test('should update $allWallets on walletRestore', async () => {
const wallets = walletMock.getWallets(0);
const walletToRestore = wallets.find((wallet) => wallet.isHidden)!;

const updateSpy = jest.spyOn(storageService.wallets, 'update').mockResolvedValue(walletToRestore.id);

const scope = fork({
values: new Map().set(walletModel.$allWallets, wallets),
});

await allSettled(walletModel.$allWallets, { scope, params: wallets });
expect(scope.getState(walletModel.$hiddenWallets)).toEqual([walletToRestore]);

await allSettled(walletModel.events.walletRestored, { scope, params: walletToRestore });

expect(updateSpy).toHaveBeenCalledWith(walletToRestore.id, { isHidden: false });
expect(scope.getState(walletModel.$allWallets)).toEqual(
wallets.map((wallet) => {
return wallet.id === walletToRestore.id ? { ...wallet, isHidden: false } : wallet;
}),
);
expect(scope.getState(walletModel._test.$allWallets)).toEqual(remainingWallets);
});

test('should update $wallets and $hiddenWallets when $allWallets is updated', async () => {
Expand All @@ -153,7 +70,7 @@ describe('entities/wallet/model/wallet-model', () => {

const scope = fork();

await allSettled(walletModel.$allWallets, { scope, params: wallets });
await allSettled(walletModel._test.$allWallets, { scope, params: wallets });

expect(scope.getState(walletModel.$hiddenWallets)).toEqual([hiddenWallet]);
expect(scope.getState(walletModel.$wallets)).toEqual(visibleWallets);
Expand Down
126 changes: 107 additions & 19 deletions src/renderer/entities/wallet/model/wallet-model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { combine, createEffect, createEvent, createStore, sample } from 'effector';
import groupBy from 'lodash/groupBy';
import { combineEvents } from 'patronum';
import { combineEvents, readonly } from 'patronum';

import { storageService } from '@/shared/api/storage';
import {
Expand All @@ -10,10 +10,12 @@ import {
type ID,
type MultisigAccount,
type NoID,
type ProxiedAccount,
type ShardAccount,
type Wallet,
type WcAccount,
} from '@/shared/core';
import { dictionary } from '@/shared/lib/utils';
import { dictionary, nonNullable } from '@/shared/lib/utils';
import { modelUtils } from '../lib/model-utils';

type DbWallet = Omit<Wallet, 'accounts'>;
Expand All @@ -25,15 +27,21 @@ type CreateParams<T extends Account = Account> = {

const walletStarted = createEvent();
const watchOnlyCreated = createEvent<CreateParams<BaseAccount>>();
const multishardCreated = createEvent<CreateParams<BaseAccount | ChainAccount>>();
const multishardCreated = createEvent<CreateParams<BaseAccount | ChainAccount | ShardAccount>>();
const singleshardCreated = createEvent<CreateParams<BaseAccount>>();
const multisigCreated = createEvent<CreateParams<MultisigAccount>>();
const proxiedCreated = createEvent<CreateParams<ProxiedAccount>>();
const walletConnectCreated = createEvent<CreateParams<WcAccount>>();

const walletRestored = createEvent<Wallet>();
const walletHidden = createEvent<Wallet>();
const walletRemoved = createEvent<ID>();
const walletsRemoved = createEvent<ID[]>();
const selectWallet = createEvent<ID>();
// TODO this is temp solution, accounts should be separated from wallets
const updateAccounts = createEvent<{ walletId: ID; accounts: Account[] }>();
// TODO this is temp solution, each type of wallet should update own data inside feature
const updateWallet = createEvent<{ walletId: ID; data: NonNullable<unknown> }>();

const $allWallets = createStore<Wallet[]>([]);
const $wallets = $allWallets.map((wallets) => wallets.filter((x) => !x.isHidden));
Expand Down Expand Up @@ -92,12 +100,15 @@ const walletCreatedFx = createEffect(async ({ wallet, accounts }: CreateParams):
});

const multishardCreatedFx = createEffect(
async ({ wallet, accounts }: CreateParams<BaseAccount | ChainAccount>): Promise<CreateResult | undefined> => {
async ({
wallet,
accounts,
}: CreateParams<BaseAccount | ChainAccount | ShardAccount>): Promise<CreateResult | undefined> => {
const dbWallet = await storageService.wallets.create({ ...wallet, isActive: false });

if (!dbWallet) return undefined;

const { base, chains } = modelUtils.groupAccounts(accounts);
const { base, chains, shards } = modelUtils.groupAccounts(accounts);

const multishardAccounts = [];

Expand All @@ -106,14 +117,32 @@ const multishardCreatedFx = createEffect(
if (!dbBaseAccount) return undefined;

multishardAccounts.push(dbBaseAccount);
if (!chains[index]) continue;

const accountsPayload = chains[index].map((account) => ({
...account,
walletId: dbWallet.id,
baseId: dbBaseAccount.id,
}));
const dbChainAccounts = await storageService.accounts.createAll(accountsPayload);
let accountPayloads: NoID<Account>[] = [];

if (chains[index]) {
accountPayloads = accountPayloads.concat(
chains[index].map((account) => ({
...account,
walletId: dbWallet.id,
baseId: dbBaseAccount.id,
})),
);
}

if (shards[index]) {
accountPayloads = accountPayloads.concat(
shards[index].map((account) => ({
...account,
walletId: dbWallet.id,
})),
);
}

if (accountPayloads.length === 0) {
continue;
}

const dbChainAccounts = await storageService.accounts.createAll(accountPayloads);
if (!dbChainAccounts) return undefined;

multishardAccounts.push(...dbChainAccounts);
Expand Down Expand Up @@ -159,6 +188,18 @@ const restoreWalletFx = createEffect(async (wallet: Wallet): Promise<Wallet> =>
return wallet;
});

const walletSelectedFx = createEffect(async (nextId: ID): Promise<ID | undefined> => {
const wallets = await storageService.wallets.readAll();
const inactiveWallets = wallets.filter((wallet) => wallet.isActive).map((wallet) => ({ ...wallet, isActive: false }));

const [, nextWallet] = await Promise.all([
storageService.wallets.updateAll(inactiveWallets),
storageService.wallets.update(nextId, { isActive: true }),
]);

return nextWallet;
});

sample({
clock: walletStarted,
target: [fetchAllAccountsFx, fetchAllWalletsFx],
Expand All @@ -174,8 +215,12 @@ sample({
target: $allWallets,
});

const walletCreatedDone = sample({
clock: [walletCreatedFx.doneData, multishardCreatedFx.doneData],
}).filter({ fn: nonNullable });

sample({
clock: [walletConnectCreated, watchOnlyCreated, multisigCreated, singleshardCreated],
clock: [walletConnectCreated, watchOnlyCreated, multisigCreated, singleshardCreated, proxiedCreated],
target: walletCreatedFx,
});

Expand All @@ -185,11 +230,10 @@ sample({
});

sample({
clock: [walletCreatedFx.doneData, multishardCreatedFx.doneData],
clock: walletCreatedDone,
source: $allWallets,
filter: (_, data) => Boolean(data),
fn: (wallets, data) => {
return wallets.concat({ ...data!.wallet, accounts: data!.accounts });
return wallets.concat({ ...data.wallet, accounts: data.accounts });
},
target: $allWallets,
});
Expand Down Expand Up @@ -273,9 +317,45 @@ sample({
target: $allWallets,
});

sample({ clock: selectWallet, target: walletSelectedFx });

sample({
clock: walletSelectedFx.doneData,
source: $allWallets,
filter: (_, nextId) => Boolean(nextId),
fn: (wallets, nextId) => {
return wallets.map((wallet) => ({ ...wallet, isActive: wallet.id === nextId }));
},
target: $allWallets,
});

sample({
clock: updateAccounts,
source: $allWallets,
fn: (wallets, { walletId, accounts }) => {
return wallets.map<Wallet>((wallet) => {
if (wallet.id !== walletId) return wallet;

return { ...wallet, accounts };
});
},
target: $allWallets,
});

sample({
clock: updateWallet,
source: $allWallets,
fn: (wallets, { walletId, data }) => {
return wallets.map((wallet) => {
return wallet.id === walletId ? { ...wallet, ...data } : wallet;
});
},
target: $allWallets,
});

export const walletModel = {
$wallets,
$allWallets,
$allWallets: readonly($allWallets),
$hiddenWallets,
$activeWallet,
$isLoadingWallets: fetchAllWalletsFx.pending,
Expand All @@ -287,7 +367,11 @@ export const walletModel = {
singleshardCreated,
multisigCreated,
walletConnectCreated,
walletCreatedDone: walletCreatedFx.done,
proxiedCreated,
walletCreatedDone,
selectWallet,
updateAccounts,
updateWallet,
walletRemoved,
walletHidden,
walletHiddenSuccess: hideWalletFx.done,
Expand All @@ -296,4 +380,8 @@ export const walletModel = {
walletRestored,
walletRestoredSuccess: restoreWalletFx.done,
},

_test: {
$allWallets,
},
};
Loading

0 comments on commit 0c1aa31

Please sign in to comment.