From 37e90965b419cb78b179daad67314369750d3eb7 Mon Sep 17 00:00:00 2001 From: Anastasiia Sokolova <63446892+sokolova-an@users.noreply.github.com> Date: Thu, 30 Jan 2025 19:19:15 +0100 Subject: [PATCH] fix: onboarding multisig duplication (#3076) * fix: onbording mutisig duplication * fix: test --- src/renderer/app/bootstrap.ts | 2 +- .../entities/multisig/api/multisigsService.ts | 9 +- .../model/__tests__/multisigs-model.test.ts | 3 +- .../multisig/model/multisigs-model.ts | 123 ++++++++++-------- .../proxy-add/model/add-proxy-model.ts | 3 +- .../components/SelectMultisigWalletType.tsx | 2 +- src/renderer/shared/effector/takeLast.ts | 28 ++++ 7 files changed, 110 insertions(+), 60 deletions(-) create mode 100644 src/renderer/shared/effector/takeLast.ts diff --git a/src/renderer/app/bootstrap.ts b/src/renderer/app/bootstrap.ts index c5c12c150d..78484cc9f8 100644 --- a/src/renderer/app/bootstrap.ts +++ b/src/renderer/app/bootstrap.ts @@ -71,8 +71,8 @@ const populate = async () => { await networkModel.startNetworks(); await accounts.populate(); await walletModel.populate(); - await proxyModel.populate(); multisigsModel.subscribe(); + await proxyModel.populate(); // TODO rework as populate effects kernelModel.events.appStarted(); diff --git a/src/renderer/entities/multisig/api/multisigsService.ts b/src/renderer/entities/multisig/api/multisigsService.ts index 4eaade8afd..5262bcc4ac 100644 --- a/src/renderer/entities/multisig/api/multisigsService.ts +++ b/src/renderer/entities/multisig/api/multisigsService.ts @@ -1,5 +1,6 @@ import { type GraphQLClient } from 'graphql-request'; +import { type Chain } from '@/shared/core'; import { type AccountId } from '@/shared/polkadotjs-schemas'; import { FILTER_MULTISIG_ACCOUNT_IDS } from './graphql/queries/multisigs'; @@ -12,15 +13,21 @@ export type MultisigResult = { accountId: AccountId; threshold: number; signatories: AccountId[]; + chain: Chain; }; -async function filterMultisigsAccounts(client: GraphQLClient, accountIds: AccountId[]): Promise { +async function filterMultisigsAccounts( + client: GraphQLClient, + accountIds: AccountId[], + chain: Chain, +): Promise { const data = await client.request(FILTER_MULTISIG_ACCOUNT_IDS, { accountIds }); const filteredMultisigs = (data as any)?.accounts?.nodes?.map(({ id, threshold, signatories }: any) => ({ accountId: id, threshold, signatories: signatories.nodes.map(({ signatory }: any) => signatory.id), + chain, })); return filteredMultisigs || []; diff --git a/src/renderer/entities/multisig/model/__tests__/multisigs-model.test.ts b/src/renderer/entities/multisig/model/__tests__/multisigs-model.test.ts index c55d1c95b2..b7866c8c01 100644 --- a/src/renderer/entities/multisig/model/__tests__/multisigs-model.test.ts +++ b/src/renderer/entities/multisig/model/__tests__/multisigs-model.test.ts @@ -1,6 +1,6 @@ import { allSettled, fork } from 'effector'; -import { AccountType, ChainOptions, ConnectionType, ExternalType, WalletType } from '@/shared/core'; +import { AccountType, type Chain, ChainOptions, ConnectionType, ExternalType, WalletType } from '@/shared/core'; import { type AccountId } from '@/shared/polkadotjs-schemas'; // TODO multisig model should be in some kind of feature // eslint-disable-next-line boundaries/element-types @@ -37,6 +37,7 @@ describe('multisigs model', () => { accountId: '0x00' as AccountId, threshold: 2, signatories: ['0x01' as AccountId, '0x02' as AccountId, '0x03' as AccountId], + chain: mockChains['0x01'] as Chain, }, ]); }); diff --git a/src/renderer/entities/multisig/model/multisigs-model.ts b/src/renderer/entities/multisig/model/multisigs-model.ts index f8797862c2..495d0cd13f 100644 --- a/src/renderer/entities/multisig/model/multisigs-model.ts +++ b/src/renderer/entities/multisig/model/multisigs-model.ts @@ -19,12 +19,13 @@ import { SigningType, WalletType, } from '@/shared/core'; +import { takeLast } from '@/shared/effector/takeLast'; import { nonNullable, nullable, toAddress } from '@/shared/lib/utils'; import { type AnyAccount, accounts } from '@/domains/network'; import { networkModel, networkUtils } from '@/entities/network'; import { notificationModel } from '@/entities/notification'; import { accountUtils, walletModel, walletUtils } from '@/entities/wallet'; -import { multisigService } from '../api'; +import { type MultisigResult, multisigService } from '../api'; import { multisigUtils } from '../lib/mulitisigs-utils'; const MULTISIG_DISCOVERY_TIMEOUT = 30000; @@ -35,9 +36,7 @@ const request = createEvent(); const createWallets = attach({ effect: walletModel.createWallets }); -const $multisigAccounts = walletModel.$allWallets - .map(walletUtils.getAllAccounts) - .map((accounts) => accounts.filter(accountUtils.isMultisigAccount)); +const $multisigAccounts = accounts.$list.map((accounts) => accounts.filter(accountUtils.isMultisigAccount)); const { tick: pollingRequest, isRunning: $isPollingRunning } = interval({ start: subscribe, @@ -73,7 +72,6 @@ const $multisigChains = combine(networkModel.$chains, (chains) => { type GetMultisigsParams = { chains: Chain[]; accounts: AnyAccount[]; - multisigAccounts: AnyAccount[]; }; type MultisigResponse = { @@ -90,65 +88,39 @@ type FlexibleMultisigResponse = { type GetMultisigResponse = MultisigResponse | FlexibleMultisigResponse; -const getMultisigsFx = createEffect( - ({ chains, accounts, multisigAccounts }: GetMultisigsParams): Promise => { - const requests = chains.flatMap(async (chain) => { - const multisigIndexer = networkUtils.getProxyExternalApi(chain); +const getMultisigsFx = createEffect(({ chains, accounts }: GetMultisigsParams): Promise => { + const requests = chains.flatMap(async (chain) => { + const multisigIndexer = networkUtils.getProxyExternalApi(chain); - if (nullable(multisigIndexer) || accounts.length === 0) return []; + if (nullable(multisigIndexer) || accounts.length === 0) return []; - const client = new GraphQLClient(multisigIndexer.url); - const accountIds = uniq( - accounts.filter((a) => accountUtils.isChainIdMatch(a, chain.chainId)).map((account) => account.accountId), - ); + const client = new GraphQLClient(multisigIndexer.url); + const accountIds = uniq( + accounts.filter((a) => accountUtils.isChainIdMatch(a, chain.chainId)).map((account) => account.accountId), + ); - if (accountIds.length === 0) return []; + if (accountIds.length === 0) return []; - const indexedMultisigs = await multisigService.filterMultisigsAccounts(client, accountIds); + const indexedMultisigs = await multisigService.filterMultisigsAccounts(client, accountIds, chain); - return ( - indexedMultisigs - // filter out multisigs that already exists - .filter((multisigResult) => nullable(multisigAccounts.find((a) => a.accountId === multisigResult.accountId))) - .map(({ threshold, accountId, signatories }): GetMultisigResponse => { - // TODO: run a proxy worker for new multisiig since we don't have these proxies at the moment - - // TODO check if there's a multisig with no proxy and only one ongoing operation 'create pure proxy' - build flexible shell - // if (proxy) { - // return { - // type: 'flexibleMultisig', - // account: multisigUtils.buildFlexibleMultisigAccount({ - // threshold, - // proxyAccount: proxy, - // accountId, - // signatories, - // chain, - // }), - // chain, - // }; - // } - - return { - type: 'multisig', - account: multisigUtils.buildMultisigAccount({ threshold, accountId, signatories, chain }), - chain, - }; - }) - ); - }); + return indexedMultisigs; + }); - return Promise.all(requests).then((res) => res.flat()); - }, -); + return Promise.all(requests).then((res) => res.flat()); +}); + +const getLastMultisigsFx = takeLast({ + fn: getMultisigsFx, + key: (params) => JSON.stringify(params), +}); sample({ clock: [updateRequested, request], source: { - multisigAccounts: $multisigAccounts, chains: $multisigChains, connections: networkModel.$connections, }, - fn: ({ multisigAccounts, chains, connections }, accounts) => { + fn: ({ chains, connections }, accounts) => { const filteredChains = chains.filter((chain) => { if (nullable(connections[chain.chainId])) return false; @@ -157,23 +129,64 @@ sample({ return { chains: filteredChains, - multisigAccounts, accounts, }; }, - target: getMultisigsFx, + target: getLastMultisigsFx, +}); + +type FilteredMultisigParams = { multisigAccounts: AnyAccount[]; indexedMultisigs: MultisigResult[] }; + +const filterMultisigFx = createEffect( + ({ multisigAccounts, indexedMultisigs }: FilteredMultisigParams): GetMultisigResponse[] => { + return ( + indexedMultisigs + // filter out multisigs that already exists + .filter((multisigResult) => nullable(multisigAccounts.find((a) => a.accountId === multisigResult.accountId))) + .map(({ threshold, accountId, signatories, chain }): GetMultisigResponse => { + // TODO check if there's a multisig with no proxy and only one ongoing operation 'create pure proxy' - build flexible shell + // if (proxy) { + // return { + // type: 'flexibleMultisig', + // account: multisigUtils.buildFlexibleMultisigAccount({ + // threshold, + // proxyAccount: proxy, + // accountId, + // signatories, + // chain, + // }), + // chain, + // }; + // } + + return { + type: 'multisig', + account: multisigUtils.buildMultisigAccount({ threshold, accountId, signatories, chain }), + chain, + }; + }) + ); + }, +); + +sample({ + clock: getLastMultisigsFx.doneData, + source: $multisigAccounts, + filter: (_, multisigs) => multisigs.length > 0, + fn: (multisigAccounts, indexedMultisigs) => ({ multisigAccounts, indexedMultisigs }), + target: filterMultisigFx, }); const populateWallets = createEvent(); sample({ - clock: getMultisigsFx.doneData, + clock: filterMultisigFx.doneData, filter: (response) => response.length > 0, target: populateWallets, }); sample({ - clock: getMultisigsFx.done, + clock: filterMultisigFx.done, source: $isPollingRunning, filter: (isRunning) => isRunning, target: [stop, subscribe], diff --git a/src/renderer/features/proxy-add/model/add-proxy-model.ts b/src/renderer/features/proxy-add/model/add-proxy-model.ts index 9e3b912cd7..715554efed 100644 --- a/src/renderer/features/proxy-add/model/add-proxy-model.ts +++ b/src/renderer/features/proxy-add/model/add-proxy-model.ts @@ -157,7 +157,8 @@ sample({ sample({ clock: submitModel.output.formSubmitted, - filter: (results) => submitUtils.isSuccessResult(results[0].result), + source: $addProxyStore, + filter: (addProxyStore, results) => nonNullable(addProxyStore) && submitUtils.isSuccessResult(results[0].result), target: flowFinished, }); diff --git a/src/renderer/features/wallet-pairing-multisig/components/SelectMultisigWalletType.tsx b/src/renderer/features/wallet-pairing-multisig/components/SelectMultisigWalletType.tsx index 895ea2198c..5bc4971dbd 100644 --- a/src/renderer/features/wallet-pairing-multisig/components/SelectMultisigWalletType.tsx +++ b/src/renderer/features/wallet-pairing-multisig/components/SelectMultisigWalletType.tsx @@ -15,7 +15,7 @@ import { type MultisigWalletType, descriptionMultisig } from './common/constants export const SelectMultisigWalletType = ({ children }: PropsWithChildren) => { // TODO: make null when we're ready to work with flexible multisig - const [selectedFlow, setSelectedFlow] = useState('regularMultisig'); + const [selectedFlow, setSelectedFlow] = useState(null); const open = useUnit(flowModel.flow.status); const toggleModal = (open: boolean) => { diff --git a/src/renderer/shared/effector/takeLast.ts b/src/renderer/shared/effector/takeLast.ts new file mode 100644 index 0000000000..89a1b0f5f0 --- /dev/null +++ b/src/renderer/shared/effector/takeLast.ts @@ -0,0 +1,28 @@ +import { createEffect } from 'effector'; + +type Params = { + fn(params: P, abort: AbortSignal): Awaited | Promise>; + key(params: P): string; +}; + +export const takeLast = ({ fn, key }: Params) => { + const controllers: Record = {}; + + return createEffect(async (params: P) => { + const effectKey = key(params); + + let controller = controllers[effectKey]; + if (controller) controller.abort(); + controller = new AbortController(); + controllers[effectKey] = controller; + + try { + return await fn(params, controller.signal); + } finally { + if (controllers[effectKey] === controller) { + delete controllers[effectKey]; + } + controller.signal.throwIfAborted(); + } + }); +};