Skip to content

Commit

Permalink
fix: onboarding multisig duplication (#3076)
Browse files Browse the repository at this point in the history
* fix: onbording mutisig duplication

* fix: test
  • Loading branch information
sokolova-an authored Jan 30, 2025
1 parent 8ee699d commit 37e9096
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 60 deletions.
2 changes: 1 addition & 1 deletion src/renderer/app/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
9 changes: 8 additions & 1 deletion src/renderer/entities/multisig/api/multisigsService.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -12,15 +13,21 @@ export type MultisigResult = {
accountId: AccountId;
threshold: number;
signatories: AccountId[];
chain: Chain;
};

async function filterMultisigsAccounts(client: GraphQLClient, accountIds: AccountId[]): Promise<MultisigResult[]> {
async function filterMultisigsAccounts(
client: GraphQLClient,
accountIds: AccountId[],
chain: Chain,
): Promise<MultisigResult[]> {
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 || [];
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
},
]);
});
Expand Down
123 changes: 68 additions & 55 deletions src/renderer/entities/multisig/model/multisigs-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,9 +36,7 @@ const request = createEvent<AnyAccount[]>();

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,
Expand Down Expand Up @@ -73,7 +72,6 @@ const $multisigChains = combine(networkModel.$chains, (chains) => {
type GetMultisigsParams = {
chains: Chain[];
accounts: AnyAccount[];
multisigAccounts: AnyAccount[];
};

type MultisigResponse = {
Expand All @@ -90,65 +88,39 @@ type FlexibleMultisigResponse = {

type GetMultisigResponse = MultisigResponse | FlexibleMultisigResponse;

const getMultisigsFx = createEffect(
({ chains, accounts, multisigAccounts }: GetMultisigsParams): Promise<GetMultisigResponse[]> => {
const requests = chains.flatMap(async (chain) => {
const multisigIndexer = networkUtils.getProxyExternalApi(chain);
const getMultisigsFx = createEffect(({ chains, accounts }: GetMultisigsParams): Promise<MultisigResult[]> => {
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;

Expand All @@ -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<GetMultisigResponse[]>();

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],
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/features/proxy-add/model/add-proxy-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<MultisigWalletType | null>('regularMultisig');
const [selectedFlow, setSelectedFlow] = useState<MultisigWalletType | null>(null);
const open = useUnit(flowModel.flow.status);

const toggleModal = (open: boolean) => {
Expand Down
28 changes: 28 additions & 0 deletions src/renderer/shared/effector/takeLast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createEffect } from 'effector';

type Params<P, R> = {
fn(params: P, abort: AbortSignal): Awaited<R> | Promise<Awaited<R>>;
key(params: P): string;
};

export const takeLast = <P, R>({ fn, key }: Params<P, R>) => {
const controllers: Record<string, AbortController> = {};

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();
}
});
};

0 comments on commit 37e9096

Please sign in to comment.