diff --git a/packages/blockchain-link-types/src/responses.ts b/packages/blockchain-link-types/src/responses.ts index a28c3afdba3..6e26e189edd 100644 --- a/packages/blockchain-link-types/src/responses.ts +++ b/packages/blockchain-link-types/src/responses.ts @@ -98,6 +98,7 @@ export interface EstimateFee { payload: { feePerUnit: string; feePerTx?: string; + feePayer?: string; feeLimit?: string; eip1559?: Eip1559Fees; }[]; diff --git a/packages/blockchain-link/src/workers/solana/fee.ts b/packages/blockchain-link/src/workers/solana/fee.ts index 9727f95d65c..3cdc8cd28f8 100644 --- a/packages/blockchain-link/src/workers/solana/fee.ts +++ b/packages/blockchain-link/src/workers/solana/fee.ts @@ -1,6 +1,7 @@ import { Address, Base64EncodedWireTransaction, + CompilableTransactionMessage, CompiledTransactionMessage, GetFeeForMessageApi, GetRecentPrioritizationFeesApi, @@ -9,7 +10,6 @@ import { SimulateTransactionApi, TransactionMessageBytes, TransactionMessageBytesBase64, - decompileTransactionMessage, getBase64Decoder, getCompiledTransactionMessageEncoder, getTransactionEncoder, @@ -56,12 +56,12 @@ export const getBaseFee = async ( // https://solana.com/developers/guides/advanced/how-to-use-priority-fees#how-do-i-estimate-priority-fees export const getPriorityFee = async ( api: Rpc, + decompiledMessage: CompilableTransactionMessage, compiledMessage: CompiledTransactionMessage, signatures: SignaturesMap, ) => { - const message = decompileTransactionMessage(compiledMessage); const affectedAccounts = new Set
( - message.instructions + decompiledMessage.instructions .flatMap(instruction => instruction.accounts ?? []) .filter(({ role }) => isWritableRole(role)) .map(({ address }) => address), diff --git a/packages/blockchain-link/src/workers/solana/index.ts b/packages/blockchain-link/src/workers/solana/index.ts index f16d1d38ae9..ddd68aef26d 100644 --- a/packages/blockchain-link/src/workers/solana/index.ts +++ b/packages/blockchain-link/src/workers/solana/index.ts @@ -22,6 +22,7 @@ import { createSolanaRpcFromTransport, createSolanaRpcSubscriptions, decompileTransactionMessage, + decompileTransactionMessageFetchingLookupTables, getBase16Encoder, getBase64Encoder, getCompiledTransactionMessageDecoder, @@ -482,7 +483,17 @@ const estimateFee = async (request: Request) => { const transaction = pipe(messageHex, getBase16Encoder().encode, getTransactionDecoder().decode); const message = pipe(transaction.messageBytes, getCompiledTransactionMessageDecoder().decode); - const priorityFee = await getPriorityFee(api.rpc, message, transaction.signatures); + const decompiledTransactionMessage = await decompileTransactionMessageFetchingLookupTables( + message, + api.rpc, + ); + + const priorityFee = await getPriorityFee( + api.rpc, + decompiledTransactionMessage, + message, + transaction.signatures, + ); const baseFee = await getBaseFee(api.rpc, message); const accountCreationFee = newAccountProgramName @@ -501,6 +512,7 @@ const estimateFee = async (request: Request) => { .toString(10), feePerUnit: priorityFee.computeUnitPrice, feeLimit: priorityFee.computeUnitLimit, + feePayer: decompiledTransactionMessage.feePayer.address, }, ]; diff --git a/packages/components/src/components/form/Select/Select.tsx b/packages/components/src/components/form/Select/Select.tsx index 401c607cd5a..80410eb15ef 100644 --- a/packages/components/src/components/form/Select/Select.tsx +++ b/packages/components/src/components/form/Select/Select.tsx @@ -116,6 +116,7 @@ type WrapperProps = TransientProps< $isWithPlaceholder: boolean; $elevation: Elevation; $isLoading?: boolean; + $menuFitContent?: boolean; }; const SelectWrapper = styled.div` @@ -230,6 +231,11 @@ const SelectWrapper = styled.div` ${menuStyle} border: none; z-index: ${zIndices.base}; + ${({ $menuFitContent }) => + $menuFitContent && + css` + width: fit-content; + `} } ${({ $isDisabled }) => @@ -276,6 +282,7 @@ export type SelectProps = KeyPressScrollProps & label?: ReactNode; size?: InputSize; minValueWidth?: string; + menuFitContent?: boolean; isMenuOpen?: boolean; isLoading?: boolean; onChange?: (value: Option, ref?: SelectInstance | null) => void; @@ -289,6 +296,7 @@ export const Select = ({ useKeyPressScroll, isSearchable = false, minValueWidth = 'initial', + menuFitContent, isMenuOpen, components, onChange, @@ -348,6 +356,7 @@ export const Select = ({ $isSearchable={isSearchable} $size={size} $minValueWidth={minValueWidth} + $menuFitContent={menuFitContent} $isDisabled={isDisabled} $isLoading={isLoading} $isMenuOpen={isMenuOpen} diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/WalletConnectProposalModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/WalletConnectProposalModal.tsx index 2153f13af45..fdfdf81689a 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/WalletConnectProposalModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/WalletConnectProposalModal.tsx @@ -1,34 +1,131 @@ +import { useEffect, useMemo, useState } from 'react'; +import { GroupBase } from 'react-select'; + +import { TrezorDevice } from '@suite-common/suite-types'; +import { AccountType, NetworkType } from '@suite-common/wallet-config'; +import { selectAccounts, selectDevices } from '@suite-common/wallet-core'; +import { Account } from '@suite-common/wallet-types'; import { selectPendingProposal, sessionProposalApproveThunk, sessionProposalRejectThunk, } from '@suite-common/walletconnect'; -import { Banner, Card, H2, NewModal, Note, Paragraph, Row, Text } from '@trezor/components'; +import { PendingConnectionProposalNetwork } from '@suite-common/walletconnect/src/walletConnectTypes'; +import { + Banner, + Card, + H2, + NewModal, + Note, + Option, + Paragraph, + Row, + Select, + Text, +} from '@trezor/components'; import { BannerButton } from '@trezor/components/src/components/Banner/BannerButton'; import { CoinLogo } from '@trezor/product-components'; import { spacings } from '@trezor/theme'; import { onCancel } from 'src/actions/suite/modalActions'; import { goto } from 'src/actions/suite/routerActions'; -import { Translation } from 'src/components/suite'; +import { AccountLabel, Translation, WalletLabeling } from 'src/components/suite'; +import { AccountTypeBadge } from 'src/components/suite/AccountTypeBadge'; import { useDispatch, useSelector } from 'src/hooks/suite'; +import { selectAccountLabels } from 'src/reducers/suite/metadataReducer'; interface WalletConnectProposalModalProps { eventId: number; } +interface AccountGroup extends GroupBase { + options: Account[]; + label: string; + device: TrezorDevice; + accountType: AccountType; + networkType: NetworkType; +} + export const WalletConnectProposalModal = ({ eventId }: WalletConnectProposalModalProps) => { const dispatch = useDispatch(); const pendingProposal = useSelector(selectPendingProposal); + const accounts = useSelector(selectAccounts); + const accountLabels = useSelector(selectAccountLabels); + const devices = useSelector(selectDevices); + const [selectedDefaultAccounts, setSelectedDefaultAccounts] = useState([]); const handleAccept = () => { - dispatch(sessionProposalApproveThunk({ eventId })); + dispatch(sessionProposalApproveThunk({ eventId, selectedDefaultAccounts })); dispatch(onCancel()); }; const handleReject = () => { dispatch(sessionProposalRejectThunk({ eventId })); dispatch(onCancel()); }; + const handleSelectAccount = (account: Account) => { + setSelectedDefaultAccounts(prev => { + const newAccounts = prev.filter(prevAccount => prevAccount.symbol !== account.symbol); + + return [...newAccounts, account]; + }); + }; + + const orderedAccounts = useMemo( + () => + [...accounts] + .filter(account => account.visible) + .map(account => ({ + ...account, + accountLabel: accountLabels[account.key], + })) + .sort((a, b) => { + if (a.deviceState !== b.deviceState) { + return a.deviceState.localeCompare(b.deviceState); + } + if (a.accountType !== b.accountType) { + // normal first + if (a.accountType === 'normal' && b.accountType !== 'normal') return -1; + if (a.accountType !== 'normal' && b.accountType === 'normal') return 1; + + return a.accountType.localeCompare(b.accountType); + } + + return a.index - b.index; + }), + [accounts, accountLabels], + ); + useEffect(() => { + const newDefaultAccounts = pendingProposal?.networks + .filter(network => network.status === 'active') + .map(network => orderedAccounts.find(account => account.symbol === network.symbol)) + .filter(Boolean) as Account[]; + setSelectedDefaultAccounts(newDefaultAccounts); + }, [orderedAccounts, pendingProposal?.networks]); + + const buildAccountOptionGroups = (network: PendingConnectionProposalNetwork) => { + const groups: AccountGroup[] = []; + orderedAccounts + .filter(account => account.symbol === network.symbol) + .forEach(account => { + const device = devices.find(d => d.state?.staticSessionId === account.deviceState); + if (!device) return; + const label = `${account.deviceState}-${account.accountType}`; + const group = groups.find(g => g.label === label); + if (group) { + group.options.push(account); + } else { + groups.push({ + label, + device, + accountType: account.accountType, + networkType: account.networkType, + options: [account], + }); + } + }); + + return groups; + }; if (!pendingProposal) return null; @@ -103,12 +200,59 @@ export const WalletConnectProposalModal = ({ eventId }: WalletConnectProposalMod size={24} /> )} - - {network.name} - {network.required && ( - * - )} - + {status === 'active' && + network.namespaceId.startsWith('solana') ? ( +