diff --git a/src/renderer/app/modelInit.ts b/src/renderer/app/modelInit.ts index 924273f6ef..0b5b67b806 100644 --- a/src/renderer/app/modelInit.ts +++ b/src/renderer/app/modelInit.ts @@ -17,12 +17,17 @@ import { contactsNavigationFeature } from '@/features/contacts-navigation'; import { fellowshipNavigationFeature } from '@/features/fellowship-navigation'; import { flexibleMultisigNavigationFeature } from '@/features/flexible-multisig-navigation'; import { governanceNavigationFeature } from '@/features/governance-navigation'; +import { governanceOperationDetailFeature } from '@/features/governance-operation-details'; import { importDBFeature } from '@/features/import-db'; +import { multisigOperationDetailsFeature } from '@/features/multisig-operation-details'; import { notificationsNavigationFeature } from '@/features/notifications-navigation'; import { operationsNavigationFeature } from '@/features/operations-navigation'; import { proxiesModel } from '@/features/proxies'; +import { proxyOperationDetailFeature } from '@/features/proxy-operation-details'; import { settingsNavigationFeature } from '@/features/settings-navigation'; import { stakingNavigationFeature } from '@/features/staking-navigation'; +import { stakingOperationDetailFeature } from '@/features/staking-operation-details'; +import { transferOperationDetailFeature } from '@/features/transfer-operation-details'; import { walletDetailsFeature } from '@/features/wallet-details'; import { walletMultisigFeature } from '@/features/wallet-multisig'; import { walletPairingFeature } from '@/features/wallet-pairing'; @@ -79,6 +84,12 @@ export const initModel = () => { walletWalletConnectFeature, walletWatchOnlyFeature, + governanceOperationDetailFeature, + multisigOperationDetailsFeature, + proxyOperationDetailFeature, + stakingOperationDetailFeature, + transferOperationDetailFeature, + importDBFeature, ]); }; diff --git a/src/renderer/entities/multisig/lib/multisigTx/common/utils.ts b/src/renderer/entities/multisig/lib/multisigTx/common/utils.ts index 46fa9fa9b4..2230a541b3 100644 --- a/src/renderer/entities/multisig/lib/multisigTx/common/utils.ts +++ b/src/renderer/entities/multisig/lib/multisigTx/common/utils.ts @@ -58,7 +58,7 @@ export const createNewEventsPayload = ( approvals: Vec, ): MultisigEvent[] => { return approvals.reduce((acc, a) => { - const hasApprovalEvent = events.find((e) => e.status === 'SIGNED' && e.accountId === a.toHex()); + const hasApprovalEvent = events.some((e) => e.status === 'SIGNED' && e.accountId === a.toHex()); if (!hasApprovalEvent) { acc.push({ diff --git a/src/renderer/entities/multisig/lib/multisigTx/multisigTxService.ts b/src/renderer/entities/multisig/lib/multisigTx/multisigTxService.ts index 39e8306e8e..71aaee1222 100644 --- a/src/renderer/entities/multisig/lib/multisigTx/multisigTxService.ts +++ b/src/renderer/entities/multisig/lib/multisigTx/multisigTxService.ts @@ -84,6 +84,7 @@ export const useMultisigTx = ({ addTask }: Props): IMultisigTxService => { }); const newEvents = createNewEventsPayload(oldEvents, newestOldTx, pendingTx.params.approvals); + for (const e of newEvents) { addEventWithQueue(e); } diff --git a/src/renderer/entities/operations/index.ts b/src/renderer/entities/operations/index.ts index 7224f2f978..48635e5b92 100644 --- a/src/renderer/entities/operations/index.ts +++ b/src/renderer/entities/operations/index.ts @@ -1,3 +1,4 @@ export * from './ui'; export { operationsModel } from './model/operations-model'; export { operationsUtils } from './lib/operationsUtils'; +export * as operationDetailsUtils from './lib/operationDetailsUtils'; diff --git a/src/renderer/pages/Operations/common/utils.ts b/src/renderer/entities/operations/lib/operationDetailsUtils.ts similarity index 92% rename from src/renderer/pages/Operations/common/utils.ts rename to src/renderer/entities/operations/lib/operationDetailsUtils.ts index c26f6ef8a6..90dbbbc022 100644 --- a/src/renderer/pages/Operations/common/utils.ts +++ b/src/renderer/entities/operations/lib/operationDetailsUtils.ts @@ -18,7 +18,7 @@ import { TransactionType, type Wallet, } from '@/shared/core'; -import { toAddress } from '@/shared/lib/utils'; +import { dictionary, toAddress } from '@/shared/lib/utils'; import { convictionVotingPallet } from '@/shared/pallet/convictionVoting'; import { type AccountId } from '@/shared/polkadotjs-schemas'; import { type TransactionVote, votingService } from '@/entities/governance'; @@ -48,7 +48,7 @@ export const getSignatoryName = ( addressPrefix?: number, ): string => { const finderFn = (collection: T[]): T | undefined => { - return collection.find((c) => c?.accountId === signatoryId); + return collection.find((c) => c.accountId === signatoryId); }; // signatory data source priority: transaction -> contacts -> wallets -> address @@ -72,33 +72,35 @@ export const getSignatoryAccounts = ( signatories: Signatory[], chainId: ChainId, ): Account[] => { - const walletsMap = new Map(wallets.map((wallet) => [wallet.id, wallet])); + const walletsMap = dictionary(wallets, 'id'); - return signatories.reduce((acc: Account[], signatory) => { + const result = []; + + for (const signatory of signatories) { const filteredAccounts = accounts.filter( (a) => a.accountId === signatory.accountId && !events.some((e) => e.accountId === a.accountId), ); const signatoryAccount = filteredAccounts.find((a) => { const isChainMatch = accountUtils.isChainIdMatch(a, chainId); - const wallet = walletsMap.get(a.walletId); + const wallet = walletsMap[a.walletId]; return isChainMatch && walletUtils.isValidSignatory(wallet); }); if (signatoryAccount) { - acc.push(signatoryAccount); + result.push(signatoryAccount); } else { const legacySignatoryAccount = filteredAccounts.find( - (a) => accountUtils.isVaultChainAccount(a) && a.chainId === chainId, + (a) => accountUtils.isChainDependant(a) && a.chainId === chainId, ); if (legacySignatoryAccount) { - acc.push(legacySignatoryAccount); + result.push(legacySignatoryAccount); } } + } - return acc; - }, []); + return result; }; export const getDestination = ( @@ -117,6 +119,16 @@ export const getDestination = ( return toAddress(tx.transaction.args.dest, { prefix: chain.addressPrefix }); }; +export const getDestinationAccountId = (tx: MultisigTransaction): AccountId | undefined => { + if (!tx.transaction) return undefined; + + if (isProxyTransaction(tx.transaction)) { + return tx.transaction.args.transaction.args.dest; + } + + return tx.transaction.args.dest; +}; + export const getPayee = (tx: MultisigTransaction): { Account: Address } | string | undefined => { if (!tx.transaction) return undefined; diff --git a/src/renderer/entities/operations/ui/OperationTitleDate.tsx b/src/renderer/entities/operations/ui/OperationTitleDate.tsx new file mode 100644 index 0000000000..ff27a7347b --- /dev/null +++ b/src/renderer/entities/operations/ui/OperationTitleDate.tsx @@ -0,0 +1,40 @@ +import { useStoreMap } from 'effector-react'; + +import { type MultisigTransaction } from '@/shared/core'; +import { useI18n } from '@/shared/i18n'; +import { FootnoteText } from '@/shared/ui'; +import { operationsModel } from '../model/operations-model'; + +type Props = { + operation: MultisigTransaction; +}; + +export const OperationTitleDate = ({ operation }: Props) => { + const { formatDate } = useI18n(); + + const events = useStoreMap({ + store: operationsModel.$multisigEvents, + keys: [operation], + fn: (events, [operation]) => { + return events.filter( + (e) => + e.txAccountId === operation.accountId && + e.txChainId === operation.chainId && + e.txCallHash === operation.callHash && + e.txBlock === operation.blockCreated && + e.txIndex === operation.indexCreated, + ); + }, + }); + const approvals = events.filter((e) => e.status === 'SIGNED'); + const initEvent = approvals.find((e) => e.accountId === operation.depositor); + const date = new Date(operation.dateCreated || initEvent?.dateCreated || Date.now()); + + return ( +
+ + {formatDate(date, 'p')} + +
+ ); +}; diff --git a/src/renderer/entities/operations/ui/OperationTitleStatus.tsx b/src/renderer/entities/operations/ui/OperationTitleStatus.tsx new file mode 100644 index 0000000000..a0e16f71fb --- /dev/null +++ b/src/renderer/entities/operations/ui/OperationTitleStatus.tsx @@ -0,0 +1,38 @@ +import { useStoreMap, useUnit } from 'effector-react'; + +import { type MultisigTransaction } from '@/shared/core'; +import { accountUtils, walletModel } from '@/entities/wallet'; +import { operationsModel } from '../model/operations-model'; + +import { Status } from './Status'; + +type Props = { + operation: MultisigTransaction; +}; + +export const OperationTitleStatus = ({ operation }: Props) => { + const events = useStoreMap({ + store: operationsModel.$multisigEvents, + keys: [operation], + fn: (events, [operation]) => { + return events.filter( + (e) => + e.txAccountId === operation.accountId && + e.txChainId === operation.chainId && + e.txCallHash === operation.callHash && + e.txBlock === operation.blockCreated && + e.txIndex === operation.indexCreated, + ); + }, + }); + + const approvals = events.filter((e) => e.status === 'SIGNED'); + const activeWallet = useUnit(walletModel.$activeWallet); + const account = activeWallet?.accounts.find(accountUtils.isMultisigAccount); + + return ( +
+ +
+ ); +}; diff --git a/src/renderer/pages/Operations/components/Status.tsx b/src/renderer/entities/operations/ui/Status.tsx similarity index 100% rename from src/renderer/pages/Operations/components/Status.tsx rename to src/renderer/entities/operations/ui/Status.tsx diff --git a/src/renderer/entities/operations/ui/index.ts b/src/renderer/entities/operations/ui/index.ts index d2fde64ba8..de9d64a171 100644 --- a/src/renderer/entities/operations/ui/index.ts +++ b/src/renderer/entities/operations/ui/index.ts @@ -1,2 +1,5 @@ export { SignatorySelector } from './SignatorySelector'; export { SignButton } from './SignButton'; +export { Status } from './Status'; +export { OperationTitleStatus } from './OperationTitleStatus'; +export { OperationTitleDate } from './OperationTitleDate'; diff --git a/src/renderer/features/flexible-multisig-create/components/SelectThreshold/SelectSignatoriesThreshold.tsx b/src/renderer/features/flexible-multisig-create/components/SelectThreshold/SelectSignatoriesThreshold.tsx index 2871825542..8dd634a00b 100644 --- a/src/renderer/features/flexible-multisig-create/components/SelectThreshold/SelectSignatoriesThreshold.tsx +++ b/src/renderer/features/flexible-multisig-create/components/SelectThreshold/SelectSignatoriesThreshold.tsx @@ -21,7 +21,6 @@ export const SelectSignatoriesThreshold = () => { fields: { threshold }, submit, } = useForm(formModel.$createMultisigForm); - const chain = useUnit(formModel.$chain); const signatories = useUnit(signatoryModel.$signatories); const multisigAlreadyExists = useUnit(formModel.$multisigAlreadyExists); diff --git a/src/renderer/features/governance-operation-details/components/GovernanceDelegateDetails.tsx b/src/renderer/features/governance-operation-details/components/GovernanceDelegateDetails.tsx new file mode 100644 index 0000000000..aeed54a895 --- /dev/null +++ b/src/renderer/features/governance-operation-details/components/GovernanceDelegateDetails.tsx @@ -0,0 +1,134 @@ +import { useUnit } from 'effector-react'; +import { useEffect, useState } from 'react'; + +import { type Address, type MultisigTransaction, TransactionType } from '@/shared/core'; +import { useI18n } from '@/shared/i18n'; +import { toAccountId } from '@/shared/lib/utils'; +import { DetailRow, FootnoteText } from '@/shared/ui'; +import { Account } from '@/shared/ui-entities'; +import { Skeleton } from '@/shared/ui-kit'; +import { AssetBalance } from '@/entities/asset'; +import { TracksDetails } from '@/entities/governance'; +import { getTransactionFromMultisigTx } from '@/entities/multisig'; +import { networkModel } from '@/entities/network'; +import { operationDetailsUtils } from '@/entities/operations'; +import { isUndelegateTransaction } from '@/entities/transaction'; + +type Props = { operation: MultisigTransaction }; + +export const GovernanceDelegateDetails = ({ operation }: Props) => { + const { t } = useI18n(); + const transaction = getTransactionFromMultisigTx(operation); + + const chains = useUnit(networkModel.$chains); + const apis = useUnit(networkModel.$apis); + + const chain = chains[operation.chainId]; + const api = apis[operation.chainId]; + + const defaultAsset = chain?.assets[0]; + + const [isUndelegationLoading, setIsUndelegationLoading] = useState(false); + const [undelegationVotes, setUndelegationVotes] = useState(); + const [undelegationTarget, setUndelegationTarget] = useState
(); + + const result = []; + + useEffect(() => { + if (isUndelegateTransaction(transaction)) { + setIsUndelegationLoading(true); + } + + if (!api) return; + + operationDetailsUtils.getUndelegationData(api, operation).then(({ votes, target }) => { + setUndelegationVotes(votes); + setUndelegationTarget(target); + setIsUndelegationLoading(false); + }); + }, [api, operation]); + + if ( + transaction?.type && + ![TransactionType.DELEGATE, TransactionType.UNDELEGATE, TransactionType.EDIT_DELEGATION].includes(transaction.type) + ) { + return null; + } + + // TODO: Move this to domain layer + const delegationTarget = operationDetailsUtils.getDelegationTarget(operation); + const delegationTracks = operationDetailsUtils.getDelegationTracks(operation); + const delegationVotes = operationDetailsUtils.getDelegationVotes(operation); + + if (isUndelegationLoading) { + result.push( + <> + + + + + + + + , + ); + } + + if (delegationTarget) { + result.push( + + + , + ); + } + + if (!delegationTarget && undelegationTarget) { + result.push( + + + , + ); + } + + if (delegationVotes) { + result.push( + + + + + , + ); + } + + if (!delegationVotes && undelegationVotes) { + result.push( + + + + + , + ); + } + + if (delegationTracks) { + result.push( + +
+ +
+
, + ); + } + + return <>{result.map((e) => e)}; +}; diff --git a/src/renderer/features/governance-operation-details/components/GovernanceOperationTitle.tsx b/src/renderer/features/governance-operation-details/components/GovernanceOperationTitle.tsx new file mode 100644 index 0000000000..0fc27bf6d9 --- /dev/null +++ b/src/renderer/features/governance-operation-details/components/GovernanceOperationTitle.tsx @@ -0,0 +1,32 @@ +import { chainsService } from '@/shared/api/network'; +import { type MultisigTransaction } from '@/shared/core'; +import { getAssetById } from '@/shared/lib/utils'; +import { Box } from '@/shared/ui-kit'; +import { AssetBalance } from '@/entities/asset'; +import { ChainTitle } from '@/entities/chain'; +import { TransactionTitle, getTransactionAmount } from '@/entities/transaction'; + +type Props = { + operation: MultisigTransaction; +}; + +export const GovernanceOperationTitle = ({ operation }: Props) => { + const asset = + operation.transaction && + getAssetById(operation.transaction.args.asset, chainsService.getChainById(operation.chainId)?.assets); + const amount = operation.transaction && getTransactionAmount(operation.transaction); + + return ( + <> + + + {asset && amount && ( + + + + )} + + + + ); +}; diff --git a/src/renderer/features/governance-operation-details/components/GovernanceVoteDetails.tsx b/src/renderer/features/governance-operation-details/components/GovernanceVoteDetails.tsx new file mode 100644 index 0000000000..5751e32233 --- /dev/null +++ b/src/renderer/features/governance-operation-details/components/GovernanceVoteDetails.tsx @@ -0,0 +1,72 @@ +import { useUnit } from 'effector-react'; +import { Trans } from 'react-i18next'; + +import { type MultisigTransaction, TransactionType } from '@/shared/core'; +import { useI18n } from '@/shared/i18n'; +import { DetailRow, FootnoteText } from '@/shared/ui'; +import { AssetBalance } from '@/entities/asset'; +import { voteTransactionService } from '@/entities/governance'; +import { getTransactionFromMultisigTx } from '@/entities/multisig'; +import { networkModel } from '@/entities/network'; +import { operationDetailsUtils } from '@/entities/operations'; + +type Props = { operation: MultisigTransaction }; + +export const GovernanceVoteDetails = ({ operation }: Props) => { + const { t } = useI18n(); + const transaction = getTransactionFromMultisigTx(operation); + + const chains = useUnit(networkModel.$chains); + const chain = chains[operation.chainId]; + const defaultAsset = chain?.assets[0]; + + const result = []; + + if ( + transaction?.type && + ![TransactionType.UNLOCK, TransactionType.VOTE, TransactionType.REVOTE, TransactionType.REMOVE_VOTE].includes( + transaction.type, + ) + ) { + return null; + } + + const referendumId = operationDetailsUtils.getReferendumId(operation); + const vote = operationDetailsUtils.getVote(operation); + + if (referendumId) { + result.push( + + #{referendumId} + , + ); + } + + if (vote) { + result.push( + + + <> + {t(`governance.referendum.${voteTransactionService.getDecision(vote)}`)}:{' '} + + ), + }} + /> + + + , + ); + } + + return <>{result.map((e) => e)}; +}; diff --git a/src/renderer/features/governance-operation-details/governance-operation-details-model.tsx b/src/renderer/features/governance-operation-details/governance-operation-details-model.tsx new file mode 100644 index 0000000000..c9da9d2ce2 --- /dev/null +++ b/src/renderer/features/governance-operation-details/governance-operation-details-model.tsx @@ -0,0 +1,43 @@ +import { TransactionType } from '@/shared/core'; +import { createFeature } from '@/shared/feature'; +import { getTransactionFromMultisigTx } from '@/entities/multisig'; +import { multisigOperationsFeature } from '@/features/multisig-operations'; + +import { GovernanceDelegateDetails } from './components/GovernanceDelegateDetails'; +import { GovernanceOperationTitle } from './components/GovernanceOperationTitle'; +import { GovernanceVoteDetails } from './components/GovernanceVoteDetails'; + +export const governanceOperationDetailFeature = createFeature({ + name: 'governance/operation-details', +}); + +governanceOperationDetailFeature.inject(multisigOperationsFeature.slots.operationDetails, { + render: ({ operation }) => , + order: 1, +}); + +governanceOperationDetailFeature.inject(multisigOperationsFeature.slots.operationDetails, { + render: ({ operation }) => , + order: 1, +}); + +governanceOperationDetailFeature.inject(multisigOperationsFeature.slots.operationTitle, ({ operation }) => { + const transaction = getTransactionFromMultisigTx(operation); + + if ( + transaction?.type && + [ + TransactionType.UNLOCK, + TransactionType.VOTE, + TransactionType.REVOTE, + TransactionType.REMOVE_VOTE, + TransactionType.DELEGATE, + TransactionType.UNDELEGATE, + TransactionType.EDIT_DELEGATION, + ].includes(transaction.type) + ) { + return ; + } + + return null; +}); diff --git a/src/renderer/features/governance-operation-details/index.ts b/src/renderer/features/governance-operation-details/index.ts new file mode 100644 index 0000000000..e0fe287d90 --- /dev/null +++ b/src/renderer/features/governance-operation-details/index.ts @@ -0,0 +1 @@ +export { governanceOperationDetailFeature } from './governance-operation-details-model'; diff --git a/src/renderer/features/multisig-operation-details/components/OperationAdvancedDetails.tsx b/src/renderer/features/multisig-operation-details/components/OperationAdvancedDetails.tsx new file mode 100644 index 0000000000..a5b4f7a4c2 --- /dev/null +++ b/src/renderer/features/multisig-operation-details/components/OperationAdvancedDetails.tsx @@ -0,0 +1,151 @@ +import { useUnit } from 'effector-react'; + +import { type MultisigTransaction } from '@/shared/core'; +import { useI18n } from '@/shared/i18n'; +import { useToggle } from '@/shared/lib/hooks'; +import { cnTw, copyToClipboard, toAddress, truncate } from '@/shared/lib/utils'; +import { Button, DetailRow, FootnoteText, Icon } from '@/shared/ui'; +import { AccountExplorers, Address } from '@/shared/ui-entities'; +import { AssetBalance } from '@/entities/asset'; +import { networkModel } from '@/entities/network'; +import { operationDetailsUtils } from '@/entities/operations'; +import { signatoryUtils } from '@/entities/signatory'; +import { ExplorersPopover, WalletCardSm, accountUtils, walletModel } from '@/entities/wallet'; + +type Props = { + operation: MultisigTransaction; +}; + +const InteractionStyle = + 'rounded hover:bg-action-background-hover hover:text-text-primary cursor-pointer py-[3px] px-2 -mr-2'; + +export const OperationAdvancedDetails = ({ operation }: Props) => { + const { t } = useI18n(); + + const wallets = useUnit(walletModel.$wallets); + const activeWallet = useUnit(walletModel.$activeWallet); + const chains = useUnit(networkModel.$chains); + const chain = chains[operation.chainId]; + const account = activeWallet?.accounts.find(accountUtils.isMultisigAccount); + + const defaultAsset = chain?.assets[0]; + const addressPrefix = chain?.addressPrefix; + const explorers = chain?.explorers; + + const [isAdvancedShown, toggleAdvanced] = useToggle(); + + const { indexCreated, blockCreated, deposit, depositor, callHash, callData } = operation; + + const depositorSignatory = account?.signatories.find((s) => s.accountId === depositor); + const extrinsicLink = operationDetailsUtils.getMultisigExtrinsicLink(callHash, indexCreated, blockCreated, explorers); + + const valueClass = 'text-text-secondary'; + const depositorWallet = + depositorSignatory && signatoryUtils.getSignatoryWallet(wallets, depositorSignatory.accountId); + + return ( + <> + + + {isAdvancedShown && ( + <> + {callHash && ( + + + + )} + + {callData && ( + + + + )} + + {deposit && defaultAsset && depositorSignatory &&
} + + {depositorSignatory && ( + +
+ {depositorWallet ? ( + } + address={depositorSignatory.accountId} + explorers={explorers} + addressPrefix={addressPrefix} + /> + ) : ( +
+ +
+ + +
+ )} +
+
+ )} + + {deposit && defaultAsset && ( + + + + )} + + {deposit && defaultAsset && depositorSignatory &&
} + + {indexCreated && blockCreated && ( + + {extrinsicLink ? ( + + + {blockCreated}-{indexCreated} + + + + ) : ( + `${blockCreated}-${indexCreated}` + )} + + )} + + )} + + ); +}; diff --git a/src/renderer/features/multisig-operation-details/components/OperationDetails.tsx b/src/renderer/features/multisig-operation-details/components/OperationDetails.tsx new file mode 100644 index 0000000000..7c76a5cb13 --- /dev/null +++ b/src/renderer/features/multisig-operation-details/components/OperationDetails.tsx @@ -0,0 +1,35 @@ +import { useUnit } from 'effector-react'; + +import { type MultisigTransaction } from '@/shared/core'; +import { useI18n } from '@/shared/i18n'; +import { DetailRow } from '@/shared/ui'; +import { AccountExplorers } from '@/shared/ui-entities'; +import { Box } from '@/shared/ui-kit'; +import { networkModel } from '@/entities/network'; +import { WalletIcon, walletModel } from '@/entities/wallet'; + +type Props = { + operation: MultisigTransaction; +}; + +export const OperationDetails = ({ operation }: Props) => { + const { t } = useI18n(); + const chains = useUnit(networkModel.$chains); + const activeWallet = useUnit(walletModel.$activeWallet); + const activeAccounts = useUnit(walletModel.$activeAccounts); + + const accountId = activeAccounts.at(0)?.accountId; + const chain = chains[operation.chainId]; + + if (!activeWallet || !accountId || !chain) return null; + + return ( + + + + {activeWallet.name} + + + + ); +}; diff --git a/src/renderer/features/multisig-operation-details/index.ts b/src/renderer/features/multisig-operation-details/index.ts new file mode 100644 index 0000000000..97985377ea --- /dev/null +++ b/src/renderer/features/multisig-operation-details/index.ts @@ -0,0 +1 @@ +export { multisigOperationDetailsFeature } from './multisig-operation-details-model'; diff --git a/src/renderer/features/multisig-operation-details/multisig-operation-details-model.tsx b/src/renderer/features/multisig-operation-details/multisig-operation-details-model.tsx new file mode 100644 index 0000000000..808c67c5d7 --- /dev/null +++ b/src/renderer/features/multisig-operation-details/multisig-operation-details-model.tsx @@ -0,0 +1,36 @@ +import { createFeature } from '@/shared/feature'; +import { ChainTitle } from '@/entities/chain'; +import { getTransactionFromMultisigTx } from '@/entities/multisig'; +import { TransactionTitle } from '@/entities/transaction'; +import { multisigOperationsFeature } from '@/features/multisig-operations'; + +import { OperationAdvancedDetails } from './components/OperationAdvancedDetails'; +import { OperationDetails } from './components/OperationDetails'; + +export const multisigOperationDetailsFeature = createFeature({ + name: 'multisig/operation details', +}); + +multisigOperationDetailsFeature.inject(multisigOperationsFeature.slots.operationDetails, { + render: ({ operation }) => , + order: 0, +}); + +multisigOperationDetailsFeature.inject(multisigOperationsFeature.slots.operationTitle, ({ operation }) => { + const transaction = getTransactionFromMultisigTx(operation); + + if (transaction && transaction.type) return null; + + return ( + <> + + + + + ); +}); + +multisigOperationDetailsFeature.inject(multisigOperationsFeature.slots.operationDetails, { + render: ({ operation }) => , + order: 999, +}); diff --git a/src/renderer/features/multisig-operations/components/Operation.tsx b/src/renderer/features/multisig-operations/components/Operation.tsx new file mode 100644 index 0000000000..56f5c67a18 --- /dev/null +++ b/src/renderer/features/multisig-operations/components/Operation.tsx @@ -0,0 +1,26 @@ +import { type MultisigTransaction } from '@/shared/core'; +import { createSlot, useSlot } from '@/shared/di'; + +type Props = { + operation: MultisigTransaction; +}; + +type SlotProps = { + operation: MultisigTransaction; +}; + +export const operationDetailsSlot = createSlot(); +export const operationTitleSlot = createSlot(); + +// TODO: Temp solution +export const Operation = ({ operation }: Props) => { + const operationDetails = useSlot(operationDetailsSlot, { props: { operation } }); + const operationTitle = useSlot(operationTitleSlot, { props: { operation } }); + + return ( + + ); +}; diff --git a/src/renderer/features/multisig-operations/components/OperationsList.tsx b/src/renderer/features/multisig-operations/components/OperationsList.tsx new file mode 100644 index 0000000000..b704ce4fcd --- /dev/null +++ b/src/renderer/features/multisig-operations/components/OperationsList.tsx @@ -0,0 +1,19 @@ +import { type MultisigTransaction } from '@/shared/core'; + +import { Operation } from './Operation'; + +type Props = { + operations?: MultisigTransaction[]; +}; + +export const OperationList = ({ operations }: Props) => { + return ( + + ); +}; diff --git a/src/renderer/features/multisig-operations/index.ts b/src/renderer/features/multisig-operations/index.ts index f0c5368ab5..6e9ff91eca 100644 --- a/src/renderer/features/multisig-operations/index.ts +++ b/src/renderer/features/multisig-operations/index.ts @@ -1,3 +1,4 @@ +import { operationDetailsSlot, operationTitleSlot } from './components/Operation'; import { operationsModel } from './model/list'; export const multisigOperationsFeature = { @@ -5,4 +6,8 @@ export const multisigOperationsFeature = { operations: operationsModel, }, views: {}, + slots: { + operationDetails: operationDetailsSlot, + operationTitle: operationTitleSlot, + }, }; diff --git a/src/renderer/features/proxy-add-pure/model/form-model.ts b/src/renderer/features/proxy-add-pure/model/form-model.ts index 87fd9c39c2..0f14136336 100644 --- a/src/renderer/features/proxy-add-pure/model/form-model.ts +++ b/src/renderer/features/proxy-add-pure/model/form-model.ts @@ -479,13 +479,13 @@ sample({ 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.isRegularMultisig(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), + isMultisig: walletUtils.isRegularMultisig(accountWallet), isProxy: true, }; }, diff --git a/src/renderer/features/proxy-add/model/form-model.ts b/src/renderer/features/proxy-add/model/form-model.ts index 87dabca799..08c0c8268c 100644 --- a/src/renderer/features/proxy-add/model/form-model.ts +++ b/src/renderer/features/proxy-add/model/form-model.ts @@ -567,7 +567,7 @@ sample({ filter: (_, account) => Boolean(account), fn: ({ wallet, wallets }, account): Record => { if (!wallet) return { isMultisig: false, isProxy: false }; - if (walletUtils.isRegularMultisig(wallet)) return { isMultisig: true, 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); diff --git a/src/renderer/features/proxy-operation-details/components/ProxyOperationTitle.tsx b/src/renderer/features/proxy-operation-details/components/ProxyOperationTitle.tsx new file mode 100644 index 0000000000..51869e8240 --- /dev/null +++ b/src/renderer/features/proxy-operation-details/components/ProxyOperationTitle.tsx @@ -0,0 +1,31 @@ +import { chainsService } from '@/shared/api/network'; +import { type MultisigTransaction } from '@/shared/core'; +import { getAssetById } from '@/shared/lib/utils'; +import { AssetBalance } from '@/entities/asset'; +import { ChainTitle } from '@/entities/chain'; +import { TransactionTitle, getTransactionAmount } from '@/entities/transaction'; + +type Props = { + operation: MultisigTransaction; +}; + +export const ProxyOperationTitle = ({ operation }: Props) => { + const asset = + operation.transaction && + getAssetById(operation.transaction.args.asset, chainsService.getChainById(operation.chainId)?.assets); + const amount = operation.transaction && getTransactionAmount(operation.transaction); + + return ( + <> + + + {asset && amount && ( +
+ +
+ )} + + + + ); +}; diff --git a/src/renderer/features/proxy-operation-details/index.ts b/src/renderer/features/proxy-operation-details/index.ts new file mode 100644 index 0000000000..24d2b860ed --- /dev/null +++ b/src/renderer/features/proxy-operation-details/index.ts @@ -0,0 +1 @@ +export { proxyOperationDetailFeature } from './proxy-operation-details-model'; diff --git a/src/renderer/features/proxy-operation-details/proxy-operation-details-model.tsx b/src/renderer/features/proxy-operation-details/proxy-operation-details-model.tsx new file mode 100644 index 0000000000..cf39a967ee --- /dev/null +++ b/src/renderer/features/proxy-operation-details/proxy-operation-details-model.tsx @@ -0,0 +1,93 @@ +import { useUnit } from 'effector-react'; + +import { TransactionType } from '@/shared/core'; +import { createFeature } from '@/shared/feature'; +import { useI18n } from '@/shared/i18n'; +import { toAccountId } from '@/shared/lib/utils'; +import { DetailRow, FootnoteText } from '@/shared/ui'; +import { Account } from '@/shared/ui-entities'; +import { getTransactionFromMultisigTx } from '@/entities/multisig'; +import { networkModel } from '@/entities/network'; +import { operationDetailsUtils } from '@/entities/operations'; +import { proxyUtils } from '@/entities/proxy'; +import { + isAddProxyTransaction, + isManageProxyTransaction, + isRemoveProxyTransaction, + isRemovePureProxyTransaction, +} from '@/entities/transaction'; +import { multisigOperationsFeature } from '@/features/multisig-operations'; + +import { ProxyOperationTitle } from './components/ProxyOperationTitle'; + +export const proxyOperationDetailFeature = createFeature({ + name: 'proxy/operation-details', +}); + +proxyOperationDetailFeature.inject(multisigOperationsFeature.slots.operationDetails, { + render: ({ operation }) => { + const { t } = useI18n(); + const transaction = getTransactionFromMultisigTx(operation); + const chains = useUnit(networkModel.$chains); + const chain = chains[operation.chainId]; + + const result = []; + + const delegate = operationDetailsUtils.getDelegate(operation); + const sender = operationDetailsUtils.getSender(operation); + const proxyType = operationDetailsUtils.getProxyType(operation); + + if (isAddProxyTransaction(transaction) && delegate) { + result.push( + + + , + ); + } + + if (isRemoveProxyTransaction(transaction) && delegate) { + result.push( + + + , + ); + } + + if (isRemovePureProxyTransaction(transaction) && sender) { + result.push( + + + , + ); + } + + if (isManageProxyTransaction(transaction) && proxyType) { + result.push( + + {t(proxyUtils.getProxyTypeName(proxyType))} + , + ); + } + + return <>{result.map((e) => e)}; + }, + order: 1, +}); + +proxyOperationDetailFeature.inject(multisigOperationsFeature.slots.operationTitle, ({ operation }) => { + const transaction = getTransactionFromMultisigTx(operation); + + if ( + transaction?.type && + [ + TransactionType.ADD_PROXY, + TransactionType.REMOVE_PROXY, + TransactionType.CREATE_PURE_PROXY, + TransactionType.REMOVE_PURE_PROXY, + ].includes(transaction.type) + ) { + return ; + } + + return null; +}); diff --git a/src/renderer/features/proxy-remove-pure/model/form-model.ts b/src/renderer/features/proxy-remove-pure/model/form-model.ts index cedd0a2f5e..ee3427c558 100644 --- a/src/renderer/features/proxy-remove-pure/model/form-model.ts +++ b/src/renderer/features/proxy-remove-pure/model/form-model.ts @@ -271,13 +271,13 @@ sample({ 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.isRegularMultisig(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), + isMultisig: walletUtils.isRegularMultisig(accountWallet), isProxy: true, }; }, diff --git a/src/renderer/features/proxy-remove/model/form-model.ts b/src/renderer/features/proxy-remove/model/form-model.ts index 1e980f2fcc..4c2e356d19 100644 --- a/src/renderer/features/proxy-remove/model/form-model.ts +++ b/src/renderer/features/proxy-remove/model/form-model.ts @@ -271,7 +271,7 @@ sample({ filter: (_, account) => Boolean(account), fn: ({ wallet, wallets }, account): Record => { if (!wallet) return { isMultisig: false, isProxy: false }; - if (walletUtils.isRegularMultisig(wallet)) return { isMultisig: true, 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); diff --git a/src/renderer/features/staking-operation-details/components/PayeeOperationDetails.tsx b/src/renderer/features/staking-operation-details/components/PayeeOperationDetails.tsx new file mode 100644 index 0000000000..388d525f30 --- /dev/null +++ b/src/renderer/features/staking-operation-details/components/PayeeOperationDetails.tsx @@ -0,0 +1,41 @@ +import { useUnit } from 'effector-react'; + +import { type MultisigTransaction } from '@/shared/core'; +import { useI18n } from '@/shared/i18n'; +import { cnTw } from '@/shared/lib/utils'; +import { type AccountId } from '@/shared/polkadotjs-schemas'; +import { DetailRow } from '@/shared/ui'; +import { Account } from '@/shared/ui-entities'; +import { networkModel } from '@/entities/network'; +import { operationDetailsUtils } from '@/entities/operations'; + +type Props = { + operation: MultisigTransaction; +}; + +export const PayeeOperationDetails = ({ operation }: Props) => { + const { t } = useI18n(); + + const chains = useUnit(networkModel.$chains); + + const result = []; + + const payee = operationDetailsUtils.getPayee(operation); + + if (payee) { + result.push( + + {typeof payee === 'string' ? ( + t('staking.confirmation.restakeRewards') + ) : ( + + )} + , + ); + } + + return <>{result.map((e) => e)}; +}; diff --git a/src/renderer/features/staking-operation-details/components/StakingOperationTitle.tsx b/src/renderer/features/staking-operation-details/components/StakingOperationTitle.tsx new file mode 100644 index 0000000000..09c5603671 --- /dev/null +++ b/src/renderer/features/staking-operation-details/components/StakingOperationTitle.tsx @@ -0,0 +1,31 @@ +import { chainsService } from '@/shared/api/network'; +import { type MultisigTransaction } from '@/shared/core'; +import { getAssetById } from '@/shared/lib/utils'; +import { AssetBalance } from '@/entities/asset'; +import { ChainTitle } from '@/entities/chain'; +import { TransactionTitle, getTransactionAmount } from '@/entities/transaction'; + +type Props = { + operation: MultisigTransaction; +}; + +export const StakingOperationTitle = ({ operation }: Props) => { + const asset = + operation.transaction && + getAssetById(operation.transaction.args.asset, chainsService.getChainById(operation.chainId)?.assets); + const amount = operation.transaction && getTransactionAmount(operation.transaction); + + return ( + <> + + + {asset && amount && ( +
+ +
+ )} + + + + ); +}; diff --git a/src/renderer/features/staking-operation-details/components/ValidatorsOperationDetails.tsx b/src/renderer/features/staking-operation-details/components/ValidatorsOperationDetails.tsx new file mode 100644 index 0000000000..630bb28881 --- /dev/null +++ b/src/renderer/features/staking-operation-details/components/ValidatorsOperationDetails.tsx @@ -0,0 +1,106 @@ +import { useStoreMap, useUnit } from 'effector-react'; +import { useEffect } from 'react'; + +import { chainsService } from '@/shared/api/network'; +import { + type Address, + type MultisigTransaction, + type Transaction, + TransactionType, + type Validator, +} from '@/shared/core'; +import { useI18n } from '@/shared/i18n'; +import { useToggle } from '@/shared/lib/hooks'; +import { cnTw, getAssetById, toAccountId } from '@/shared/lib/utils'; +import { type AccountId } from '@/shared/polkadotjs-schemas'; +import { DetailRow, FootnoteText, Icon } from '@/shared/ui'; +import { identityDomain } from '@/domains/identity'; +import { getTransactionFromMultisigTx } from '@/entities/multisig'; +import { networkModel, networkUtils } from '@/entities/network'; +import { ValidatorsModal, useValidatorsMap } from '@/entities/staking'; + +type Props = { + operation: MultisigTransaction; +}; + +export const ValidatorsOperationDetails = ({ operation }: Props) => { + const { t } = useI18n(); + + const [isValidatorsOpen, toggleValidators] = useToggle(); + + const chains = useUnit(networkModel.$chains); + const apis = useUnit(networkModel.$apis); + const connections = useUnit(networkModel.$connections); + + const api = apis[operation.chainId]; + const connection = connections[operation.chainId]; + const chain = chains[operation.chainId]; + const defaultAsset = chain?.assets[0]; + + const result = []; + + const transaction = getTransactionFromMultisigTx(operation); + const validatorsMap = useValidatorsMap(api, connection && networkUtils.isLightClientConnection(connection)); + + const identities = useStoreMap({ + store: identityDomain.identity.$list, + keys: [operation.chainId], + fn: (value, [chainId]) => value[chainId] ?? {}, + }); + + useEffect(() => { + const accounts = Object.keys(validatorsMap).map(toAccountId) as AccountId[]; + + if (accounts.length === 0) return; + + identityDomain.identity.request({ chainId: operation.chainId, accounts }); + }, [validatorsMap]); + + const allValidators = Object.values(validatorsMap); + + const startStakingValidators: Address[] = + (transaction?.type === TransactionType.BATCH_ALL && + transaction.args.transactions.find((tx: Transaction) => tx.type === TransactionType.NOMINATE)?.args?.targets) || + []; + + const selectedValidators: Validator[] = + allValidators.filter((v) => (transaction?.args.targets || startStakingValidators).includes(v.address)) || []; + const selectedValidatorsAddress = selectedValidators.map((validator) => validator.address); + const notSelectedValidators = allValidators.filter((v) => !selectedValidatorsAddress.includes(v.address)); + const validatorsAsset = + transaction && getAssetById(transaction.args.asset, chainsService.getChainById(operation.chainId)?.assets); + + if (Boolean(selectedValidators?.length) && defaultAsset) { + result.push( + <> + + + + + + , + ); + } + + return <>{result.map((e) => e)}; +}; diff --git a/src/renderer/features/staking-operation-details/index.ts b/src/renderer/features/staking-operation-details/index.ts new file mode 100644 index 0000000000..81770e5f43 --- /dev/null +++ b/src/renderer/features/staking-operation-details/index.ts @@ -0,0 +1 @@ +export { stakingOperationDetailFeature } from './staking-operation-details-model'; diff --git a/src/renderer/features/staking-operation-details/staking-operation-details-model.tsx b/src/renderer/features/staking-operation-details/staking-operation-details-model.tsx new file mode 100644 index 0000000000..1e6f61dc4a --- /dev/null +++ b/src/renderer/features/staking-operation-details/staking-operation-details-model.tsx @@ -0,0 +1,68 @@ +import { TransactionType } from '@/shared/core'; +import { createFeature } from '@/shared/feature'; +import { getTransactionFromMultisigTx } from '@/entities/multisig'; +import { multisigOperationsFeature } from '@/features/multisig-operations'; + +import { PayeeOperationDetails } from './components/PayeeOperationDetails'; +import { StakingOperationTitle } from './components/StakingOperationTitle'; +import { ValidatorsOperationDetails } from './components/ValidatorsOperationDetails'; + +export const stakingOperationDetailFeature = createFeature({ + name: 'staking/operation-details', +}); + +stakingOperationDetailFeature.inject(multisigOperationsFeature.slots.operationDetails, { + render: ({ operation }) => { + const transaction = getTransactionFromMultisigTx(operation); + + if ( + transaction?.type && + [ + TransactionType.BOND, + TransactionType.STAKE_MORE, + TransactionType.UNSTAKE, + TransactionType.RESTAKE, + TransactionType.REDEEM, + ].includes(transaction.type) + ) { + return ; + } + + return null; + }, + order: 1, +}); + +stakingOperationDetailFeature.inject(multisigOperationsFeature.slots.operationDetails, { + render: ({ operation }) => { + const transaction = getTransactionFromMultisigTx(operation); + + if (transaction?.type && [TransactionType.BOND, TransactionType.NOMINATE].includes(transaction.type)) { + return ; + } + + return null; + }, + order: 2, +}); + +stakingOperationDetailFeature.inject(multisigOperationsFeature.slots.operationTitle, ({ operation }) => { + const transaction = getTransactionFromMultisigTx(operation); + + if ( + transaction?.type && + [ + TransactionType.BOND, + TransactionType.STAKE_MORE, + TransactionType.UNSTAKE, + TransactionType.RESTAKE, + TransactionType.REDEEM, + TransactionType.NOMINATE, + TransactionType.DESTINATION, + ].includes(transaction.type) + ) { + return ; + } + + return null; +}); diff --git a/src/renderer/features/transfer-operation-details/components/TransferOperationDetails.tsx b/src/renderer/features/transfer-operation-details/components/TransferOperationDetails.tsx new file mode 100644 index 0000000000..8accdafa92 --- /dev/null +++ b/src/renderer/features/transfer-operation-details/components/TransferOperationDetails.tsx @@ -0,0 +1,63 @@ +import { useUnit } from 'effector-react'; + +import { type MultisigTransaction } from '@/shared/core'; +import { useI18n } from '@/shared/i18n'; +import { toAccountId } from '@/shared/lib/utils'; +import { DetailRow } from '@/shared/ui'; +import { Account } from '@/shared/ui-entities'; +import { ChainTitle } from '@/entities/chain'; +import { getTransactionFromMultisigTx } from '@/entities/multisig'; +import { networkModel } from '@/entities/network'; +import { operationDetailsUtils } from '@/entities/operations'; +import { isXcmTransaction } from '@/entities/transaction'; + +type Props = { + operation: MultisigTransaction; +}; + +export const TransferOperationDetails = ({ operation }: Props) => { + const { t } = useI18n(); + const chains = useUnit(networkModel.$chains); + + const transaction = getTransactionFromMultisigTx(operation); + + const result = []; + + const destination = operationDetailsUtils.getDestinationAccountId(operation); + const sender = operationDetailsUtils.getSender(operation); + const destinationChain = operationDetailsUtils.getDestinationChain(operation); + + if (destination) { + result.push( + + + , + ); + } + + if (isXcmTransaction(transaction) && sender) { + result.push( + + + , + ); + } + + if (isXcmTransaction(transaction)) { + result.push( + + + , + ); + } + + if (isXcmTransaction(transaction) && destinationChain) { + result.push( + + + , + ); + } + + return <>{result.map((e) => e)}; +}; diff --git a/src/renderer/features/transfer-operation-details/components/TransferOperationTitle.tsx b/src/renderer/features/transfer-operation-details/components/TransferOperationTitle.tsx new file mode 100644 index 0000000000..2e602bbb76 --- /dev/null +++ b/src/renderer/features/transfer-operation-details/components/TransferOperationTitle.tsx @@ -0,0 +1,31 @@ +import { chainsService } from '@/shared/api/network'; +import { type MultisigTransaction } from '@/shared/core'; +import { getAssetById } from '@/shared/lib/utils'; +import { AssetBalance } from '@/entities/asset'; +import { ChainTitle } from '@/entities/chain'; +import { TransactionTitle, getTransactionAmount } from '@/entities/transaction'; + +type Props = { + operation: MultisigTransaction; +}; + +export const TransferOperationTitle = ({ operation }: Props) => { + const asset = + operation.transaction && + getAssetById(operation.transaction.args.asset, chainsService.getChainById(operation.chainId)?.assets); + const amount = operation.transaction && getTransactionAmount(operation.transaction); + + return ( + <> + + + {asset && amount && ( +
+ +
+ )} + + + + ); +}; diff --git a/src/renderer/features/transfer-operation-details/components/XcmTransferOperationTitle.tsx b/src/renderer/features/transfer-operation-details/components/XcmTransferOperationTitle.tsx new file mode 100644 index 0000000000..4c570239d8 --- /dev/null +++ b/src/renderer/features/transfer-operation-details/components/XcmTransferOperationTitle.tsx @@ -0,0 +1,35 @@ +import { chainsService } from '@/shared/api/network'; +import { type FlexibleMultisigTransactionDS, type MultisigTransactionDS } from '@/shared/api/storage'; +import { getAssetById } from '@/shared/lib/utils'; +import { AssetBalance } from '@/entities/asset'; +import { XcmChains } from '@/entities/chain'; +import { TransactionTitle, getTransactionAmount } from '@/entities/transaction'; + +type Props = { + operation: MultisigTransactionDS | FlexibleMultisigTransactionDS; +}; + +export const XcmTransferOperationTitle = ({ operation }: Props) => { + const assetId = operation.transaction?.args.assetId || operation.transaction?.args.asset; + const asset = getAssetById(assetId, chainsService.getChainById(operation.chainId)?.assets); + + const amount = operation.transaction && getTransactionAmount(operation.transaction); + + return ( + <> + + + {asset && amount && ( +
+ +
+ )} + + + + ); +}; diff --git a/src/renderer/features/transfer-operation-details/index.ts b/src/renderer/features/transfer-operation-details/index.ts new file mode 100644 index 0000000000..bd37c6cfc9 --- /dev/null +++ b/src/renderer/features/transfer-operation-details/index.ts @@ -0,0 +1 @@ +export { transferOperationDetailFeature } from './transfer-operation-details-model'; diff --git a/src/renderer/features/transfer-operation-details/transfer-operation-details-model.tsx b/src/renderer/features/transfer-operation-details/transfer-operation-details-model.tsx new file mode 100644 index 0000000000..a6c5d883b8 --- /dev/null +++ b/src/renderer/features/transfer-operation-details/transfer-operation-details-model.tsx @@ -0,0 +1,39 @@ +import { createFeature } from '@/shared/feature'; +import { getTransactionFromMultisigTx } from '@/entities/multisig'; +import { isTransferTransaction, isXcmTransaction } from '@/entities/transaction'; +import { multisigOperationsFeature } from '@/features/multisig-operations'; + +import { TransferOperationDetails } from './components/TransferOperationDetails'; +import { TransferOperationTitle } from './components/TransferOperationTitle'; +import { XcmTransferOperationTitle } from './components/XcmTransferOperationTitle'; + +export const transferOperationDetailFeature = createFeature({ + name: 'transfer/operations', +}); + +transferOperationDetailFeature.inject(multisigOperationsFeature.slots.operationDetails, { + render: ({ operation }) => { + const transaction = getTransactionFromMultisigTx(operation); + + if (isTransferTransaction(transaction) || isXcmTransaction(transaction)) { + return ; + } + + return null; + }, + order: 1, +}); + +transferOperationDetailFeature.inject(multisigOperationsFeature.slots.operationTitle, ({ operation }) => { + const transaction = getTransactionFromMultisigTx(operation); + + if (isTransferTransaction(transaction)) { + return ; + } + + if (isXcmTransaction(transaction)) { + return ; + } + + return null; +}); diff --git a/src/renderer/pages/Operations/components/Details.tsx b/src/renderer/pages/Operations/components/Details.tsx index 59f30d501b..613dfe3b52 100644 --- a/src/renderer/pages/Operations/components/Details.tsx +++ b/src/renderer/pages/Operations/components/Details.tsx @@ -28,6 +28,7 @@ import { ChainTitle } from '@/entities/chain'; import { TracksDetails, voteTransactionService } from '@/entities/governance'; import { getTransactionFromMultisigTx } from '@/entities/multisig'; import { networkModel, networkUtils } from '@/entities/network'; +import { operationDetailsUtils } from '@/entities/operations'; import { proxyUtils } from '@/entities/proxy'; import { SelectedValidatorsModal, useValidatorsMap } from '@/entities/staking'; import { @@ -42,21 +43,6 @@ import { } from '@/entities/transaction'; import { WalletIcon, walletModel } from '@/entities/wallet'; import { InteractionStyle } from '../common/constants'; -import { - getDelegate, - getDelegationTarget, - getDelegationTracks, - getDelegationVotes, - getDestination, - getDestinationChain, - getPayee, - getProxyType, - getReferendumId, - getSpawner, - getUndelegationData, - getVote, - // eslint-disable-next-line import-x/max-dependencies -} from '../common/utils'; type Props = { tx: MultisigTransaction | FlexibleMultisigTransaction; @@ -78,23 +64,23 @@ export const Details = ({ api, tx, account, chain, signatory }: Props) => { const wallets = useUnit(walletModel.$wallets); const chains = useUnit(networkModel.$chains); - const payee = getPayee(tx); - const spawner = getSpawner(tx); - const delegate = getDelegate(tx); - const proxyType = getProxyType(tx); - const destinationChain = getDestinationChain(tx); - const destination = getDestination(tx, chains, destinationChain); + const payee = operationDetailsUtils.getPayee(tx); + const spawner = operationDetailsUtils.getSpawner(tx); + const delegate = operationDetailsUtils.getDelegate(tx); + const proxyType = operationDetailsUtils.getProxyType(tx); + const destinationChain = operationDetailsUtils.getDestinationChain(tx); + const destination = operationDetailsUtils.getDestination(tx, chains, destinationChain); - const delegationTarget = getDelegationTarget(tx); - const delegationTracks = getDelegationTracks(tx); - const delegationVotes = getDelegationVotes(tx); + const delegationTarget = operationDetailsUtils.getDelegationTarget(tx); + const delegationTracks = operationDetailsUtils.getDelegationTracks(tx); + const delegationVotes = operationDetailsUtils.getDelegationVotes(tx); const [isUndelegationLoading, setIsUndelegationLoading] = useState(false); const [undelegationVotes, setUndelegationVotes] = useState(); const [undelegationTarget, setUndelegationTarget] = useState
(); - const referendumId = getReferendumId(tx); - const vote = getVote(tx); + const referendumId = operationDetailsUtils.getReferendumId(tx); + const vote = operationDetailsUtils.getVote(tx); const identities = useStoreMap({ store: identityDomain.identity.$list, @@ -111,7 +97,7 @@ export const Details = ({ api, tx, account, chain, signatory }: Props) => { if (!api) return; - getUndelegationData(api, tx).then(({ votes, target }) => { + operationDetailsUtils.getUndelegationData(api, tx).then(({ votes, target }) => { setUndelegationVotes(votes); setUndelegationTarget(target); setIsUndelegationLoading(false); diff --git a/src/renderer/pages/Operations/components/FlexibleMultisigShell.tsx b/src/renderer/pages/Operations/components/FlexibleMultisigShell.tsx index 91a4ebb3eb..d04e007983 100644 --- a/src/renderer/pages/Operations/components/FlexibleMultisigShell.tsx +++ b/src/renderer/pages/Operations/components/FlexibleMultisigShell.tsx @@ -19,12 +19,11 @@ import { Accordion, Box, Progress } from '@/shared/ui-kit'; import { contactModel } from '@/entities/contact'; import { useMultisigEvent } from '@/entities/multisig'; import { type ExtendedChain, useNetworkData } from '@/entities/network'; +import { Status, operationDetailsUtils } from '@/entities/operations'; import { SignatoryCard, signatoryUtils } from '@/entities/signatory'; import { WalletIcon, permissionUtils, walletModel } from '@/entities/wallet'; -import { getSignatoryName, getSignatoryStatus } from '../common/utils'; import { OperationAdvancedDetails } from './OperationAdvancedDetails'; -import { Status } from './Status'; import ApproveTxModal from './modals/ApproveTx'; import RejectTxModal from './modals/RejectTx'; @@ -109,7 +108,7 @@ const Signatories = memo(({ signatories, connection, events }: SignatoriesParams const walletSignatories = signatories .reduce((acc, signatory) => { const signatoryWallet = signatoryUtils.getSignatoryWallet(wallets, signatory.accountId); - const status = getSignatoryStatus(events, signatory.accountId); + const status = operationDetailsUtils.getSignatoryStatus(events, signatory.accountId); if (signatoryWallet) { acc.push({ ...signatory, wallet: signatoryWallet, status }); @@ -154,11 +153,11 @@ const Signatories = memo(({ signatories, connection, events }: SignatoriesParams key={signatory.accountId} accountId={signatory.accountId} addressPrefix={connection.addressPrefix} - status={getSignatoryStatus(events, signatory.accountId)} + status={operationDetailsUtils.getSignatoryStatus(events, signatory.accountId)} explorers={connection.explorers} >
{ - const { formatDate } = useI18n(); - const { getLiveEventsByKeys } = useMultisigEvent({}); - - const events = getLiveEventsByKeys([tx]); - const approvals = events?.filter((e) => e.status === 'SIGNED') || []; - const initEvent = approvals.find((e) => e.accountId === tx.depositor); - const date = new Date(tx.dateCreated || initEvent?.dateCreated || Date.now()); - - const assetId = tx.transaction?.args.assetId || tx.transaction?.args.asset; - const asset = getAssetById(assetId, chainsService.getChainById(tx.chainId)?.assets); - const amount = tx.transaction && getTransactionAmount(tx.transaction); + const operationTitle = useSlot(multisigOperationsFeature.slots.operationTitle, { + props: { + operation: tx, + }, + }); return (
-
- - {formatDate(date, 'p')} - -
- - - - {asset && amount && ( -
- -
- )} - - {isXcmTransaction(tx.transaction) ? ( - - ) : ( - - )} - -
- -
+ + {operationTitle} +
diff --git a/src/renderer/pages/Operations/components/OperationAdvancedDetails.tsx b/src/renderer/pages/Operations/components/OperationAdvancedDetails.tsx index fc04f14be7..c8f9e48cf9 100644 --- a/src/renderer/pages/Operations/components/OperationAdvancedDetails.tsx +++ b/src/renderer/pages/Operations/components/OperationAdvancedDetails.tsx @@ -5,10 +5,10 @@ import { DetailRow, FootnoteText, Icon } from '@/shared/ui'; import { Account } from '@/shared/ui-entities'; import { Box } from '@/shared/ui-kit'; import { AssetBalance } from '@/entities/asset'; +import { operationDetailsUtils } from '@/entities/operations'; import { signatoryUtils } from '@/entities/signatory'; import { ExplorersPopover, WalletCardSm } from '@/entities/wallet'; import { InteractionStyle } from '../common/constants'; -import { getMultisigExtrinsicLink } from '../common/utils'; type Props = { tx: MultisigTransaction | FlexibleMultisigTransaction; @@ -21,7 +21,12 @@ export const OperationAdvancedDetails = ({ tx, wallets, chain }: Props) => { const { signatories, indexCreated, blockCreated, deposit, depositor, callHash, callData } = tx; const valueClass = 'text-text-secondary'; - const extrinsicLink = getMultisigExtrinsicLink(callHash, indexCreated, blockCreated, chain.explorers); + const extrinsicLink = operationDetailsUtils.getMultisigExtrinsicLink( + callHash, + indexCreated, + blockCreated, + chain.explorers, + ); const depositorSignatory = signatories.find((s) => s.accountId === depositor); const depositorWallet = depositorSignatory && signatoryUtils.getSignatoryWallet(wallets, depositorSignatory.accountId); diff --git a/src/renderer/pages/Operations/components/OperationCardDetails.tsx b/src/renderer/pages/Operations/components/OperationCardDetails.tsx deleted file mode 100644 index 0912e987ee..0000000000 --- a/src/renderer/pages/Operations/components/OperationCardDetails.tsx +++ /dev/null @@ -1,358 +0,0 @@ -import { useStoreMap, useUnit } from 'effector-react'; -import { useEffect, useState } from 'react'; -import { Trans } from 'react-i18next'; - -import { chainsService } from '@/shared/api/network'; -import { - type Address as AddressType, - type Chain, - type FlexibleMultisigAccount, - type FlexibleMultisigTransaction, - type MultisigAccount, - type MultisigTransaction, - type Transaction, - TransactionType, - type Validator, -} from '@/shared/core'; -import { useI18n } from '@/shared/i18n'; -import { useToggle } from '@/shared/lib/hooks'; -import { cnTw, getAssetById, nonNullable, toAccountId } from '@/shared/lib/utils'; -import { type AccountId } from '@/shared/polkadotjs-schemas'; -import { Button, DetailRow, FootnoteText, Icon } from '@/shared/ui'; -import { Account } from '@/shared/ui-entities'; -import { Skeleton } from '@/shared/ui-kit'; -import { identityDomain } from '@/domains/identity'; -import { AssetBalance } from '@/entities/asset'; -import { ChainTitle } from '@/entities/chain'; -import { TracksDetails, voteTransactionService } from '@/entities/governance'; -import { getTransactionFromMultisigTx } from '@/entities/multisig'; -import { type ExtendedChain, networkModel, networkUtils } from '@/entities/network'; -import { proxyUtils } from '@/entities/proxy'; -import { ValidatorsModal, useValidatorsMap } from '@/entities/staking'; -import { - isAddProxyTransaction, - isManageProxyTransaction, - isRemoveProxyTransaction, - isRemovePureProxyTransaction, - isUndelegateTransaction, - isXcmTransaction, -} from '@/entities/transaction'; -import { ExplorersPopover, WalletCardSm, walletModel } from '@/entities/wallet'; -import { InteractionStyle } from '../common/constants'; -import { - getDelegate, - getDelegationTarget, - getDelegationTracks, - getDelegationVotes, - getDestination, - getDestinationChain, - getPayee, - getProxyType, - getReferendumId, - getSender, - getUndelegationData, - getVote, - // eslint-disable-next-line import-x/max-dependencies -} from '../common/utils'; - -import { OperationAdvancedDetails } from './OperationAdvancedDetails'; - -type Props = { - tx: MultisigTransaction | FlexibleMultisigTransaction; - account: MultisigAccount | FlexibleMultisigAccount | null; - extendedChain?: ExtendedChain; -}; - -export const OperationCardDetails = ({ tx, account, extendedChain }: Props) => { - const { t } = useI18n(); - - const activeWallet = useUnit(walletModel.$activeWallet); - const wallets = useUnit(walletModel.$wallets); - const chains = useUnit(networkModel.$chains); - - const payee = getPayee(tx); - const sender = getSender(tx); - const delegate = getDelegate(tx); - const proxyType = getProxyType(tx); - const destinationChain = getDestinationChain(tx); - const destination = getDestination(tx, chains, destinationChain); - - const delegationTarget = getDelegationTarget(tx); - const delegationTracks = getDelegationTracks(tx); - const delegationVotes = getDelegationVotes(tx); - - const referendumId = getReferendumId(tx); - const vote = getVote(tx); - - const api = extendedChain?.api; - const defaultAsset = extendedChain?.assets[0]; - const addressPrefix = extendedChain?.addressPrefix; - const explorers = extendedChain?.explorers; - const connection = extendedChain?.connection; - - const [isUndelegationLoading, setIsUndelegationLoading] = useState(false); - const [undelegationVotes, setUndelegationVotes] = useState(); - const [undelegationTarget, setUndelegationTarget] = useState(); - - const identities = useStoreMap({ - store: identityDomain.identity.$list, - keys: [tx.chainId], - fn: (value, [chainId]) => value[chainId] ?? {}, - }); - - useEffect(() => { - if (isUndelegateTransaction(transaction)) { - setIsUndelegationLoading(true); - } - - if (!api) return; - - getUndelegationData(api, tx).then(({ votes, target }) => { - setUndelegationVotes(votes); - setUndelegationTarget(target); - setIsUndelegationLoading(false); - }); - }, [api, tx]); - - const [isAdvancedShown, toggleAdvanced] = useToggle(); - const [isValidatorsOpen, toggleValidators] = useToggle(); - - const transaction = getTransactionFromMultisigTx(tx); - const validatorsMap = useValidatorsMap(api, connection && networkUtils.isLightClientConnection(connection)); - - const allValidators = Object.values(validatorsMap); - - useEffect(() => { - const accounts = Object.keys(validatorsMap).map(toAccountId) as AccountId[]; - - if (accounts.length === 0) return; - - identityDomain.identity.request({ chainId: tx.chainId, accounts }); - }, [validatorsMap]); - - const startStakingValidators: AddressType[] = - (tx.transaction?.type === TransactionType.BATCH_ALL && - tx.transaction.args.transactions.find((tx: Transaction) => tx.type === TransactionType.NOMINATE)?.args - ?.targets) || - []; - - const selectedValidators: Validator[] = - allValidators.filter((v) => (transaction?.args.targets || startStakingValidators).includes(v.address)) || []; - const selectedValidatorsAddress = selectedValidators.map((validator) => validator.address); - const notSelectedValidators = allValidators.filter((v) => !selectedValidatorsAddress.includes(v.address)); - - const validatorsAsset = - transaction && getAssetById(transaction.args.asset, chainsService.getChainById(tx.chainId)?.assets); - - const valueClass = 'min-w-min text-text-secondary'; - - return ( -
- {account && activeWallet && ( - -
- } - address={account.accountId} - explorers={explorers} - addressPrefix={addressPrefix} - /> -
-
- )} - - {isXcmTransaction(transaction) && ( - <> - {sender && ( - - - - )} - - - - - - {destinationChain && ( - - - - )} - - )} - - {destination && ( - - - - )} - - {isAddProxyTransaction(transaction) && delegate && ( - - - - )} - - {isRemoveProxyTransaction(transaction) && delegate && ( - - - - )} - - {isRemovePureProxyTransaction(transaction) && sender && ( - - - - )} - - {isManageProxyTransaction(transaction) && proxyType && ( - - {t(proxyUtils.getProxyTypeName(proxyType))} - - )} - - {referendumId && ( - - #{referendumId} - - )} - - {vote && ( - - - <> - - {t(`governance.referendum.${voteTransactionService.getDecision(vote)}`)} - - :{' '} - - ), - }} - /> - - - - )} - - {isUndelegationLoading && ( - <> - - - - - - - - - )} - - {delegationTarget && ( - - - - )} - - {!delegationTarget && undelegationTarget && ( - - - - )} - - {delegationVotes && ( - - - - - - )} - - {!delegationVotes && undelegationVotes && ( - - - - - - )} - - {delegationTracks && ( - - - - )} - - {Boolean(selectedValidators?.length) && defaultAsset && ( - <> - - - - - - )} - - {payee && ( - - {typeof payee === 'string' ? ( - t('staking.confirmation.restakeRewards') - ) : ( - - )} - - )} - - - - {isAdvancedShown && nonNullable(account) && nonNullable(extendedChain) && ( - - )} -
- ); -}; diff --git a/src/renderer/pages/Operations/components/OperationFullInfo.tsx b/src/renderer/pages/Operations/components/OperationFullInfo.tsx index aff389270f..40e9b4e01e 100644 --- a/src/renderer/pages/Operations/components/OperationFullInfo.tsx +++ b/src/renderer/pages/Operations/components/OperationFullInfo.tsx @@ -3,17 +3,17 @@ import { useStoreMap, useUnit } from 'effector-react'; import { useMultisigChainContext } from '@/app/providers'; import { type FlexibleMultisigTransactionDS, type MultisigTransactionDS } from '@/shared/api/storage'; import { type CallData, type FlexibleMultisigAccount, type MultisigAccount } from '@/shared/core'; +import { useSlot } from '@/shared/di'; import { useI18n } from '@/shared/i18n'; import { useToggle } from '@/shared/lib/hooks'; import { validateCallData } from '@/shared/lib/utils'; import { Button, Icon, InfoLink, SmallTitleText } from '@/shared/ui'; import { useMultisigTx } from '@/entities/multisig'; import { useNetworkData } from '@/entities/network'; -import { operationsModel } from '@/entities/operations'; +import { operationDetailsUtils, operationsModel } from '@/entities/operations'; import { permissionUtils, walletModel, walletUtils } from '@/entities/wallet'; -import { getMultisigExtrinsicLink } from '../common/utils'; +import { multisigOperationsFeature } from '@/features/multisig-operations'; -import { OperationCardDetails } from './OperationCardDetails'; import { OperationSignatories } from './OperationSignatories'; import ApproveTxModal from './modals/ApproveTx'; import CallDataModal from './modals/CallDataModal'; @@ -53,7 +53,12 @@ export const OperationFullInfo = ({ tx, account }: Props) => { const [isCallDataModalOpen, toggleCallDataModal] = useToggle(); - const explorerLink = getMultisigExtrinsicLink(tx.callHash, tx.indexCreated, tx.blockCreated, chain?.explorers); + const explorerLink = operationDetailsUtils.getMultisigExtrinsicLink( + tx.callHash, + tx.indexCreated, + tx.blockCreated, + chain?.explorers, + ); const setupCallData = async (callData: CallData) => { if (!api || !tx) return; @@ -69,6 +74,11 @@ export const OperationFullInfo = ({ tx, account }: Props) => { return hasDepositor && permissionUtils.canRejectMultisigTx(wallet); }); + const operationDetails = useSlot(multisigOperationsFeature.slots.operationDetails, { + props: { + operation: tx, + }, + }); const isFinalSigning = events.length === activeWallet.accounts[0].threshold - 1; const isApproveAvailable = !isFinalSigning || (tx.callData && validateCallData(tx.callData, tx.callHash)); @@ -95,7 +105,7 @@ export const OperationFullInfo = ({ tx, account }: Props) => { )} - +
{operationDetails}
{connection && isRejectAvailable && account && ( diff --git a/src/renderer/pages/Operations/components/OperationSignatories.tsx b/src/renderer/pages/Operations/components/OperationSignatories.tsx index 64bfba2fe4..dc5477d7f5 100644 --- a/src/renderer/pages/Operations/components/OperationSignatories.tsx +++ b/src/renderer/pages/Operations/components/OperationSignatories.tsx @@ -18,9 +18,9 @@ import { Address } from '@/shared/ui-entities'; import { contactModel } from '@/entities/contact'; import { useMultisigEvent } from '@/entities/multisig'; import { type ExtendedChain } from '@/entities/network'; +import { operationDetailsUtils } from '@/entities/operations'; import { SignatoryCard, signatoryUtils } from '@/entities/signatory'; import { WalletIcon, walletModel } from '@/entities/wallet'; -import { getSignatoryName, getSignatoryStatus } from '../common/utils'; import LogModal from './LogModal'; @@ -112,7 +112,7 @@ export const OperationSignatories = ({ tx, connection, account }: Props) => { key={signatory.accountId} accountId={signatory.accountId} addressPrefix={connection.addressPrefix} - status={getSignatoryStatus(events, signatory.accountId)} + status={operationDetailsUtils.getSignatoryStatus(events, signatory.accountId)} explorers={connection.explorers} > @@ -134,11 +134,11 @@ export const OperationSignatories = ({ tx, connection, account }: Props) => { key={signatory.accountId} accountId={signatory.accountId} addressPrefix={connection.addressPrefix} - status={getSignatoryStatus(events, signatory.accountId)} + status={operationDetailsUtils.getSignatoryStatus(events, signatory.accountId)} explorers={connection.explorers} >
{ return acc; }, []); - const unsignedAccounts = getSignatoryAccounts(availableAccounts, wallets, events, account.signatories, tx.chainId); + const unsignedAccounts = operationDetailsUtils.getSignatoryAccounts( + availableAccounts, + wallets, + events, + account.signatories, + tx.chainId, + ); useEffect(() => { priceProviderModel.events.assetsPricesRequested({ includeRates: true }); diff --git a/src/renderer/shared/feature/createFeature.ts b/src/renderer/shared/feature/createFeature.ts index f5b7df961a..21cbc1c4e9 100644 --- a/src/renderer/shared/feature/createFeature.ts +++ b/src/renderer/shared/feature/createFeature.ts @@ -140,7 +140,7 @@ export const createFeature = ({ }; identifier.registerHandler({ - key: `feature: ${name}`, + // key: `feature: ${name}`, // TODO create correct feature toggle using effector tools // eslint-disable-next-line effector/no-getState available: () => (scope ? scope.getState(enable) : enable.getState()), @@ -148,7 +148,7 @@ export const createFeature = ({ }); } else { identifier.registerHandler({ - key: `feature: ${name}`, + // key: `feature: ${name}`, available: () => { // TODO create correct feature toggle using effector tools // eslint-disable-next-line effector/no-getState