diff --git a/app/scripts/controllers/bridge-status/bridge-status-controller.test.ts b/app/scripts/controllers/bridge-status/bridge-status-controller.test.ts index aab698551191..2b12f9d0794f 100644 --- a/app/scripts/controllers/bridge-status/bridge-status-controller.test.ts +++ b/app/scripts/controllers/bridge-status/bridge-status-controller.test.ts @@ -20,7 +20,7 @@ const getMessengerMock = ({ } = {}) => ({ call: jest.fn((method: string) => { - if (method === 'AccountsController:getSelectedAccount') { + if (method === 'AccountsController:getSelectedMultichainAccount') { return { address: account }; } else if (method === 'NetworkController:findNetworkClientIdByChainId') { return 'networkClientId'; @@ -216,7 +216,7 @@ describe('BridgeStatusController', () => { let getSelectedAccountCalledTimes = 0; const messengerMock = { call: jest.fn((method: string) => { - if (method === 'AccountsController:getSelectedAccount') { + if (method === 'AccountsController:getSelectedMultichainAccount') { let account; if (getSelectedAccountCalledTimes === 0) { account = '0xaccount1'; @@ -399,7 +399,7 @@ describe('BridgeStatusController', () => { jest.useFakeTimers(); const messengerMock = { call: jest.fn((method: string) => { - if (method === 'AccountsController:getSelectedAccount') { + if (method === 'AccountsController:getSelectedMultichainAccount') { return { address: '0xaccount1' }; } else if ( method === 'NetworkController:findNetworkClientIdByChainId' diff --git a/app/scripts/controllers/bridge-status/bridge-status-controller.ts b/app/scripts/controllers/bridge-status/bridge-status-controller.ts index 4251b7dd3328..cc364e8de318 100644 --- a/app/scripts/controllers/bridge-status/bridge-status-controller.ts +++ b/app/scripts/controllers/bridge-status/bridge-status-controller.ts @@ -157,7 +157,7 @@ export default class BridgeStatusController extends StaticIntervalPollingControl targetContractAddress, } = startPollingForBridgeTxStatusArgs; const { bridgeStatusState } = this.state; - const { address: account } = this.#getSelectedAccount(); + const accountAddress = this.#getMultichainSelectedAccountAddress(); // Write all non-status fields to state so we can reference the quote in Activity list without the Bridge API // We know it's in progress but not the exact status yet @@ -176,7 +176,7 @@ export default class BridgeStatusController extends StaticIntervalPollingControl }, initialDestAssetBalance, targetContractAddress, - account, + account: accountAddress, status: { // We always have a PENDING status when we start polling for a tx, don't need the Bridge API for that // Also we know the bare minimum fields for status at this point in time @@ -210,8 +210,14 @@ export default class BridgeStatusController extends StaticIntervalPollingControl await this.#fetchBridgeTxStatus(pollingInput); }; - #getSelectedAccount() { - return this.messagingSystem.call('AccountsController:getSelectedAccount'); + // Returns an empty string if no account is selected, but this will never happen since + // the multichain selected account defaults to the EVM account + #getMultichainSelectedAccountAddress() { + return ( + this.messagingSystem.call( + 'AccountsController:getSelectedMultichainAccount', + )?.address ?? '' + ); } #fetchBridgeTxStatus = async ({ diff --git a/app/scripts/controllers/bridge-status/types.ts b/app/scripts/controllers/bridge-status/types.ts index 12c4952163d0..0778f46e07e6 100644 --- a/app/scripts/controllers/bridge-status/types.ts +++ b/app/scripts/controllers/bridge-status/types.ts @@ -8,7 +8,7 @@ import { NetworkControllerGetNetworkClientByIdAction, NetworkControllerGetStateAction, } from '@metamask/network-controller'; -import { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; +import { AccountsControllerGetSelectedMultichainAccountAction } from '@metamask/accounts-controller'; import { TransactionControllerGetStateAction } from '@metamask/transaction-controller'; import { BridgeHistoryItem, @@ -63,7 +63,7 @@ type AllowedActions = | NetworkControllerFindNetworkClientIdByChainIdAction | NetworkControllerGetStateAction | NetworkControllerGetNetworkClientByIdAction - | AccountsControllerGetSelectedAccountAction + | AccountsControllerGetSelectedMultichainAccountAction | TransactionControllerGetStateAction; /** diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 45e15176c01c..57278eb6927e 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1826,7 +1826,7 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger.getRestricted({ name: BRIDGE_STATUS_CONTROLLER_NAME, allowedActions: [ - 'AccountsController:getSelectedAccount', + 'AccountsController:getSelectedMultichainAccount', 'NetworkController:getNetworkClientById', 'NetworkController:findNetworkClientIdByChainId', 'NetworkController:getState', diff --git a/ui/hooks/accounts/useMultichainWalletSnapClient.ts b/ui/hooks/accounts/useMultichainWalletSnapClient.ts index 1ace7a2fddf2..e8c231b9b9f5 100644 --- a/ui/hooks/accounts/useMultichainWalletSnapClient.ts +++ b/ui/hooks/accounts/useMultichainWalletSnapClient.ts @@ -41,6 +41,14 @@ export class MultichainWalletSnapSender implements Sender { }; } +export function useMultichainWalletSnapSender(snapId: SnapId) { + const client = useMemo(() => { + return new MultichainWalletSnapSender(snapId); + }, [snapId]); + + return client; +} + export class MultichainWalletSnapClient { readonly #client: KeyringClient; diff --git a/ui/hooks/bridge/useIsTxSubmittable.ts b/ui/hooks/bridge/useIsTxSubmittable.ts index 8302001c5bf8..32f3d98ed92e 100644 --- a/ui/hooks/bridge/useIsTxSubmittable.ts +++ b/ui/hooks/bridge/useIsTxSubmittable.ts @@ -3,43 +3,46 @@ import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../../../shared/constants/swaps import { getBridgeQuotes, getFromAmount, - getFromChain, getFromToken, getToChain, getValidationErrors, getToToken, } from '../../ducks/bridge/selectors'; +import { getMultichainCurrentChainId } from '../../selectors/multichain'; +import { useMultichainSelector } from '../useMultichainSelector'; +import { useIsMultichainSwap } from '../../pages/bridge/hooks/useIsMultichainSwap'; import useLatestBalance from './useLatestBalance'; export const useIsTxSubmittable = () => { const fromToken = useSelector(getFromToken); const toToken = useSelector(getToToken); - const fromChain = useSelector(getFromChain); + const fromChainId = useMultichainSelector(getMultichainCurrentChainId); const toChain = useSelector(getToChain); const fromAmount = useSelector(getFromAmount); const { activeQuote } = useSelector(getBridgeQuotes); + const isSwap = useIsMultichainSwap(); const { isInsufficientBalance, isInsufficientGasBalance, isInsufficientGasForQuote, } = useSelector(getValidationErrors); - const { balanceAmount } = useLatestBalance(fromToken, fromChain?.chainId); - const { balanceAmount: nativeAssetBalance } = useLatestBalance( - fromChain?.chainId + const balanceAmount = useLatestBalance(fromToken, fromChainId); + const nativeAssetBalance = useLatestBalance( + fromChainId ? SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ - fromChain.chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP + fromChainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP ] : null, - fromChain?.chainId, + fromChainId, ); return Boolean( fromToken && toToken && - fromChain && - toChain && + fromChainId && + (isSwap || toChain) && fromAmount && activeQuote && !isInsufficientBalance(balanceAmount) && diff --git a/ui/hooks/bridge/useLatestBalance.test.ts b/ui/hooks/bridge/useLatestBalance.test.ts index f255a3a10b9b..7b0b6b7befc1 100644 --- a/ui/hooks/bridge/useLatestBalance.test.ts +++ b/ui/hooks/bridge/useLatestBalance.test.ts @@ -57,7 +57,7 @@ describe('useLatestBalance', () => { ); await waitForNextUpdate(); - expect(result.current.balanceAmount).toStrictEqual(new BigNumber('1')); + expect(result.current).toStrictEqual(new BigNumber('1')); expect(mockGetBalance).toHaveBeenCalledTimes(1); expect(mockGetBalance).toHaveBeenCalledWith( @@ -76,7 +76,7 @@ describe('useLatestBalance', () => { ); await waitForNextUpdate(); - expect(result.current.balanceAmount).toStrictEqual(new BigNumber('15.39')); + expect(result.current).toStrictEqual(new BigNumber('15.39')); expect(mockFetchTokenBalance).toHaveBeenCalledTimes(1); expect(mockFetchTokenBalance).toHaveBeenCalledWith( diff --git a/ui/hooks/bridge/useLatestBalance.ts b/ui/hooks/bridge/useLatestBalance.ts index 3d60e106832e..55d0fb40130f 100644 --- a/ui/hooks/bridge/useLatestBalance.ts +++ b/ui/hooks/bridge/useLatestBalance.ts @@ -1,11 +1,16 @@ -import { useSelector } from 'react-redux'; import { type Hex, type CaipChainId, isCaipChainId } from '@metamask/utils'; -import { Numeric } from '../../../shared/modules/Numeric'; -import { getCurrentChainId } from '../../../shared/modules/selectors/networks'; +import { useMemo } from 'react'; import { getSelectedInternalAccount } from '../../selectors'; import { calcLatestSrcBalance } from '../../../shared/modules/bridge-utils/balance'; import { useAsyncResult } from '../useAsyncResult'; +import { Numeric } from '../../../shared/modules/Numeric'; import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils'; +import { useMultichainSelector } from '../useMultichainSelector'; +import { + getMultichainBalances, + getMultichainCurrentChainId, +} from '../../selectors/multichain'; +import { MultichainNetworks } from '../../../shared/constants/multichain/networks'; /** * Custom hook to fetch and format the latest balance of a given token or native asset. @@ -19,15 +24,21 @@ const useLatestBalance = ( address: string; decimals: number; symbol: string; + string?: string; } | null, chainId?: Hex | CaipChainId, ) => { - const { address: selectedAddress } = useSelector(getSelectedInternalAccount); - const currentChainId = useSelector(getCurrentChainId); + const { address: selectedAddress, id } = useMultichainSelector( + getSelectedInternalAccount, + ); + const currentChainId = useMultichainSelector(getMultichainCurrentChainId); + + const nonEvmBalancesByAccountId = useMultichainSelector( + getMultichainBalances, + ); + const nonEvmBalances = nonEvmBalancesByAccountId[id]; - const { value: latestBalance } = useAsyncResult< - Numeric | undefined - >(async () => { + const value = useAsyncResult(async () => { if ( token?.address && // TODO check whether chainId is EVM when MultichainNetworkController is integrated @@ -42,8 +53,29 @@ const useLatestBalance = ( chainId, ); } + + // No need to fetch the balance for non-EVM tokens, use the balance provided by the + // multichain balances controller + if ( + isCaipChainId(chainId) && + chainId === MultichainNetworks.SOLANA && + token?.decimals + ) { + return Numeric.from( + nonEvmBalances?.[token.address]?.amount ?? token?.string, + 10, + ).shiftedBy(-1 * token.decimals); + } + return undefined; - }, [currentChainId, token?.address, selectedAddress]); + }, [ + chainId, + currentChainId, + token, + selectedAddress, + global.ethereumProvider, + nonEvmBalances, + ]); if (token && !token.decimals) { throw new Error( @@ -51,14 +83,13 @@ const useLatestBalance = ( ); } - const tokenDecimals = token?.decimals ? Number(token.decimals) : 1; - - return { - balanceAmount: - token && latestBalance - ? calcTokenAmount(latestBalance.toString(), tokenDecimals) + return useMemo( + () => + value?.value + ? calcTokenAmount(value.value.toString(), token?.decimals) : undefined, - }; + [value.value, token?.decimals], + ); }; export default useLatestBalance; diff --git a/ui/hooks/bridge/useQuoteFetchEvents.ts b/ui/hooks/bridge/useQuoteFetchEvents.ts index a272430c3f2a..d890d5b30e60 100644 --- a/ui/hooks/bridge/useQuoteFetchEvents.ts +++ b/ui/hooks/bridge/useQuoteFetchEvents.ts @@ -44,8 +44,8 @@ export const useQuoteFetchEvents = () => { const fromToken = useSelector(getFromToken); const fromChain = useSelector(getFromChain); - const { balanceAmount } = useLatestBalance(fromToken, fromChain?.chainId); - const { balanceAmount: nativeAssetBalance } = useLatestBalance( + const balanceAmount = useLatestBalance(fromToken, fromChain?.chainId); + const nativeAssetBalance = useLatestBalance( fromChain?.chainId ? SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ fromChain.chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP diff --git a/ui/pages/bridge/hooks/useHandleTx.ts b/ui/pages/bridge/hooks/useHandleTx.ts index 59b6c7ee9c89..90f5fdcaa10c 100644 --- a/ui/pages/bridge/hooks/useHandleTx.ts +++ b/ui/pages/bridge/hooks/useHandleTx.ts @@ -1,8 +1,14 @@ import { - TransactionMeta, + type TransactionMeta, + type TransactionParams, + TransactionStatus, TransactionType, } from '@metamask/transaction-controller'; import { useDispatch, useSelector } from 'react-redux'; +import { KeyringRpcMethod } from '@metamask/keyring-api'; +import { useEffect } from 'react'; +import { Hex } from '@metamask/utils'; +import { useHistory } from 'react-router-dom'; import { forceUpdateMetamaskState, addTransaction, @@ -14,10 +20,26 @@ import { getTxGasEstimates, } from '../../../ducks/bridge/utils'; import { getGasFeeEstimates } from '../../../ducks/metamask/metamask'; -import { checkNetworkAndAccountSupports1559 } from '../../../selectors'; +import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; import type { ChainId } from '../../../../shared/types/bridge'; import { decimalToPrefixedHex } from '../../../../shared/modules/conversion.utils'; import { getIsSmartTransaction } from '../../../../shared/modules/selectors'; +import { + getMultichainCurrentChainId, + getMultichainIsSolana, +} from '../../../selectors/multichain'; +import { SOLANA_WALLET_SNAP_ID } from '../../../../shared/lib/accounts/solana-wallet-snap'; +import { useMultichainWalletSnapSender } from '../../../hooks/accounts/useMultichainWalletSnapClient'; +import { + checkNetworkAndAccountSupports1559, + getMemoizedUnapprovedTemplatedConfirmations, + getMemoizedUnapprovedConfirmations, + getSelectedInternalAccount, +} from '../../../selectors'; +import { + CONFIRM_TRANSACTION_ROUTE, + CONFIRMATION_V_NEXT_ROUTE, +} from '../../../helpers/constants/routes'; export default function useHandleTx() { const dispatch = useDispatch(); @@ -27,7 +49,7 @@ export default function useHandleTx() { const networkGasFeeEstimates = useSelector(getGasFeeEstimates); const shouldUseSmartTransaction = useSelector(getIsSmartTransaction); - const handleTx = async ({ + const handleEvmTx = async ({ txType, txParams, fieldsToAddToTxMeta, @@ -88,5 +110,101 @@ export default function useHandleTx() { return txMeta; }; - return { handleTx }; + const selectedAccount = useSelector(getSelectedInternalAccount); + const currentChainId = useSelector(getMultichainCurrentChainId); + const snapSender = useMultichainWalletSnapSender(SOLANA_WALLET_SNAP_ID); + const history = useHistory(); + + // Find unapproved confirmations which the snap has initiated + const unapprovedTemplatedConfirmations = useSelector( + getMemoizedUnapprovedTemplatedConfirmations, + ); + const unapprovedConfirmations = useSelector( + getMemoizedUnapprovedConfirmations, + ); + // Redirect to the confirmation page if an unapproved confirmation exists + useEffect(() => { + const templatedSnapApproval = unapprovedTemplatedConfirmations.find( + (approval) => approval.origin === SOLANA_WALLET_SNAP_ID, + ); + const snapApproval = unapprovedConfirmations.find( + (approval) => approval.origin === SOLANA_WALLET_SNAP_ID, + ); + if (templatedSnapApproval) { + history.push(`${CONFIRMATION_V_NEXT_ROUTE}/${templatedSnapApproval.id}`); + } else if (snapApproval) { + history.push(`${CONFIRM_TRANSACTION_ROUTE}/${snapApproval.id}`); + } + }, [history, unapprovedTemplatedConfirmations, unapprovedConfirmations]); + + const handleSolanaTx = async ({ + txType, + txParams, + fieldsToAddToTxMeta, + }: { + txType: TransactionType.bridge; + txParams: string; + fieldsToAddToTxMeta: Omit, 'status'>; + }): Promise => { + // Submit a signing request to the snap + (await snapSender.send({ + id: crypto.randomUUID(), + jsonrpc: '2.0', + method: KeyringRpcMethod.SubmitRequest, + params: { + request: { + params: { + account: { address: selectedAccount.address }, + transaction: txParams, + scope: currentChainId, + }, + method: 'signAndSendTransaction', + }, + id: crypto.randomUUID(), + account: selectedAccount.id, + scope: currentChainId, + }, + })) as string; + + return { + ...fieldsToAddToTxMeta, + id: crypto.randomUUID(), + chainId: currentChainId as Hex, + networkClientId: selectedAccount.id, + time: Date.now(), + txParams: { data: txParams } as TransactionParams, + type: txType, + status: TransactionStatus.submitted, + }; + }; + + const isSolana = useMultichainSelector(getMultichainIsSolana); + + return { + handleTx: async ({ + txType, + txParams, + fieldsToAddToTxMeta, + }: { + txType: TransactionType.bridgeApproval | TransactionType.bridge; + txParams: { + chainId: ChainId; + to: string; + from: string; + value: string; + data: string; + gasLimit: number | null; + }; + fieldsToAddToTxMeta: Omit, 'status'>; // We don't add status, so omit it to fix the type error + }) => { + if ( + isSolana && + txType === TransactionType.bridge && + typeof txParams === 'string' + ) { + return handleSolanaTx({ txType, txParams, fieldsToAddToTxMeta }); + } + return handleEvmTx({ txType, txParams, fieldsToAddToTxMeta }); + }, + }; } diff --git a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts index 1a1982078fbd..aa02cebbb2a5 100644 --- a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts +++ b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts @@ -31,6 +31,8 @@ import { MetricsBackgroundState, StatusTypes, } from '../../../../shared/types/bridge-status'; +import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; +import { getMultichainIsEvm } from '../../../selectors/multichain'; import useAddToken from './useAddToken'; import useHandleApprovalTx, { APPROVAL_TX_ERROR, @@ -83,6 +85,7 @@ export default function useSubmitBridgeTransaction() { const { slippage } = useSelector(getQuoteRequest); const selectedAddress = useSelector(getSelectedAddress); const trackCrossChainSwapsEvent = useCrossChainSwapsEventTracker(); + const isEvm = useMultichainSelector(getMultichainIsEvm); const submitBridgeTransaction = async ( quoteResponse: QuoteResponse & QuoteMetadata, @@ -210,15 +213,17 @@ export default function useSubmitBridgeTransaction() { startTime: bridgeTxMeta.time, }), ); - - // Add tokens if not the native gas token - if (quoteResponse.quote.srcAsset.address !== zeroAddress()) { - addSourceToken(quoteResponse); - } - if (quoteResponse.quote.destAsset.address !== zeroAddress()) { - await addDestToken(quoteResponse); + // Only add tokens if the source chain is an EVM chain bc non-evm tokens + // are detected by the multichain asset controllers + if (isEvm) { + // Add tokens if not the native gas token + if (quoteResponse.quote.srcAsset.address !== zeroAddress()) { + addSourceToken(quoteResponse); + } + if (quoteResponse.quote.destAsset.address !== zeroAddress()) { + await addDestToken(quoteResponse); + } } - // Route user to activity tab on Home page await dispatch(setDefaultHomeActiveTabName('activity')); history.push({ diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx index c68c0a81e73d..890828138fd0 100644 --- a/ui/pages/bridge/prepare/bridge-cta-button.tsx +++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx @@ -75,8 +75,8 @@ export const BridgeCTAButton = ({ const wasTxDeclined = useSelector(getWasTxDeclined); - const { balanceAmount } = useLatestBalance(fromToken, fromChain?.chainId); - const { balanceAmount: nativeAssetBalance } = useLatestBalance( + const balanceAmount = useLatestBalance(fromToken, fromChain?.chainId); + const nativeAssetBalance = useLatestBalance( fromChain?.chainId ? SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ fromChain.chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP diff --git a/ui/pages/bridge/prepare/bridge-input-group.tsx b/ui/pages/bridge/prepare/bridge-input-group.tsx index 8f85e7e3933c..7a98b7a44500 100644 --- a/ui/pages/bridge/prepare/bridge-input-group.tsx +++ b/ui/pages/bridge/prepare/bridge-input-group.tsx @@ -87,7 +87,7 @@ export const BridgeInputGroup = ({ const locale = useSelector(getIntlLocale); const selectedChainId = networkProps?.network?.chainId; - const { balanceAmount } = useLatestBalance(token, selectedChainId); + const balanceAmount = useLatestBalance(token, selectedChainId); const [, handleCopy] = useCopyToClipboard(MINUTE) as [ boolean, diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index a0aa88e88a1d..3ecf81ed59be 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -188,17 +188,14 @@ const PrepareBridgePage = () => { const { quotesRefreshCount } = useSelector(getBridgeQuotes); const { openBuyCryptoInPdapp } = useRamps(); - const { balanceAmount: nativeAssetBalance } = useLatestBalance( + const nativeAssetBalance = useLatestBalance( SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ fromChain?.chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP ], fromChain?.chainId, ); - const { balanceAmount: srcTokenBalance } = useLatestBalance( - fromToken, - fromChain?.chainId, - ); + const srcTokenBalance = useLatestBalance(fromToken, fromChain?.chainId); const { filteredTokenListGenerator: toTokenListGenerator,