diff --git a/src/renderer/widgets/AddProxyModal/model/__tests__/add-proxy-model.test.ts b/src/renderer/widgets/AddProxyModal/model/__tests__/add-proxy-model.test.ts index b36e34ee15..1180785011 100644 --- a/src/renderer/widgets/AddProxyModal/model/__tests__/add-proxy-model.test.ts +++ b/src/renderer/widgets/AddProxyModal/model/__tests__/add-proxy-model.test.ts @@ -12,13 +12,14 @@ import { Step } from '../../lib/types'; import { formModel } from '../form-model'; import { confirmModel } from '../confirm-model'; import { addProxyModel } from '../add-proxy-model'; +import { Transaction } from '@entities/transaction'; jest.mock('@shared/lib/utils', () => ({ ...jest.requireActual('@shared/lib/utils'), getProxyTypes: jest.fn().mockReturnValue(['Any', 'Staking']), })); -describe('widgets/AddPureProxyModal/model/add-proxy-model', () => { +describe('widgets/AddProxyModal/model/add-proxy-model', () => { beforeAll(() => { jest.useFakeTimers(); }); @@ -49,14 +50,19 @@ describe('widgets/AddPureProxyModal/model/add-proxy-model', () => { await allSettled(formModel.output.formSubmitted, { scope, params: { - proxyDeposit: '1', - oldProxyDeposit: '0', - proxyNumber: 1, - chain: testChain, - account: { accountId: '0x00' } as unknown as Account, - delegate: '0x00', - proxyType: ProxyType.ANY, - description: '', + transactions: { + wrappedTx: {} as Transaction, + coreTx: {} as Transaction, + }, + formData: { + proxyDeposit: '1', + proxyNumber: 1, + chain: testChain, + account: { accountId: '0x00' } as unknown as Account, + delegate: '0x00', + proxyType: ProxyType.ANY, + description: '', + }, }, }); diff --git a/src/renderer/widgets/AddProxyModal/model/add-proxy-model.ts b/src/renderer/widgets/AddProxyModal/model/add-proxy-model.ts index c554a01e9e..1d1f77195d 100644 --- a/src/renderer/widgets/AddProxyModal/model/add-proxy-model.ts +++ b/src/renderer/widgets/AddProxyModal/model/add-proxy-model.ts @@ -1,17 +1,16 @@ import { createEvent, createStore, sample } from 'effector'; import { spread, delay } from 'patronum'; -import { Transaction, TransactionType, transactionService, TxWrapper, WrapperKind } from '@entities/transaction'; +import { Transaction } from '@entities/transaction'; import { signModel } from '@features/operations/OperationSign/model/sign-model'; import { submitModel } from '@features/operations/OperationSubmit'; -import { toAddress, toAccountId } from '@shared/lib/utils'; +import { toAccountId } from '@shared/lib/utils'; import { walletSelectModel } from '@features/wallets'; -import { walletModel, walletUtils } from '@entities/wallet'; -import { ProxyGroup, NoID, MultisigAccount, Account } from '@shared/core'; +import { walletModel } from '@entities/wallet'; +import { ProxyGroup, NoID } from '@shared/core'; import { proxyModel, proxyUtils } from '@entities/proxy'; -import { networkModel } from '@entities/network'; import { balanceSubModel } from '@features/balances'; -import { Step, TxWrappers, AddProxyStore } from '../lib/types'; +import { Step, AddProxyStore } from '../lib/types'; import { formModel } from './form-model'; import { confirmModel } from './confirm-model'; @@ -22,12 +21,10 @@ const flowFinished = createEvent(); const $step = createStore(Step.NONE); -const $addProxyStore = createStore(null); -const $wrappedTx = createStore(null); -const $coreTx = createStore(null); -const $multisigTx = createStore(null); - -const $txWrappers = createStore([]); +const $addProxyStore = createStore(null).reset(flowFinished); +const $wrappedTx = createStore(null).reset(flowFinished); +const $coreTx = createStore(null).reset(flowFinished); +const $multisigTx = createStore(null).reset(flowFinished); sample({ clock: stepChanged, target: $step }); @@ -59,77 +56,24 @@ sample({ sample({ clock: formModel.output.formSubmitted, - target: $addProxyStore, -}); - -sample({ - clock: formModel.output.formSubmitted, - source: { - wallet: walletSelectModel.$walletForDetails, - wallets: walletModel.$wallets, - }, - fn: ({ wallet, wallets }, { account }): TxWrappers => { - if (!wallet) return []; - if (walletUtils.isMultisig(wallet)) return ['multisig']; - if (!walletUtils.isProxied(wallet)) return []; - - const accountWallet = walletUtils.getWalletById(wallets, account.walletId); - - return walletUtils.isMultisig(accountWallet) ? ['multisig', 'proxy'] : ['proxy']; - }, - target: $txWrappers, -}); - -sample({ - clock: formModel.output.formSubmitted, - source: { - txWrappers: $txWrappers, - apis: networkModel.$apis, - }, - fn: ({ txWrappers, apis }, formData) => { - const { chain, account, signatory, delegate, proxyType } = formData; - - const transaction: Transaction = { - chainId: chain.chainId, - address: toAddress(account.accountId, { prefix: chain.addressPrefix }), - type: TransactionType.ADD_PROXY, - args: { delegate, proxyType, delay: 0 }, - }; - - const isMultisig = txWrappers.includes('multisig'); - const txWrappersAdapter: TxWrapper[] = isMultisig - ? [ - { - kind: WrapperKind.MULTISIG, - multisigAccount: account as MultisigAccount, - signatories: (account as MultisigAccount).signatories.map((s) => ({ accountId: s.accountId })) as Account[], - signer: { accountId: signatory!.accountId } as Account, - }, - ] - : []; - - const transactions = transactionService.getWrappedTransaction({ - api: apis[chain.chainId], - addressPrefix: chain.addressPrefix, - transaction, - txWrappers: txWrappersAdapter, - }); - - return { ...transactions, multisigTx: transactions.multisigTx || null }; - }, + fn: ({ transactions, formData }) => ({ + wrappedTx: transactions.wrappedTx, + multisigTx: transactions.multisigTx || null, + coreTx: transactions.coreTx, + store: formData, + }), target: spread({ wrappedTx: $wrappedTx, - coreTx: $coreTx, multisigTx: $multisigTx, + coreTx: $coreTx, + store: $addProxyStore, }), }); sample({ clock: formModel.output.formSubmitted, - source: $wrappedTx, - filter: (wrappedTx: Transaction | null): wrappedTx is Transaction => Boolean(wrappedTx), - fn: (wrappedTx, formData) => ({ - event: { ...formData, transaction: wrappedTx }, + fn: ({ formData, transactions }) => ({ + event: { ...formData, transaction: transactions.wrappedTx }, step: Step.CONFIRM, }), target: spread({ @@ -166,7 +110,6 @@ sample({ addProxyStore: $addProxyStore, coreTx: $coreTx, multisigTx: $multisigTx, - txWrappers: $txWrappers, }, filter: (proxyData) => { return Boolean(proxyData.addProxyStore) && Boolean(proxyData.coreTx); diff --git a/src/renderer/widgets/AddProxyModal/model/confirm-model.ts b/src/renderer/widgets/AddProxyModal/model/confirm-model.ts index f55bcef87c..8a406ef3e1 100644 --- a/src/renderer/widgets/AddProxyModal/model/confirm-model.ts +++ b/src/renderer/widgets/AddProxyModal/model/confirm-model.ts @@ -1,6 +1,6 @@ import { createEvent, combine, restore } from 'effector'; -import { Chain, Account, ProxyType, Address } from '@shared/core'; +import { Chain, Account, ProxyType, Address, ProxiedAccount } from '@shared/core'; import { networkModel } from '@entities/network'; import { Transaction } from '@entities/transaction'; import { walletModel, walletUtils } from '@entities/wallet'; @@ -13,15 +13,16 @@ type Input = { delegate: Address; description: string; transaction: Transaction; + proxiedAccount?: ProxiedAccount; - oldProxyDeposit: string; + proxyDeposit: string; proxyNumber: number; }; const formInitiated = createEvent(); const formSubmitted = createEvent(); -const $confirmStore = restore(formInitiated, null); +const $confirmStore = restore(formInitiated, null).reset(formSubmitted); const $api = combine( { @@ -43,6 +44,20 @@ const $initiatorWallet = combine( return walletUtils.getWalletById(wallets, store.account.walletId); }, + { skipVoid: false }, +); + +const $proxiedWallet = combine( + { + store: $confirmStore, + wallets: walletModel.$wallets, + }, + ({ store, wallets }) => { + if (!store || !store.proxiedAccount) return undefined; + + return walletUtils.getWalletById(wallets, store.proxiedAccount.walletId); + }, + { skipVoid: false }, ); const $signerWallet = combine( @@ -55,12 +70,14 @@ const $signerWallet = combine( return walletUtils.getWalletById(wallets, store.signatory?.walletId || store.account.walletId); }, + { skipVoid: false }, ); export const confirmModel = { $confirmStore, $initiatorWallet, $signerWallet, + $proxiedWallet, $api, events: { formInitiated, diff --git a/src/renderer/widgets/AddProxyModal/model/form-model.ts b/src/renderer/widgets/AddProxyModal/model/form-model.ts index e9de8d3219..8541c8f68a 100644 --- a/src/renderer/widgets/AddProxyModal/model/form-model.ts +++ b/src/renderer/widgets/AddProxyModal/model/form-model.ts @@ -4,13 +4,20 @@ import { createForm } from 'effector-forms'; import { BN } from '@polkadot/util'; import { spread } from 'patronum'; -import { Address, ProxyType, Chain, Account, PartialBy } from '@shared/core'; +import { Address, ProxyType, Chain, Account, PartialBy, ProxiedAccount } from '@shared/core'; import { networkModel, networkUtils } from '@entities/network'; import { walletSelectModel } from '@features/wallets'; import { proxiesUtils } from '@features/proxies/lib/proxies-utils'; import { walletUtils, accountUtils, walletModel, permissionUtils } from '@entities/wallet'; import { proxyService } from '@shared/api/proxy'; -import { TransactionType, Transaction, DESCRIPTION_LENGTH } from '@entities/transaction'; +import { + TransactionType, + Transaction, + ProxyTxWrapper, + MultisigTxWrapper, + transactionService, + DESCRIPTION_LENGTH, +} from '@entities/transaction'; import { balanceModel, balanceUtils } from '@entities/balance'; import { getProxyTypes, @@ -39,10 +46,19 @@ type FormParams = { description: string; }; -type FormSubmitEvent = PartialBy & { - proxyDeposit: string; - oldProxyDeposit: string; - proxyNumber: number; +type FormSubmitEvent = { + transactions: { + wrappedTx: Transaction; + multisigTx?: Transaction; + coreTx: Transaction; + }; + formData: PartialBy & { + proxiedAccount?: ProxiedAccount; + fee: string; + multisigDeposit: string; + proxyDeposit: string; + proxyNumber: number; + }; }; const formInitiated = createEvent(); @@ -55,19 +71,19 @@ const feeChanged = createEvent(); const isFeeLoadingChanged = createEvent(); const isProxyDepositLoadingChanged = createEvent(); -const $fee = createStore('0'); -const $oldProxyDeposit = createStore('0'); -const $newProxyDeposit = createStore('0'); -const $multisigDeposit = createStore('0'); -const $isFeeLoading = createStore(true); -const $isProxyDepositLoading = createStore(true); +const $fee = createStore('0').reset(formSubmitted); +const $oldProxyDeposit = createStore('0').reset(formSubmitted); +const $newProxyDeposit = createStore('0').reset(formSubmitted); +const $multisigDeposit = createStore('0').reset(formSubmitted); +const $isFeeLoading = createStore(true).reset(formSubmitted); +const $isProxyDepositLoading = createStore(true).reset(formSubmitted); -const $proxyQuery = createStore(''); -const $maxProxies = createStore(0); -const $activeProxies = createStore([]); +const $proxyQuery = createStore('').reset(formSubmitted); +const $maxProxies = createStore(0).reset(formSubmitted); +const $activeProxies = createStore([]).reset(formSubmitted); -const $isMultisig = createStore(false); -const $isProxy = createStore(false); +const $isMultisig = createStore(false).reset(formSubmitted); +const $isProxy = createStore(false).reset(formSubmitted); const $proxyForm = createForm({ fields: { @@ -188,6 +204,69 @@ const $proxyForm = createForm({ // Options for selectors +const $txWrappers = combine( + { + wallet: walletSelectModel.$walletForDetails, + wallets: walletModel.$wallets, + account: $proxyForm.fields.account.$value, + chain: $proxyForm.fields.chain.$value, + accounts: walletModel.$accounts, + }, + ({ wallet, account, chain, accounts, wallets }) => { + if (!wallet || !chain || !account.id) return []; + + const walletFiltered = wallets.filter((wallet) => { + return !walletUtils.isProxied(wallet) && !walletUtils.isWatchOnly(wallet); + }); + const walletsMap = dictionary(walletFiltered, 'id'); + const chainFilteredAccounts = accounts.filter((account) => { + if (accountUtils.isBaseAccount(account) && walletUtils.isPolkadotVault(walletsMap[account.walletId])) { + return false; + } + + return accountUtils.isChainAndCryptoMatch(account, chain); + }); + + return transactionService.getTxWrappers({ + wallet, + wallets: walletFiltered, + account, + accounts: chainFilteredAccounts, + signatories: [], + }); + }, +); + +const $realAccount = combine( + { + txWrappers: $txWrappers, + account: $proxyForm.fields.account.$value, + }, + ({ txWrappers, account }) => { + if (txWrappers.length === 0) return account; + + if (transactionService.hasMultisig([txWrappers[0]])) { + return (txWrappers[0] as MultisigTxWrapper).multisigAccount; + } + + return (txWrappers[0] as ProxyTxWrapper).proxyAccount; + }, +); + +const $proxyWallet = combine( + { + isProxy: $isProxy, + proxyAccount: $realAccount, + wallets: walletModel.$wallets, + }, + ({ isProxy, proxyAccount, wallets }) => { + if (!isProxy) return undefined; + + return walletUtils.getWalletById(wallets, proxyAccount.walletId); + }, + { skipVoid: false }, +); + const $proxyChains = combine(networkModel.$chains, (chains) => { return Object.values(chains).filter(proxiesUtils.isRegularProxy); }); @@ -324,6 +403,49 @@ const $api = combine( { skipVoid: false }, ); +const $pureTx = combine( + { + form: $proxyForm.$values, + account: $realAccount, + isConnected: $isChainConnected, + }, + ({ form, account, isConnected }): Transaction | undefined => { + if (!isConnected || !account) return undefined; + + return { + chainId: form.chain.chainId, + address: toAddress(account.accountId, { prefix: form.chain.addressPrefix }), + type: TransactionType.ADD_PROXY, + args: { + delegate: toAddress(form.delegate, { prefix: form.chain.addressPrefix }), + proxyType: form.proxyType, + delay: 0, + }, + }; + }, + { skipVoid: false }, +); + +const $transaction = combine( + { + apis: networkModel.$apis, + chain: $proxyForm.fields.chain.$value, + pureTx: $pureTx, + txWrappers: $txWrappers, + }, + ({ apis, chain, pureTx, txWrappers }) => { + if (!chain || !pureTx) return undefined; + + return transactionService.getWrappedTransaction({ + api: apis[chain.chainId], + addressPrefix: chain.addressPrefix, + transaction: pureTx, + txWrappers, + }); + }, + { skipVoid: false }, +); + const $fakeTx = combine( { chain: $proxyForm.fields.chain.$value, @@ -529,12 +651,19 @@ sample({ clock: isFeeLoadingChanged, target: $isFeeLoading }); sample({ clock: $proxyForm.formValidated, source: { - newProxyDeposit: $newProxyDeposit, - oldProxyDeposit: $oldProxyDeposit, + realAccount: $realAccount, + transaction: $transaction, + isProxy: $isProxy, + fee: $fee, + multisigDeposit: $multisigDeposit, + proxyDeposit: $newProxyDeposit, proxies: $activeProxies, }, - fn: ({ newProxyDeposit, oldProxyDeposit, proxies }, formData) => { - const signatory = Object.keys(formData.signatory).length > 0 ? formData.signatory : undefined; + filter: ({ transaction }) => { + return Boolean(transaction); + }, + fn: ({ proxyDeposit, multisigDeposit, proxies, realAccount, transaction, isProxy, fee }, formData) => { + const signatory = formData.signatory.accountId ? formData.signatory : undefined; const proxiedAddress = toAddress(formData.account.accountId, { prefix: formData.chain.addressPrefix, }); @@ -542,12 +671,22 @@ sample({ const description = signatory ? formData.description || multisigDescription : ''; return { - ...formData, - signatory, - description, - proxyDeposit: newProxyDeposit, - oldProxyDeposit, - proxyNumber: proxies.length + 1, + transactions: { + wrappedTx: transaction!.wrappedTx, + multisigTx: transaction!.multisigTx, + coreTx: transaction!.coreTx, + }, + formData: { + ...formData, + fee, + account: realAccount, + signatory, + description, + proxyDeposit, + multisigDeposit, + proxyNumber: proxies.length, + ...(isProxy && { proxiedAccount: formData.account as ProxiedAccount }), + }, }; }, target: formSubmitted, @@ -561,6 +700,7 @@ export const formModel = { $proxyAccounts, $proxyTypes, $proxyQuery, + $proxyWallet, $activeProxies, $oldProxyDeposit, diff --git a/src/renderer/widgets/AddProxyModal/ui/Confirmation.tsx b/src/renderer/widgets/AddProxyModal/ui/Confirmation.tsx index e42f341cad..371975cff4 100644 --- a/src/renderer/widgets/AddProxyModal/ui/Confirmation.tsx +++ b/src/renderer/widgets/AddProxyModal/ui/Confirmation.tsx @@ -19,6 +19,8 @@ export const Confirmation = ({ onGoBack }: Props) => { const confirmStore = useUnit(confirmModel.$confirmStore); const initiatorWallet = useUnit(confirmModel.$initiatorWallet); const signerWallet = useUnit(confirmModel.$signerWallet); + const proxiedWallet = useUnit(confirmModel.$proxiedWallet); + const api = useUnit(confirmModel.$api); if (!confirmStore || !api || !initiatorWallet) return null; @@ -37,21 +39,63 @@ export const Confirmation = ({ onGoBack }: Props) => {
- - - {initiatorWallet.name} - + {proxiedWallet && confirmStore.proxiedAccount && ( + <> + + + {proxiedWallet.name} + + + + + + +
+ + + + {initiatorWallet.name} + + + + + + + )} - - - + {!proxiedWallet && ( + <> + + + {initiatorWallet.name} + + + + + + + )} {signerWallet && confirmStore.signatory && ( @@ -84,8 +128,8 @@ export const Confirmation = ({ onGoBack }: Props) => { diff --git a/src/renderer/widgets/RemoveProxy/model/__tests__/remove-proxy-model.test.ts b/src/renderer/widgets/RemoveProxy/model/__tests__/remove-proxy-model.test.ts deleted file mode 100644 index 67d4451021..0000000000 --- a/src/renderer/widgets/RemoveProxy/model/__tests__/remove-proxy-model.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { allSettled, fork } from 'effector'; -import { ApiPromise } from '@polkadot/api'; - -import type { Chain, HexString, ProxyAccount, Wallet } from '@shared/core'; -import { Account, AccountType, ProxyType, SigningType, WalletType } from '@shared/core'; -import { walletModel } from '@entities/wallet'; -import { removeProxyModel } from '@widgets/RemoveProxy/model/remove-proxy-model'; -import { TEST_CHAIN_ICON, TEST_CHAIN_ID } from '@shared/lib/utils'; -import { storageService } from '@shared/api/storage'; -import { proxyModel } from '@entities/proxy'; - -const proxyAccountMock = { - id: 1, - chainId: TEST_CHAIN_ID, - accountId: '0x00' as HexString, - proxiedAccountId: '0x01' as HexString, - proxyType: ProxyType.ANY, - delay: 0, -} as ProxyAccount; - -const proxiedAccountMock: Account = { - id: 2, - chainId: '0x00' as HexString, - accountId: '0x01' as HexString, - walletId: 1, - name: '', - type: AccountType.BASE, - chainType: 0, - cryptoType: 0, -}; - -const chainMock: Chain = { - chainId: TEST_CHAIN_ID, - specName: 'name', - name: 'name', - assets: [], - nodes: [], - icon: TEST_CHAIN_ICON, - addressPrefix: 42, -}; - -const proxiedWalletMock: Wallet = { - id: 1, - name: 'proxied', - type: WalletType.SINGLE_PARITY_SIGNER, - isActive: false, - signingType: SigningType.PARITY_SIGNER, -}; - -describe('widgets/RemoveProxy/model/remove-proxy-model', () => { - afterEach(() => { - jest.restoreAllMocks(); - }); - - test('should set proxied account and wallet when flow starts', async () => { - const scope = fork({ - values: new Map().set(walletModel.$wallets, [proxiedWalletMock]).set(walletModel.$accounts, [proxiedAccountMock]), - }); - - await allSettled(removeProxyModel.events.removeStarted, { - scope, - params: { proxyAccount: proxyAccountMock, chain: chainMock }, - }); - - expect(scope.getState(removeProxyModel.$proxiedAccount)).toEqual(proxiedAccountMock); - expect(scope.getState(removeProxyModel.$proxiedWallet)).toEqual(proxiedWalletMock); - }); - - test('should delete proxy from app when flow successfully finished', async () => { - jest.spyOn(storageService.proxies, 'deleteAll').mockResolvedValue([1]); - - const scope = fork({ - values: new Map() - .set(proxyModel.$proxies, { '0x01': [proxyAccountMock] }) - .set(removeProxyModel.$proxyAccount, proxyAccountMock) - .set(proxyModel.$proxyGroups, [ - { - id: 1, - walletId: 1, - proxiedAccountId: '0x01', - chainId: TEST_CHAIN_ID, - totalDeposit: '11111111', - }, - ]), - }); - - await allSettled(removeProxyModel.events.proxyRemoved, { scope, params: {} as ApiPromise }); - - expect(scope.getState(proxyModel.$proxies)).toEqual({}); - }); -}); diff --git a/src/renderer/widgets/RemoveProxy/model/remove-proxy-model.ts b/src/renderer/widgets/RemoveProxy/model/remove-proxy-model.ts deleted file mode 100644 index f7edc45544..0000000000 --- a/src/renderer/widgets/RemoveProxy/model/remove-proxy-model.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { UnsignedTransaction } from '@substrate/txwrapper-polkadot'; -import { combine, createEvent, createStore, sample } from 'effector'; -import { spread } from 'patronum'; -import { ApiPromise } from '@polkadot/api'; -import { BN } from '@polkadot/util'; - -import { Account, Chain, HexString, ProxyAccount, ProxyGroup } from '@shared/core'; -import { accountUtils, walletModel, walletUtils } from '@entities/wallet'; -import { dictionary } from '@shared/lib/utils'; -import { getSignatoryAccounts } from '@pages/Operations/common/utils'; -import { proxyModel } from '@entities/proxy'; - -export const enum Step { - CONFIRMATION, - SIGNING, - SUBMIT, -} - -type FlowStartedProps = { - chain: Chain; - proxyAccount: ProxyAccount; -}; -const removeStarted = createEvent(); -const signatorySelected = createEvent(); -const activeStepChanged = createEvent(); - -type TransactionSignedProps = { - signature: HexString; - unsignedTx: UnsignedTransaction; -}; -const transactionSigned = createEvent(); -const proxyRemoved = createEvent(); -type ProxyGroupUpdatedProps = { - proxyGroup: ProxyGroup; - shouldDelete: boolean; -}; -const proxyGroupUpdated = createEvent(); - -const $chain = createStore(null); -const $proxyAccount = createStore(null); -const $activeStep = createStore(Step.CONFIRMATION).reset(removeStarted); -const $signatory = createStore(null).reset(removeStarted); - -const $unsignedTx = createStore(null).reset(removeStarted); -const $signature = createStore(null).reset(removeStarted); - -const $proxiedAccount = combine( - { - wallets: walletModel.$wallets, - accounts: walletModel.$accounts, - proxyAccount: $proxyAccount, - }, - ({ wallets, accounts, proxyAccount }) => { - if (!proxyAccount) return null; - - const walletsMap = dictionary(wallets, 'id'); - - return ( - accounts.find( - (a) => a.accountId === proxyAccount.proxiedAccountId && !walletUtils.isWatchOnly(walletsMap[a.walletId]), - ) || null - ); - }, - { skipVoid: false }, -); - -const $proxiedWallet = combine( - { - wallets: walletModel.$wallets, - proxiedAccount: $proxiedAccount, - }, - ({ wallets, proxiedAccount }) => { - if (!proxiedAccount) return null; - - return wallets.find((w) => w.id === proxiedAccount.walletId) || null; - }, - { skipVoid: false }, -); - -const $signatories = combine( - { - wallets: walletModel.$wallets, - accounts: walletModel.$accounts, - proxiedAccount: $proxiedAccount, - chain: $chain, - }, - ({ wallets, accounts, proxiedAccount, chain }) => { - if (!chain || !proxiedAccount) return null; - - return accountUtils.isMultisigAccount(proxiedAccount) - ? getSignatoryAccounts(accounts, wallets, [], proxiedAccount.signatories, chain.chainId) - : null; - }, - { skipVoid: false }, -); - -sample({ - source: removeStarted, - target: spread({ - targets: { chain: $chain, proxyAccount: $proxyAccount }, - }), -}); - -sample({ - source: transactionSigned, - target: spread({ - targets: { unsignedTx: $unsignedTx, signature: $signature }, - }), -}); - -sample({ - clock: transactionSigned, - fn: () => Step.SUBMIT, - target: $activeStep, -}); - -sample({ - source: signatorySelected, - target: $signatory, -}); - -sample({ - source: signatorySelected, - fn: () => Step.SIGNING, - target: $activeStep, -}); - -sample({ - source: activeStepChanged, - target: $activeStep, -}); - -sample({ - clock: proxyRemoved, - source: { - proxyAccount: $proxyAccount, - proxies: proxyModel.$proxies, - proxyGroups: proxyModel.$proxyGroups, - }, - filter: (proxyAccount) => Boolean(proxyAccount), - fn: ({ proxyAccount, proxies, proxyGroups }, api) => { - // find other proxies in the same group as proxy we are removing - const sameGroupProxies = proxies[proxyAccount!.proxiedAccountId].filter((p) => p.chainId === proxyAccount!.chainId); - const proxyGroup = proxyGroups.find( - (p) => p.chainId === proxyAccount!.chainId && p.proxiedAccountId === proxyAccount!.proxiedAccountId, - ); - - if (sameGroupProxies.length === 1 && proxyGroup) { - return { proxyGroup, shouldDelete: true }; - } - - const updatedDeposit = new BN(proxyGroup!.totalDeposit) - .sub(api.consts.proxy.proxyDepositBase.toBn()) - .sub(api.consts.proxy.proxyDepositFactor); - - return { - proxyGroup: { ...proxyGroup, totalDeposit: updatedDeposit.toString() } as ProxyGroup, - shouldDelete: false, - }; - }, - target: proxyGroupUpdated, -}); - -sample({ - clock: proxyGroupUpdated, - filter: ({ shouldDelete }) => shouldDelete, - fn: ({ proxyGroup }) => [proxyGroup], - target: proxyModel.events.proxyGroupsRemoved, -}); - -sample({ - clock: proxyGroupUpdated, - filter: ({ shouldDelete }) => !shouldDelete, - fn: ({ proxyGroup }) => [proxyGroup], - target: proxyModel.events.proxyGroupsUpdated, -}); - -sample({ - clock: proxyRemoved, - source: $proxyAccount, - filter: (proxyAccount) => Boolean(proxyAccount), - fn: (proxyAccount) => [proxyAccount!], - target: proxyModel.events.proxiesRemoved, -}); - -export const removeProxyModel = { - $chain, - $proxiedWallet, - $proxiedAccount, - $proxyAccount, - $activeStep, - $unsignedTx, - $signature, - $signatories, - $signatory, - events: { - removeStarted, - signatorySelected, - transactionSigned, - activeStepChanged, - proxyRemoved, - }, -}; diff --git a/src/renderer/widgets/RemoveProxy/ui/Confirmation.tsx b/src/renderer/widgets/RemoveProxy/ui/Confirmation.tsx deleted file mode 100644 index bf22159017..0000000000 --- a/src/renderer/widgets/RemoveProxy/ui/Confirmation.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { useState } from 'react'; - -import { Fee, Transaction } from '@entities/transaction'; -import { Button, DetailRow, FootnoteText, Icon } from '@shared/ui'; -import { ExtendedChain } from '@entities/network'; -import { useI18n } from '@app/providers'; -import { SignButton } from '@entities/operation/ui/SignButton'; -import { Account, ProxyAccount, Wallet } from '@shared/core'; -import { AddressWithExplorers, WalletIcon } from '@entities/wallet'; -import { proxyUtils } from '@entities/proxy'; - -type Props = { - transaction: Transaction; - proxyAccount: ProxyAccount; - proxiedAccount: Account | null; - proxiedWallet: Wallet | null; - connection: ExtendedChain; - onResult?: () => void; - onBack?: () => void; -}; - -export const Confirmation = ({ - proxyAccount, - proxiedAccount, - proxiedWallet, - connection, - transaction, - onResult, - onBack, -}: Props) => { - const { t } = useI18n(); - - const [feeLoaded, setFeeLoaded] = useState(false); - - const { addressPrefix, explorers } = connection; - - if (!proxiedWallet || !proxiedAccount) return null; - - return ( -
-
- -
- -
- - - {proxiedWallet.name} - - - - - - -
- - - {t(proxyUtils.getProxyTypeName(proxyAccount.proxyType))} - - - - - - -
- - - {connection.api && transaction && ( - setFeeLoaded(Boolean(fee))} - /> - )} - -
- -
- - - -
-
- ); -}; diff --git a/src/renderer/widgets/RemoveProxy/ui/RemoveProxy.tsx b/src/renderer/widgets/RemoveProxy/ui/RemoveProxy.tsx deleted file mode 100644 index f831a3fbca..0000000000 --- a/src/renderer/widgets/RemoveProxy/ui/RemoveProxy.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import { useEffect } from 'react'; -import { useUnit } from 'effector-react'; -import { BN } from '@polkadot/util'; -import { UnsignedTransaction } from '@substrate/txwrapper-polkadot'; - -import { Account, HexString, MultisigAccount, ProxyAccount } from '@shared/core'; -import { OperationTitle } from '@entities/chain'; -import { BaseModal, Button, Loader } from '@shared/ui'; -import { useI18n } from '@app/providers'; -import { Confirmation } from './Confirmation'; -import { networkModel, useNetworkData } from '@entities/network'; -import { OperationResult, TransactionType, useTransaction, validateBalance } from '@entities/transaction'; -import { SigningSwitch } from '@features/operations'; -import { toAddress, transferableAmount } from '@shared/lib/utils'; -import { SignatorySelectModal } from '@pages/Operations/components/modals/SignatorySelectModal'; -import { useToggle } from '@shared/lib/hooks'; -import { balanceModel, balanceUtils } from '@entities/balance'; -import { Submit } from './Submit'; -import { accountUtils } from '@entities/wallet'; -import { removeProxyModel, Step } from '../model/remove-proxy-model'; - -type Props = { - isOpen: boolean; - proxyAccount: ProxyAccount; - onClose: () => void; -}; - -export const RemoveProxy = ({ isOpen, proxyAccount, onClose }: Props) => { - const { t } = useI18n(); - - const proxiedAccount = useUnit(removeProxyModel.$proxiedAccount); - const proxiedWallet = useUnit(removeProxyModel.$proxiedWallet); - const activeStep = useUnit(removeProxyModel.$activeStep); - const signatory = useUnit(removeProxyModel.$signatory); - const unsignedTx = useUnit(removeProxyModel.$unsignedTx); - const signature = useUnit(removeProxyModel.$signature); - const signatories = useUnit(removeProxyModel.$signatories); - const chain = useUnit(networkModel.$chains)[proxyAccount.chainId]; - - const [isSelectAccountModalOpen, toggleSelectAccountModal] = useToggle(); - const [isFeeModalOpen, toggleFeeModal] = useToggle(); - - const balances = useUnit(balanceModel.$balances); - - const { api, extendedChain } = useNetworkData(chain.chainId); - const { getTransactionFee, setTxs, txs, setWrappers, wrapTx, buildTransaction } = useTransaction(); - - const nativeToken = chain.assets[0]; - const transaction = txs[0]; - const { addressPrefix } = chain; - const isMultisigAccount = proxiedAccount && accountUtils.isMultisigAccount(proxiedAccount); - - useEffect(() => { - setTxs([ - buildTransaction( - TransactionType.REMOVE_PROXY, - toAddress(proxyAccount.proxiedAccountId, { prefix: addressPrefix }), - chain.chainId, - { - delegate: toAddress(proxyAccount.accountId, { prefix: addressPrefix }), - proxyType: proxyAccount.proxyType, - delay: proxyAccount.delay, - }, - ), - ]); - - removeProxyModel.events.removeStarted({ proxyAccount, chain }); - }, [proxyAccount]); - - useEffect(() => { - if (signatory && proxiedAccount) { - setWrappers([ - { - signatoryId: signatory.accountId, - account: proxiedAccount as MultisigAccount, - }, - ]); - } - }, [proxiedAccount, signatory]); - - const validateBalanceForFee = async (signAccount: Account): Promise => { - if (!extendedChain.api || !transaction || !signAccount.accountId || !nativeToken) return false; - - const fee = await getTransactionFee(transaction, extendedChain.api); - const balance = balanceUtils.getBalance( - balances, - signAccount.accountId, - extendedChain.chainId, - nativeToken.assetId.toString(), - ); - - if (!balance) return false; - - return new BN(fee).lte(new BN(transferableAmount(balance))); - }; - - const selectSignatoryAccount = async (account: Account) => { - toggleSelectAccountModal(); - - const isValid = await validateBalanceForFee(account); - - if (isValid) { - removeProxyModel.events.signatorySelected(account); - } else { - toggleFeeModal(); - } - }; - - const handleConfirmation = () => { - if (isMultisigAccount) { - trySetSignatoryAccount(); - } else { - removeProxyModel.events.activeStepChanged(Step.SIGNING); - } - }; - - const trySetSignatoryAccount = () => { - if (signatories?.length === 1) { - removeProxyModel.events.signatorySelected(signatories[0]); - } else { - toggleSelectAccountModal(); - } - }; - - const onSignResult = (signature: HexString[], tx: UnsignedTransaction[]) => { - removeProxyModel.events.transactionSigned({ unsignedTx: tx[0], signature: signature[0] }); - }; - - const checkBalance = () => - validateBalance({ - api: extendedChain.api, - chainId: extendedChain.chainId, - transaction: transaction, - assetId: nativeToken.assetId.toString(), - getBalance: balanceUtils.getBalanceWrapped(balances), - getTransactionFee, - }); - - if (activeStep === Step.SUBMIT) { - return api ? ( - removeProxyModel.events.proxyRemoved(api)} - onClose={onClose} - /> - ) : ( -
- -
- ); - } - - return ( - - } - contentClass="" - panelClass="w-[440px]" - headerClass="py-3 pl-5 pr-3" - onClose={onClose} - > - {activeStep === Step.CONFIRMATION && ( - - )} - - {activeStep === Step.SIGNING && proxiedWallet && proxiedAccount && ( - removeProxyModel.events.activeStepChanged(Step.CONFIRMATION)} - onResult={onSignResult} - /> - )} - - {signatories && ( - - )} - - - - - - ); -}; diff --git a/src/renderer/widgets/RemoveProxy/ui/Submit.tsx b/src/renderer/widgets/RemoveProxy/ui/Submit.tsx deleted file mode 100644 index a3a085ee2c..0000000000 --- a/src/renderer/widgets/RemoveProxy/ui/Submit.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { ApiPromise } from '@polkadot/api'; -import { UnsignedTransaction } from '@substrate/txwrapper-polkadot'; -import { ComponentProps, useEffect, useState } from 'react'; -import { useUnit } from 'effector-react'; - -import { useI18n, useMultisigChainContext } from '@app/providers'; -import { - ExtrinsicResultParams, - MultisigTransaction, - OperationResult, - Transaction, - useTransaction, -} from '@entities/transaction'; -import type { Account, MultisigAccount } from '@shared/core'; -import { HexString } from '@shared/core'; -import { buildMultisigTx, useMultisigEvent, useMultisigTx } from '@entities/multisig'; -import { toAccountId } from '@shared/lib/utils'; -import { useToggle } from '@shared/lib/hooks'; -import { Button } from '@shared/ui'; -import { accountUtils } from '@entities/wallet'; -import { matrixModel } from '@entities/matrix'; - -type ResultProps = Pick, 'title' | 'description' | 'variant'>; - -type Props = { - api: ApiPromise; - account?: Account | MultisigAccount | null; - tx: Transaction; - multisigTx?: Transaction; - unsignedTx: UnsignedTransaction | null; - signature: HexString | null; - onClose: () => void; - onSubmitted: () => void; -}; - -export const Submit = ({ api, tx, multisigTx, account, unsignedTx, signature, onClose, onSubmitted }: Props) => { - const { t } = useI18n(); - - const matrix = useUnit(matrixModel.$matrix); - - const { addTask } = useMultisigChainContext(); - const { addMultisigTx } = useMultisigTx({ addTask }); - const { submitAndWatchExtrinsic, getSignedExtrinsic } = useTransaction(); - const { addEventWithQueue } = useMultisigEvent({ addTask }); - - const [inProgress, toggleInProgress] = useToggle(true); - const [successMessage, toggleSuccessMessage] = useToggle(); - const [errorMessage, setErrorMessage] = useState(''); - - const isMultisigAccount = account && accountUtils.isMultisigAccount(account); - - const handleClose = () => { - onClose(); - }; - - const closeSuccessMessage = () => { - onClose(); - onSubmitted(); - toggleSuccessMessage(); - }; - - const closeErrorMessage = () => { - onClose(); - setErrorMessage(''); - }; - - useEffect(() => { - signature && submitExtrinsic(signature).catch(() => console.warn('Error getting signed extrinsics')); - }, []); - - const submitExtrinsic = async (signature: HexString) => { - if (!unsignedTx) return; - - const extrinsic = await getSignedExtrinsic(unsignedTx, signature, api); - - submitAndWatchExtrinsic(extrinsic, unsignedTx, api, async (executed, params) => { - if (executed) { - if (multisigTx && isMultisigAccount) { - const result = buildMultisigTx(tx, multisigTx, params as ExtrinsicResultParams, account); - - await Promise.all([addMultisigTx(result.transaction), addEventWithQueue(result.event)]); - - console.log(`New removeProxy transaction was created with call hash ${result.transaction.callHash}`); - - if (matrix.userIsLoggedIn && account.matrixRoomId) { - sendMultisigEvent(account.matrixRoomId, result.transaction, params as ExtrinsicResultParams); - } - } - - toggleSuccessMessage(); - setTimeout(closeSuccessMessage, 2000); - } else { - setErrorMessage(params as string); - } - - toggleInProgress(); - }); - }; - - const sendMultisigEvent = (matrixRoomId: string, updatedTx: MultisigTransaction, params: ExtrinsicResultParams) => { - if (!multisigTx) return; - - matrix - .sendApprove(matrixRoomId, { - senderAccountId: toAccountId(multisigTx.address), - chainId: updatedTx.chainId, - callHash: updatedTx.callHash, - callData: updatedTx.callData, - extrinsicTimepoint: params.timepoint, - extrinsicHash: params.extrinsicHash, - error: Boolean(params.multisigError), - callTimepoint: { - height: updatedTx.blockCreated || params.timepoint.height, - index: updatedTx.indexCreated || params.timepoint.index, - }, - }) - .catch(console.warn); - }; - - const getResultProps = (): ResultProps => { - if (inProgress) { - return { title: t('transfer.inProgress'), variant: 'loading' }; - } - if (successMessage) { - return { title: t('transfer.successMessage'), variant: 'success' }; - } - if (errorMessage) { - return { title: t('operation.feeErrorTitle'), description: errorMessage, variant: 'error' }; - } - - return { title: '' }; - }; - - return ( - - {errorMessage && } - - ); -}; diff --git a/src/renderer/widgets/RemoveProxy/index.ts b/src/renderer/widgets/RemoveProxyModal/index.ts similarity index 100% rename from src/renderer/widgets/RemoveProxy/index.ts rename to src/renderer/widgets/RemoveProxyModal/index.ts diff --git a/src/renderer/widgets/RemoveProxyModal/lib/remove-proxy-utils.ts b/src/renderer/widgets/RemoveProxyModal/lib/remove-proxy-utils.ts new file mode 100644 index 0000000000..3d4a8079a1 --- /dev/null +++ b/src/renderer/widgets/RemoveProxyModal/lib/remove-proxy-utils.ts @@ -0,0 +1,29 @@ +import { Step } from './types'; + +export const removeProxyUtils = { + isNoneStep, + isInitStep, + isConfirmStep, + isSignStep, + isSubmitStep, +}; + +function isNoneStep(step: Step): boolean { + return step === Step.NONE; +} + +function isInitStep(step: Step): boolean { + return step === Step.INIT; +} + +function isConfirmStep(step: Step): boolean { + return step === Step.CONFIRM; +} + +function isSignStep(step: Step): boolean { + return step === Step.SIGN; +} + +function isSubmitStep(step: Step): boolean { + return step === Step.SUBMIT; +} diff --git a/src/renderer/widgets/RemoveProxyModal/lib/types.ts b/src/renderer/widgets/RemoveProxyModal/lib/types.ts new file mode 100644 index 0000000000..a8d49282aa --- /dev/null +++ b/src/renderer/widgets/RemoveProxyModal/lib/types.ts @@ -0,0 +1,25 @@ +import { Address, Chain, Account, ProxyType } from '@shared/core'; + +export const enum Step { + NONE, + INIT, + CONFIRM, + SIGN, + SUBMIT, +} + +export const enum SubmitStep { + LOADING, + SUCCESS, + ERROR, +} + +export type RemoveProxyStore = { + chain: Chain; + account: Account; + signatory?: Account; + delegate: Address; + proxyType: ProxyType; + delay: number; + description: string; +}; diff --git a/src/renderer/widgets/RemoveProxyModal/model/confirm-model.ts b/src/renderer/widgets/RemoveProxyModal/model/confirm-model.ts new file mode 100644 index 0000000000..52e9e9fc29 --- /dev/null +++ b/src/renderer/widgets/RemoveProxyModal/model/confirm-model.ts @@ -0,0 +1,106 @@ +import { createEvent, combine, restore } from 'effector'; + +import { Account, Address, Chain, ProxiedAccount, ProxyType } from '@shared/core'; +import { networkModel } from '@entities/network'; +import { Transaction } from '@entities/transaction'; +import { walletModel, walletUtils } from '@entities/wallet'; + +type Input = { + signatory?: Account; + description: string; + transaction: Transaction; + delegate: Address; + proxyType: ProxyType; + chain?: Chain; + account?: Account; + proxiedAccount?: ProxiedAccount; +}; + +const formInitiated = createEvent(); +const formSubmitted = createEvent(); + +const $confirmStore = restore(formInitiated, null); + +const $api = combine( + { + apis: networkModel.$apis, + store: $confirmStore, + }, + ({ apis, store }) => { + return store && store.chain ? apis[store.chain.chainId] : null; + }, +); + +const $initiatorWallet = combine( + { + store: $confirmStore, + wallets: walletModel.$wallets, + }, + ({ store, wallets }) => { + if (!store || !store.account) return null; + + return walletUtils.getWalletById(wallets, store.account.walletId); + }, + { skipVoid: false }, +); + +const $proxyWallet = combine( + { + store: $confirmStore, + wallets: walletModel.$wallets, + accounts: walletModel.$accounts, + }, + ({ store, wallets, accounts }) => { + if (!store || !store.account) return null; + + const account = accounts.find((a) => a.accountId === (store.account as ProxiedAccount).proxyAccountId); + + if (!account) return null; + + return walletUtils.getWalletById(wallets, account.walletId); + }, + { skipVoid: false }, +); + +const $signerWallet = combine( + { + store: $confirmStore, + wallets: walletModel.$wallets, + }, + ({ store, wallets }) => { + if (!store || !store.account) return null; + + return walletUtils.getWalletById(wallets, store.signatory?.walletId || store.account.walletId); + }, + { skipVoid: false }, +); + +const $proxiedWallet = combine( + { + store: $confirmStore, + wallets: walletModel.$wallets, + }, + ({ store, wallets }) => { + if (!store || !store.proxiedAccount) return undefined; + + return walletUtils.getWalletById(wallets, store.proxiedAccount.walletId); + }, + { skipVoid: false }, +); + +export const confirmModel = { + $confirmStore, + $initiatorWallet, + $signerWallet, + $proxyWallet, + $proxiedWallet, + $api, + + events: { + formInitiated, + }, + + output: { + formSubmitted, + }, +}; diff --git a/src/renderer/widgets/RemoveProxyModal/model/form-model.ts b/src/renderer/widgets/RemoveProxyModal/model/form-model.ts new file mode 100644 index 0000000000..44a459d746 --- /dev/null +++ b/src/renderer/widgets/RemoveProxyModal/model/form-model.ts @@ -0,0 +1,436 @@ +import { createEvent, createStore, sample, combine, createEffect, restore } from 'effector'; +import { ApiPromise } from '@polkadot/api'; +import { createForm } from 'effector-forms'; +import { BN } from '@polkadot/util'; +import { spread } from 'patronum'; + +import { Address, ProxyType, Account, PartialBy, Chain, ProxiedAccount } from '@shared/core'; +import { networkModel, networkUtils } from '@entities/network'; +import { walletSelectModel } from '@features/wallets'; +import { proxiesUtils } from '@features/proxies/lib/proxies-utils'; +import { walletUtils, accountUtils, walletModel, permissionUtils } from '@entities/wallet'; +import { proxyService } from '@shared/api/proxy'; +import { TransactionType, Transaction } from '@entities/transaction'; +import { balanceModel, balanceUtils } from '@entities/balance'; +import { + getProxyTypes, + isStringsMatchQuery, + toAddress, + TEST_ACCOUNTS, + dictionary, + transferableAmount, +} from '@shared/lib/utils'; + +type ProxyAccounts = { + accounts: { + address: Address; + proxyType: ProxyType; + }[]; + deposit: string; +}; + +type FormParams = { + signatory: Account; + description: string; +}; + +type FormSubmitEvent = PartialBy; +type Input = { + chain?: Chain; + account?: ProxiedAccount; +}; + +const formInitiated = createEvent(); +const formSubmitted = createEvent(); +const proxyQueryChanged = createEvent(); + +const proxyDepositChanged = createEvent(); +const multisigDepositChanged = createEvent(); +const feeChanged = createEvent(); +const isFeeLoadingChanged = createEvent(); +const isProxyDepositLoadingChanged = createEvent(); + +const $formStore = restore(formInitiated, null); + +const $multisigDeposit = restore(multisigDepositChanged, '0'); +const $fee = restore(feeChanged, '0'); +const $isFeeLoading = restore(isFeeLoadingChanged, false); + +const $proxyQuery = createStore(''); +const $activeProxies = createStore([]); + +const $isMultisig = createStore(false); +const $isProxy = createStore(false); + +const $chain = $formStore.map((store) => (store ? store.chain : null), { skipVoid: false }); +const $account = $formStore.map((store) => (store ? store.account : null), { skipVoid: false }); + +const $proxyForm = createForm({ + fields: { + signatory: { + init: {} as Account, + rules: [ + { + name: 'notEnoughTokens', + errorText: 'proxy.addProxy.notEnoughMultisigTokens', + source: combine({ + fee: $fee, + balances: balanceModel.$balances, + isMultisig: $isMultisig, + chain: $chain, + }), + validator: (value, form, { isMultisig, balances, chain, ...params }) => { + if (!isMultisig) return true; + + const signatoryBalance = balanceUtils.getBalance( + balances, + value.accountId, + chain.chainId, + chain.assets[0].assetId.toString(), + ); + + return new BN(params.multisigDeposit) + .add(new BN(params.fee)) + .lte(new BN(transferableAmount(signatoryBalance))); + }, + }, + ], + }, + description: { + init: '', + rules: [ + { + name: 'maxLength', + validator: (value) => !value || value.length <= 120, + }, + ], + }, + }, + validateOn: ['submit'], +}); + +// Options for selectors + +const $proxyChains = combine(networkModel.$chains, (chains) => { + return Object.values(chains).filter(proxiesUtils.isRegularProxy); +}); + +const $proxiedAccounts = combine( + { + wallet: walletSelectModel.$walletForDetails, + accounts: walletModel.$accounts, + chain: $chain, + balances: balanceModel.$balances, + }, + ({ wallet, accounts, chain, balances }) => { + if (!wallet || !chain) return []; + + const isPolkadotVault = walletUtils.isPolkadotVault(wallet); + const walletAccounts = accountUtils.getWalletAccounts(wallet.id, accounts).filter((account) => { + if (isPolkadotVault && accountUtils.isBaseAccount(account)) return false; + + return accountUtils.isChainAndCryptoMatch(account, chain); + }); + + return walletAccounts.map((account) => { + const balance = balanceUtils.getBalance( + balances, + account.accountId, + chain.chainId, + chain.assets[0].assetId.toString(), + ); + + return { account, balance: transferableAmount(balance) }; + }); + }, +); + +const $signatories = combine( + { + wallet: walletSelectModel.$walletForDetails, + wallets: walletModel.$wallets, + account: $account, + chain: $chain, + accounts: walletModel.$accounts, + balances: balanceModel.$balances, + }, + ({ wallet, wallets, account, accounts, chain, balances }) => { + if (!wallet || !chain || !account || !accountUtils.isMultisigAccount(account)) return []; + + const signers = dictionary(account.signatories, 'accountId', () => true); + + return wallets.reduce<{ signer: Account; balance: string }[]>((acc, wallet) => { + const walletAccounts = accountUtils.getWalletAccounts(wallet.id, accounts); + const isAvailable = permissionUtils.canCreateMultisigTx(wallet, walletAccounts); + + if (!isAvailable) return acc; + + const signer = walletAccounts.find((a) => { + return signers[a.accountId] && accountUtils.isChainAndCryptoMatch(a, chain); + }); + + if (signer) { + const balance = balanceUtils.getBalance( + balances, + signer.accountId, + chain.chainId, + chain.assets[0].assetId.toString(), + ); + + acc.push({ signer, balance: transferableAmount(balance) }); + } + + return acc; + }, []); + }, +); + +const $proxyAccounts = combine( + { + wallets: walletModel.$wallets, + accounts: walletModel.$accounts, + chain: $chain, + query: $proxyQuery, + }, + ({ wallets, accounts, chain, query }) => { + if (!chain) return []; + + return accountUtils.getAccountsForBalances(wallets, accounts, (account) => { + const isChainAndCryptoMatch = accountUtils.isChainAndCryptoMatch(account, chain); + const isShardAccount = accountUtils.isShardAccount(account); + const address = toAddress(account.accountId, { prefix: chain.addressPrefix }); + + return isChainAndCryptoMatch && !isShardAccount && isStringsMatchQuery(query, [account.name, address]); + }); + }, +); + +const $proxyTypes = combine( + { + apis: networkModel.$apis, + statuses: networkModel.$connectionStatuses, + chain: $chain, + }, + ({ apis, statuses, chain }) => { + if (!chain) return []; + + return networkUtils.isConnectedStatus(statuses[chain.chainId]) + ? getProxyTypes(apis[chain.chainId]) + : [ProxyType.ANY]; + }, +); + +// Miscellaneous + +const $isChainConnected = combine( + { + chain: $chain, + statuses: networkModel.$connectionStatuses, + }, + ({ chain, statuses }) => { + if (!chain) return false; + + return networkUtils.isConnectedStatus(statuses[chain.chainId]); + }, +); + +const $api = combine( + { + apis: networkModel.$apis, + form: $proxyForm.$values, + chain: $chain, + }, + ({ apis, chain }) => { + if (!chain?.chainId) return undefined; + + return apis[chain.chainId]; + }, + { skipVoid: false }, +); + +const $fakeTx = combine( + { + chain: $chain, + isConnected: $isChainConnected, + }, + ({ isConnected, chain }): Transaction | undefined => { + if (!chain || !isConnected) return undefined; + + return { + chainId: chain.chainId, + address: toAddress(TEST_ACCOUNTS[0], { prefix: chain.addressPrefix }), + type: TransactionType.REMOVE_PURE_PROXY, + args: { + spawner: toAddress(TEST_ACCOUNTS[0], { prefix: chain.addressPrefix }), + proxyType: ProxyType.ANY, + index: 0, + blockNumber: 1, + extrinsicIndex: 1, + }, + }; + }, + { skipVoid: false }, +); + +const $canSubmit = combine( + { + isFormValid: $proxyForm.$isValid, + isFeeLoading: $isFeeLoading, + }, + ({ isFormValid, isFeeLoading }) => { + return isFormValid && !isFeeLoading; + }, +); + +type ProxyParams = { + api: ApiPromise; + address: Address; +}; +const getAccountProxiesFx = createEffect(({ api, address }: ProxyParams): Promise => { + return proxyService.getProxiesForAccount(api, address); +}); + +// Fields connections + +sample({ + clock: formInitiated, + fn: (formStore) => { + return { + ...formStore, + signatory: undefined, + description: '', + }; + }, + target: formSubmitted, +}); + +sample({ + clock: formInitiated, + target: [$proxyForm.reset, $proxyQuery.reinit], +}); + +sample({ + clock: $account, + source: { + wallet: walletSelectModel.$walletForDetails, + wallets: walletModel.$wallets, + }, + filter: (_, account) => Boolean(account), + fn: ({ wallet, wallets }, account): Record => { + if (!wallet) return { isMultisig: false, isProxy: false }; + if (walletUtils.isMultisig(wallet)) return { isMultisig: true, isProxy: false }; + if (!walletUtils.isProxied(wallet)) return { isMultisig: false, isProxy: false }; + + const accountWallet = walletUtils.getWalletById(wallets, account!.walletId); + + return { + isMultisig: walletUtils.isMultisig(accountWallet), + isProxy: true, + }; + }, + target: spread({ + isMultisig: $isMultisig, + isProxy: $isProxy, + }), +}); + +sample({ + clock: $chain, + source: { + signatories: $signatories, + isMultisig: $isMultisig, + }, + filter: ({ isMultisig, signatories }) => { + return isMultisig && signatories.length > 0; + }, + fn: ({ signatories }) => signatories[0].signer, + target: $proxyForm.fields.signatory.onChange, +}); + +sample({ + clock: $chain, + source: { + apis: networkModel.$apis, + account: $account, + isChainConnected: $isChainConnected, + }, + filter: ({ isChainConnected, account }, chain) => isChainConnected && Boolean(account) && Boolean(chain), + fn: ({ apis, account }, chain) => ({ + api: apis[chain!.chainId], + address: toAddress(account!.accountId, { prefix: chain!.addressPrefix }), + }), + target: getAccountProxiesFx, +}); + +sample({ + clock: getAccountProxiesFx.done, + source: { + chain: $chain, + apis: networkModel.$apis, + }, + filter: ({ chain, apis }, { params }) => { + return !!chain && apis[chain.chainId].genesisHash === params.api.genesisHash; + }, + fn: (_, { result }) => result.accounts, + target: $activeProxies, +}); + +// Submit + +sample({ + clock: $proxyForm.formValidated, + source: { + chain: $chain, + account: $account, + }, + filter: ({ chain, account }) => Boolean(chain) && Boolean(account), + fn: ({ chain, account }, formData) => { + const signatory = Object.keys(formData.signatory).length > 0 ? formData.signatory : undefined; + const proxied = toAddress(account!.accountId, { + prefix: chain!.addressPrefix, + }); + const multisigDescription = `Remove pure proxy for ${proxied}`; // TODO: update after i18n effector integration + const description = signatory ? formData.description || multisigDescription : ''; + + return { + ...formData, + signatory, + description, + }; + }, + target: formSubmitted, +}); + +export const formModel = { + $proxyForm, + $proxyChains, + $proxiedAccounts, + $signatories, + $proxyAccounts, + $proxyTypes, + $proxyQuery, + + $activeProxies, + $multisigDeposit, + $fee, + + $api, + $fakeTx, + $isMultisig, + $isChainConnected, + $canSubmit, + + events: { + formInitiated, + proxyQueryChanged, + proxyDepositChanged, + multisigDepositChanged, + feeChanged, + isFeeLoadingChanged, + isProxyDepositLoadingChanged, + }, + + output: { + formSubmitted, + }, +}; diff --git a/src/renderer/widgets/RemoveProxyModal/model/remove-proxy-model.ts b/src/renderer/widgets/RemoveProxyModal/model/remove-proxy-model.ts new file mode 100644 index 0000000000..ab5302d998 --- /dev/null +++ b/src/renderer/widgets/RemoveProxyModal/model/remove-proxy-model.ts @@ -0,0 +1,327 @@ +import { combine, createEvent, createStore, sample } from 'effector'; +import { spread, delay } from 'patronum'; + +import { Transaction, TransactionType, transactionService } from '@entities/transaction'; +import { dictionary, toAddress } from '@shared/lib/utils'; +import { walletSelectModel } from '@features/wallets'; +import { accountUtils, walletModel, walletUtils } from '@entities/wallet'; +import { networkModel } from '@entities/network'; +import { balanceSubModel } from '@features/balances'; +import { Step, RemoveProxyStore } from '../lib/types'; +import { formModel } from './form-model'; +import { confirmModel } from './confirm-model'; +import { walletProviderModel } from '../../WalletDetails/model/wallet-provider-model'; +import { Chain, ProxiedAccount, ProxyType, ProxyVariant } from '@shared/core'; +import { signModel } from '@features/operations/OperationSign/model/sign-model'; +import { submitModel } from '@features/operations/OperationSubmit'; +import { proxyModel } from '@/src/renderer/entities/proxy'; +import { toAccountId } from '@/src/renderer/features/proxies/lib/worker-utils'; + +const stepChanged = createEvent(); + +const flowStarted = createEvent(); +const flowFinished = createEvent(); + +const $step = createStore(Step.NONE); + +const $removeProxyStore = createStore(null); +const $transaction = createStore(null); +const $multisigTx = createStore(null); + +const $chain = $removeProxyStore.map((store) => store?.chain, { skipVoid: false }); +const $account = $removeProxyStore.map((store) => store?.account, { skipVoid: false }); + +const $txWrappers = combine( + { + wallet: walletSelectModel.$walletForDetails, + wallets: walletModel.$wallets, + accounts: walletModel.$accounts, + account: $account, + chain: $chain, + }, + ({ wallet, account, accounts, wallets, chain }) => { + if (!wallet || !chain || !account) return []; + + const walletFiltered = wallets.filter((wallet) => { + return !walletUtils.isProxied(wallet) && !walletUtils.isWatchOnly(wallet); + }); + const walletsMap = dictionary(walletFiltered, 'id'); + const chainFilteredAccounts = accounts.filter((account) => { + if (accountUtils.isBaseAccount(account) && walletUtils.isPolkadotVault(walletsMap[account.walletId])) { + return false; + } + + return accountUtils.isChainAndCryptoMatch(account, chain); + }); + + return transactionService.getTxWrappers({ + wallet, + wallets: walletFiltered, + account, + accounts: chainFilteredAccounts, + signatories: [], + }); + }, +); + +const $shouldRemovePureProxy = combine( + { + proxies: walletProviderModel.$chainsProxies, + account: $account, + chain: $chain, + }, + ({ proxies, account, chain }) => { + if (!chain || !account) return true; + + const chainProxies = proxies[chain.chainId] || []; + const anyProxies = chainProxies.filter((proxy) => proxy.proxyType === ProxyType.ANY); + const isPureProxy = (account as ProxiedAccount).proxyVariant === ProxyVariant.PURE; + + return isPureProxy && anyProxies.length === 1; + }, +); + +sample({ clock: stepChanged, target: $step }); + +sample({ + clock: flowStarted, + source: { + proxyAccount: walletProviderModel.$proxyForRemoval, + selectedWallet: walletSelectModel.$walletForDetails, + wallets: walletModel.$wallets, + chains: networkModel.$chains, + allAccounts: walletModel.$accounts, + }, + filter: ({ proxyAccount }) => Boolean(proxyAccount), + fn: ({ chains, selectedWallet, proxyAccount, allAccounts }) => { + const chain = chains[proxyAccount!.chainId]; + const proxiedSelected = walletUtils.isProxied(selectedWallet); + + const account = allAccounts.find((a) => + a.accountId === proxyAccount!.proxiedAccountId && proxiedSelected + ? accountUtils.isProxiedAccount(a) + : a.walletId === selectedWallet!.id, + ); + + const store = { + chain: chain!, + account: account!, + delegate: toAddress(proxyAccount!.accountId!, { prefix: chain!.addressPrefix }), + proxyType: proxyAccount!.proxyType, + delay: proxyAccount!.delay, + description: '', + signatory: account, + }; + + return store; + }, + target: $removeProxyStore, +}); + +sample({ + clock: flowStarted, + fn: () => Step.INIT, + target: stepChanged, +}); + +sample({ + clock: flowStarted, + source: { + activeWallet: walletModel.$activeWallet, + walletDetails: walletSelectModel.$walletForDetails, + }, + filter: ({ activeWallet, walletDetails }) => { + if (!activeWallet || !walletDetails) return false; + + return activeWallet !== walletDetails; + }, + fn: ({ walletDetails }) => walletDetails!, + target: balanceSubModel.events.walletToSubSet, +}); + +sample({ + clock: flowStarted, + source: { + account: $account, + chain: $chain, + }, + filter: ({ account, chain }) => { + return Boolean(account) && Boolean(chain); + }, + fn: ({ account, chain }) => ({ + account: account as ProxiedAccount, + chain, + }), + target: formModel.events.formInitiated, +}); + +sample({ + clock: formModel.output.formSubmitted, + source: { + txWrappers: $txWrappers, + apis: networkModel.$apis, + data: $removeProxyStore, + }, + fn: ({ txWrappers, apis, data }, formData) => { + const account = data!.account as ProxiedAccount; + const chain = data!.chain; + + const transaction: Transaction = { + chainId: chain.chainId, + address: toAddress(account.accountId, { prefix: chain.addressPrefix }), + type: TransactionType.REMOVE_PROXY, + args: { + delegate: data!.delegate, + proxyType: data!.proxyType, + delay: 0, + }, + }; + + return transactionService.getWrappedTransaction({ + api: apis[chain.chainId], + addressPrefix: chain.addressPrefix, + transaction, + txWrappers, + }); + }, + target: spread({ + wrappedTx: $transaction, + multisigTx: $multisigTx, + }), +}); + +sample({ + clock: formModel.output.formSubmitted, + source: { transaction: $transaction, chain: $chain, account: $account, store: $removeProxyStore }, + filter: ({ transaction, chain, account, store }) => { + return Boolean(transaction) && Boolean(chain) && Boolean(account) && Boolean(store); + }, + fn: ({ transaction, chain, account, store }, formData) => ({ + event: { + ...formData, + chain: chain as Chain, + account, + transaction: transaction as Transaction, + delegate: store!.delegate, + proxyType: store!.proxyType, + }, + step: Step.CONFIRM, + }), + target: spread({ + event: confirmModel.events.formInitiated, + step: stepChanged, + }), +}); + +sample({ + clock: confirmModel.output.formSubmitted, + source: { + removeProxyStore: $removeProxyStore, + transaction: $transaction, + }, + filter: ({ removeProxyStore, transaction }) => Boolean(removeProxyStore) && Boolean(transaction), + fn: ({ removeProxyStore, transaction }) => ({ + event: { + chain: removeProxyStore!.chain, + accounts: [removeProxyStore!.account], + signatory: removeProxyStore!.signatory, + transactions: [transaction!], + }, + step: Step.SIGN, + }), + target: spread({ + event: signModel.events.formInitiated, + step: stepChanged, + }), +}); + +sample({ + clock: signModel.output.formSubmitted, + source: { + removeProxyStore: $removeProxyStore, + transaction: $transaction, + multisigTx: $multisigTx, + txWrappers: $txWrappers, + }, + filter: (proxyData) => { + const isMultisigRequired = !transactionService.hasMultisig(proxyData.txWrappers) || Boolean(proxyData.multisigTx); + + return Boolean(proxyData.removeProxyStore) && Boolean(proxyData.transaction) && isMultisigRequired; + }, + fn: (proxyData, signParams) => ({ + event: { + ...signParams, + chain: proxyData.removeProxyStore!.chain, + account: proxyData.removeProxyStore!.account, + signatory: proxyData.removeProxyStore!.signatory, + description: proxyData.removeProxyStore!.description, + transactions: [proxyData.transaction!], + multisigTxs: [proxyData.multisigTx!], + }, + step: Step.SUBMIT, + }), + target: spread({ + event: submitModel.events.formInitiated, + step: stepChanged, + }), +}); + +sample({ + clock: submitModel.output.formSubmitted, + source: { + store: $removeProxyStore, + chainProxies: walletProviderModel.$chainsProxies, + }, + fn: ({ store, chainProxies }) => { + const proxy = chainProxies[store!.chain.chainId].find( + (proxy) => + proxy.accountId === toAccountId(store!.delegate) && + proxy.proxyType === store!.proxyType && + proxy.proxiedAccountId === store!.account.accountId, + ); + + return proxy ? [proxy] : []; + }, + target: proxyModel.events.proxiesRemoved, +}); + +sample({ + clock: delay(submitModel.output.formSubmitted, 2000), + target: flowFinished, +}); + +sample({ + clock: flowFinished, + source: { + activeWallet: walletModel.$activeWallet, + walletDetails: walletSelectModel.$walletForDetails, + }, + filter: ({ activeWallet, walletDetails }) => { + if (!activeWallet || !walletDetails) return false; + + return activeWallet !== walletDetails; + }, + fn: ({ walletDetails }) => walletDetails!, + target: balanceSubModel.events.walletToUnsubSet, +}); + +sample({ + clock: flowFinished, + fn: () => Step.NONE, + target: stepChanged, +}); + +export const removeProxyModel = { + $step, + $chain, + $account, + $shouldRemovePureProxy, + + events: { + flowStarted, + stepChanged, + }, + output: { + flowFinished, + }, +}; diff --git a/src/renderer/widgets/RemoveProxyModal/ui/Confirm.tsx b/src/renderer/widgets/RemoveProxyModal/ui/Confirm.tsx new file mode 100644 index 0000000000..0040ed0129 --- /dev/null +++ b/src/renderer/widgets/RemoveProxyModal/ui/Confirm.tsx @@ -0,0 +1,157 @@ +import { useState } from 'react'; +import { useUnit } from 'effector-react'; + +import { FeeWithLabel, MultisigDepositWithLabel } from '@entities/transaction'; +import { Button, DetailRow, FootnoteText, Icon } from '@shared/ui'; +import { useI18n } from '@app/providers'; +import { SignButton } from '@entities/operation/ui/SignButton'; +import { AddressWithExplorers, WalletIcon, accountUtils, ExplorersPopover, WalletCardSm } from '@entities/wallet'; +import { proxyUtils } from '@entities/proxy'; +import { confirmModel } from '../model/confirm-model'; +import { toAddress } from '@shared/lib/utils'; + +type Props = { + onGoBack: () => void; +}; + +export const Confirmation = ({ onGoBack }: Props) => { + const { t } = useI18n(); + + const confirmStore = useUnit(confirmModel.$confirmStore); + const initiatorWallet = useUnit(confirmModel.$initiatorWallet); + const signerWallet = useUnit(confirmModel.$signerWallet); + const proxiedWallet = useUnit(confirmModel.$proxiedWallet); + const api = useUnit(confirmModel.$api); + + const [isFeeLoading, setIsFeeLoading] = useState(true); + + if (!confirmStore || !api || !initiatorWallet) return null; + + return ( +
+
+ + + + {confirmStore.description} + +
+ +
+ {proxiedWallet && confirmStore.proxiedAccount && ( + <> + + + {proxiedWallet.name} + + + + + + +
+ + + + {initiatorWallet.name} + + + + + + + )} + + {!proxiedWallet && ( + <> + + + {initiatorWallet.name} + + + + + + + )} + + {signerWallet && confirmStore.signatory && ( + + } + address={confirmStore.signatory.accountId} + explorers={confirmStore.chain?.explorers} + addressPrefix={confirmStore.chain?.addressPrefix} + /> + + )} + +
+ + + {t(proxyUtils.getProxyTypeName(confirmStore.proxyType))} + + + + + + +
+ + {accountUtils.isMultisigAccount(confirmStore.account!) && ( + + )} + + +
+ +
+ + + +
+
+ ); +}; diff --git a/src/renderer/widgets/RemoveProxyModal/ui/RemoveProxy.tsx b/src/renderer/widgets/RemoveProxyModal/ui/RemoveProxy.tsx new file mode 100644 index 0000000000..06174c6e19 --- /dev/null +++ b/src/renderer/widgets/RemoveProxyModal/ui/RemoveProxy.tsx @@ -0,0 +1,45 @@ +import { useUnit } from 'effector-react'; + +import { BaseModal } from '@shared/ui'; +import { useModalClose } from '@shared/lib/hooks'; +import { OperationTitle } from '@entities/chain'; +import { useI18n } from '@app/providers'; +import type { Chain } from '@shared/core'; +import { Step } from '../lib/types'; +import { RemoveProxyForm } from './RemoveProxyForm'; +import { Confirmation } from './Confirm'; +import { removeProxyUtils } from '../lib/remove-proxy-utils'; +import { removeProxyModel } from '../model/remove-proxy-model'; +import { OperationSign, OperationSubmit } from '@features/operations'; + +export const RemoveProxy = () => { + const { t } = useI18n(); + + const step = useUnit(removeProxyModel.$step); + const chain = useUnit(removeProxyModel.$chain); + + const [isModalOpen, closeModal] = useModalClose( + !removeProxyUtils.isNoneStep(step), + removeProxyModel.output.flowFinished, + ); + + const getModalTitle = (step: Step, chain?: Chain) => { + if (removeProxyUtils.isInitStep(step) || !chain) return t('operations.modalTitles.removeProxy'); + + return ; + }; + + if (removeProxyUtils.isSubmitStep(step)) return ; + + return ( + + {removeProxyUtils.isInitStep(step) && } + {removeProxyUtils.isConfirmStep(step) && ( + removeProxyModel.events.stepChanged(Step.INIT)} /> + )} + {removeProxyUtils.isSignStep(step) && ( + removeProxyModel.events.stepChanged(Step.CONFIRM)} /> + )} + + ); +}; diff --git a/src/renderer/widgets/RemoveProxyModal/ui/RemoveProxyForm.tsx b/src/renderer/widgets/RemoveProxyModal/ui/RemoveProxyForm.tsx new file mode 100644 index 0000000000..de105eb520 --- /dev/null +++ b/src/renderer/widgets/RemoveProxyModal/ui/RemoveProxyForm.tsx @@ -0,0 +1,188 @@ +import { useForm } from 'effector-forms'; +import { FormEvent } from 'react'; +import { useUnit } from 'effector-react'; + +import { Button, Select, Input, InputHint, Alert } from '@shared/ui'; +import { useI18n } from '@app/providers'; +import { AccountAddress, accountUtils } from '@entities/wallet'; +import { toAddress } from '@shared/lib/utils'; +import { MultisigDepositWithLabel, FeeWithLabel } from '@entities/transaction'; +import { formModel } from '../model/form-model'; +import { AssetBalance } from '@entities/asset'; +import { MultisigAccount } from '@shared/core'; +import { removeProxyModel } from '../model/remove-proxy-model'; + +type Props = { + onGoBack: () => void; +}; +export const RemoveProxyForm = ({ onGoBack }: Props) => { + const { submit } = useForm(formModel.$proxyForm); + + const submitProxy = (event: FormEvent) => { + event.preventDefault(); + submit(); + }; + + return ( +
+
+ + + +
+ + +
+ +
+ ); +}; + +const SignatorySelector = () => { + const { t } = useI18n(); + + const { + fields: { signatory }, + } = useForm(formModel.$proxyForm); + + const signatories = useUnit(formModel.$signatories); + const isMultisig = useUnit(formModel.$isMultisig); + const chain = useUnit(removeProxyModel.$chain); + + if (!isMultisig || !chain) return null; + + const options = signatories.map(({ signer, balance }) => { + const isShard = accountUtils.isShardAccount(signer); + const address = toAddress(signer.accountId, { prefix: chain.addressPrefix }); + + return { + id: signer.id.toString(), + value: signer, + element: ( +
+ + +
+ ), + }; + }); + + return ( +
+ + + {description.errorText({ + maxLength: t('proxy.addProxy.maxLengthDescriptionError', { maxLength: 120 }), + })} + +
+ ); +}; + +const FeeSection = () => { + const api = useUnit(formModel.$api); + const fakeTx = useUnit(formModel.$fakeTx); + const isMultisig = useUnit(formModel.$isMultisig); + const chain = useUnit(removeProxyModel.$chain); + const account = useUnit(removeProxyModel.$account); + + if (!chain) return null; + + return ( +
+ {isMultisig && ( + + )} + + +
+ ); +}; + +const FeeError = () => { + const { t } = useI18n(); + + const { + fields: { signatory }, + } = useForm(formModel.$proxyForm); + + const isMultisig = useUnit(formModel.$isMultisig); + + return ( + + + {isMultisig ? t('proxy.addProxy.balanceAlertMultisig') : t('proxy.addProxy.balanceAlertRegular')} + + + ); +}; + +const ActionSection = ({ onGoBack }: Props) => { + const { t } = useI18n(); + + const canSubmit = useUnit(formModel.$canSubmit); + + return ( +
+ + +
+ ); +}; diff --git a/src/renderer/widgets/RemovePureProxyModal/model/remove-pure-proxy-model.ts b/src/renderer/widgets/RemovePureProxyModal/model/remove-pure-proxy-model.ts index 67b2f3ce9a..6b221eb292 100644 --- a/src/renderer/widgets/RemovePureProxyModal/model/remove-pure-proxy-model.ts +++ b/src/renderer/widgets/RemovePureProxyModal/model/remove-pure-proxy-model.ts @@ -33,7 +33,7 @@ const $account = $removeProxyStore.map((store) => store?.account, { skipVoid: fa const $txWrappers = combine( { - wallet: walletModel.$activeWallet, + wallet: walletSelectModel.$walletForDetails, wallets: walletModel.$wallets, accounts: walletModel.$accounts, account: $account, diff --git a/src/renderer/widgets/WalletDetails/model/wallet-provider-model.ts b/src/renderer/widgets/WalletDetails/model/wallet-provider-model.ts index bbfcc71c94..fe16c85139 100644 --- a/src/renderer/widgets/WalletDetails/model/wallet-provider-model.ts +++ b/src/renderer/widgets/WalletDetails/model/wallet-provider-model.ts @@ -9,7 +9,6 @@ import { walletDetailsUtils } from '../lib/utils'; import type { MultishardMap, VaultMap } from '../lib/types'; import { proxyModel, proxyUtils } from '@entities/proxy'; import { networkModel } from '@entities/network'; -import { removeProxyModel } from '@widgets/RemoveProxy'; import type { Account, Signatory, @@ -25,7 +24,7 @@ import type { const removeProxy = createEvent(); -const $proxyForRemoval = createStore(null).reset(removeProxyModel.events.proxyRemoved); +const $proxyForRemoval = createStore(null); const $accounts = combine( { diff --git a/src/renderer/widgets/WalletDetails/ui/components/ProxiesList.tsx b/src/renderer/widgets/WalletDetails/ui/components/ProxiesList.tsx index 315521f867..4f3171d8bb 100644 --- a/src/renderer/widgets/WalletDetails/ui/components/ProxiesList.tsx +++ b/src/renderer/widgets/WalletDetails/ui/components/ProxiesList.tsx @@ -4,15 +4,15 @@ import { cnTw } from '@shared/lib/utils'; import { ChainTitle } from '@entities/chain'; import { useI18n } from '@app/providers'; import { Accordion, ConfirmModal, FootnoteText, HelpText, SmallTitleText } from '@shared/ui'; -import { type ProxyAccount } from '@shared/core'; import { networkModel } from '@entities/network'; import { AssetBalance } from '@entities/asset'; import { walletProviderModel } from '../../model/wallet-provider-model'; import { ProxyAccountWithActions } from './ProxyAccountWithActions'; import { useToggle } from '@shared/lib/hooks'; -import { RemoveProxy } from '@widgets/RemoveProxy'; -import { RemovePureProxy, removePureProxyModel } from '../../../RemovePureProxyModal'; +import { RemovePureProxy, removePureProxyModel } from '@widgets/RemovePureProxyModal'; +import { RemoveProxy, removeProxyModel } from '@widgets/RemoveProxyModal'; import { accountUtils } from '@entities/wallet'; +import { ProxyAccount } from '@/src/renderer/shared/core'; type Props = { canCreateProxy?: boolean; @@ -26,10 +26,8 @@ export const ProxiesList = ({ className, canCreateProxy = true }: Props) => { const accounts = useUnit(walletProviderModel.$accounts); const chainsProxies = useUnit(walletProviderModel.$chainsProxies); const walletProxyGroups = useUnit(walletProviderModel.$walletProxyGroups); - const proxyForRemoval = useUnit(walletProviderModel.$proxyForRemoval); const [isRemoveConfirmOpen, toggleIsRemoveConfirmOpen] = useToggle(); - const [isRemoveProxyOpen, toggleIsRemoveProxyOpen] = useToggle(); const handleDeleteProxy = (proxyAccount: ProxyAccount) => { if (accountUtils.isProxiedAccount(accounts[0])) { @@ -97,7 +95,7 @@ export const ProxiesList = ({ className, canCreateProxy = true }: Props) => { onClose={toggleIsRemoveConfirmOpen} onConfirm={() => { toggleIsRemoveConfirmOpen(); - toggleIsRemoveProxyOpen(); + removeProxyModel.events.flowStarted(); }} > @@ -108,10 +106,7 @@ export const ProxiesList = ({ className, canCreateProxy = true }: Props) => { - {proxyForRemoval && ( - - )} - + ); diff --git a/src/renderer/widgets/WalletDetails/ui/wallets/ProxiedWalletDetails.tsx b/src/renderer/widgets/WalletDetails/ui/wallets/ProxiedWalletDetails.tsx index 9be3a0c745..7d81bcd990 100644 --- a/src/renderer/widgets/WalletDetails/ui/wallets/ProxiedWalletDetails.tsx +++ b/src/renderer/widgets/WalletDetails/ui/wallets/ProxiedWalletDetails.tsx @@ -7,12 +7,14 @@ import { networkModel } from '@entities/network'; import { useModalClose, useToggle } from '@shared/lib/hooks'; import { IconNames } from '@shared/ui/Icon/data'; import { BaseModal, DropdownIconButton, FootnoteText, Icon, Tabs } from '@shared/ui'; -import { AccountsList, WalletCardLg, WalletIcon } from '@entities/wallet'; +import { AccountsList, WalletCardLg, WalletIcon, permissionUtils } from '@entities/wallet'; import { RenameWalletModal } from '@features/wallets/RenameWallet'; import { TabItem } from '@shared/ui/types'; import { ProxiesList } from '../components/ProxiesList'; import { NoProxiesAction } from '../components/NoProxiesAction'; import { walletProviderModel } from '../../model/wallet-provider-model'; +import { AddProxy, addProxyModel } from '@widgets/AddProxyModal'; +import { AddPureProxied } from '@widgets/AddPureProxiedModal'; const ProxyTypeOperation: Record = { [ProxyType.ANY]: 'proxy.operations.any', @@ -50,6 +52,17 @@ export const ProxiedWalletDetails = ({ wallet, proxyWallet, proxiedAccount, onCl }, ]; + if ( + permissionUtils.canCreateAnyProxy(wallet, [proxiedAccount]) || + permissionUtils.canCreateNonAnyProxy(wallet, [proxiedAccount]) + ) { + Options.push({ + icon: 'addCircle' as IconNames, + title: t('walletDetails.common.addProxyAction'), + onClick: addProxyModel.events.flowStarted, + }); + } + const ActionButton = ( @@ -113,6 +126,9 @@ export const ProxiedWalletDetails = ({ wallet, proxyWallet, proxiedAccount, onCl + + + ); };