Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
96249d1
feat: add universal multichain transaction confirmation POC
matthewwalsh0 May 20, 2026
c0aab9c
feat: temporarily disable showUniversalTransactionConfirmation hook f…
matthewwalsh0 May 20, 2026
98bede1
feat: enable multichain-transactions-controller yalc link
matthewwalsh0 May 20, 2026
4c555ef
feat: enable snaps-rpc-methods yalc link and restore hook
matthewwalsh0 May 20, 2026
4edb64e
feat: enable snaps-controllers yalc link
matthewwalsh0 May 20, 2026
df7b1a6
feat: enable snaps-utils yalc link
matthewwalsh0 May 20, 2026
828c144
feat: enable snaps-sdk yalc link
matthewwalsh0 May 20, 2026
fd7afdb
feat: enable local Solana snap dev and update keyring permissions for…
matthewwalsh0 May 21, 2026
9bd70d6
feat(confirmations): universal multichain confirmation via standard C…
matthewwalsh0 May 21, 2026
a991b68
feat(confirmations): redesign universal multichain rows to match EVM
matthewwalsh0 May 21, 2026
7bdf4a9
feat(confirmations): remove redundant Send header from universal conf…
matthewwalsh0 May 21, 2026
55600c6
feat(universal-tx): advanced details toggle, fee row polish, and Send…
matthewwalsh0 May 21, 2026
c4cc0d1
docs(confirmations): document universal Snap confirmation POC
matthewwalsh0 Jun 14, 2026
962393e
docs(confirmations): include Snaps Platform API layer in POC architec…
matthewwalsh0 Jun 14, 2026
52900d4
feat(confirmations): derive universal transaction metadata from asset…
matthewwalsh0 Jun 14, 2026
ba5fa73
docs(confirmations): clarify balance centralization boundary
matthewwalsh0 Jun 14, 2026
1190f62
feat(confirmations): add universal transaction balance alerts
matthewwalsh0 Jun 14, 2026
12ee1a5
feat(confirmations): query non-EVM balances via accounts API
matthewwalsh0 Jun 14, 2026
8437654
feat(confirmations): wait for non-EVM account balances
matthewwalsh0 Jun 14, 2026
904e634
feat(confirmations): support custom universal transaction data
matthewwalsh0 Jun 14, 2026
e0536a6
refactor(confirmations): move universal transaction rows
matthewwalsh0 Jun 14, 2026
4777999
docs(confirmations): remove universal confirmation POC doc
matthewwalsh0 Jun 14, 2026
9a2953a
chore: remove sisyphus artifacts
matthewwalsh0 Jun 14, 2026
448b1d1
chore: remove unrelated extension diffs
matthewwalsh0 Jun 14, 2026
64e96ae
chore: remove unrelated keyring permission diff
matthewwalsh0 Jun 14, 2026
cfcb318
chore: remove lavamoat policy churn
matthewwalsh0 Jun 14, 2026
61a9027
chore: restore lavamoat policies to branch base
matthewwalsh0 Jun 14, 2026
a732b11
refactor(confirmations): derive universal approval ids from snap ids
matthewwalsh0 Jun 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions app/scripts/constants/snaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,6 @@ export const PREINSTALLED_SNAPS_URLS = [
// @ts-expect-error TS1470: 'import.meta' is not allowed in CommonJS
import.meta.url,
),
new URL(
'@metamask/solana-wallet-snap/dist/preinstalled-snap.json',
// @ts-expect-error TS1470: 'import.meta' is not allowed in CommonJS
import.meta.url,
),
new URL(
'@metamask/tron-wallet-snap/dist/preinstalled-snap.json',
// @ts-expect-error TS1470: 'import.meta' is not allowed in CommonJS
Expand Down
104 changes: 103 additions & 1 deletion app/scripts/controllers/permissions/snaps/specifications.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import {
buildSnapEndowmentSpecifications,
buildSnapRestrictedMethodSpecifications,
type ConfirmTransactionParams,
RestrictedMethodMessenger,
type UpdateConfirmTransactionParams,
} from '@metamask/snaps-rpc-methods';
import { ControllerGetStateAction } from '@metamask/base-controller';
import { ApprovalType } from '@metamask/controller-utils';
import { CurrencyRateController } from '@metamask/assets-controllers';
import type { AssetsControllerGetStateAction } from '@metamask/assets-controller';
import type { MultichainTransactionsControllerActions } from '@metamask/multichain-transactions-controller';
import type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller';
import { rpcErrors } from '@metamask/rpc-errors';
import {
SnapControllerClearSnapStateAction,
SnapControllerGetSnapAction,
Expand All @@ -17,6 +22,7 @@ import {
SnapInterfaceControllerGetInterfaceAction,
SnapInterfaceControllerSetInterfaceDisplayedAction,
} from '@metamask/snaps-controllers';
import { nanoid } from 'nanoid';
import {
KeyringControllerGetKeyringsByTypeAction,
KeyringControllerWithKeyringAction,
Expand All @@ -32,6 +38,10 @@ import {
ExcludedSnapEndowments,
ExcludedSnapPermissions,
} from '../../../../../shared/constants/snaps/permissions';
import {
UNIVERSAL_TRANSACTION_APPROVAL_TYPE,
USE_UNIVERSAL_MULTICHAIN_CONFIRMATION,
} from '../../../../../shared/constants/confirmations';
import { PreferencesControllerGetStateAction } from '../../preferences-controller';
import { KeyringType } from '../../../../../shared/constants/keyring';
import { AppStateControllerGetUnlockPromiseAction } from '../../app-state-controller-method-action-types';
Expand All @@ -43,6 +53,12 @@ import {
} from '../../../../../shared/lib/assets-unify-state/remote-feature-flag';
import { getIsAssetsUnifiedStateIncludedInBuild } from '../../../../../shared/lib/environment';

function getUniversalTransactionConfirmationKey(snapId: string, id: string) {
const encodedId = encodeURIComponent(`${snapId}:${id}`).replace(/%/gu, '.');

return `snap-confirm-tx:${encodedId}`;
}

export type SnapPermissionSpecificationsActions =
| AppStateControllerGetUnlockPromiseAction
| SnapControllerClearSnapStateAction
Expand All @@ -66,7 +82,8 @@ export type SnapPermissionSpecificationsActions =
| SnapInterfaceControllerSetInterfaceDisplayedAction
| TestOrigin
| SnapControllerUpdateSnapStateAction
| ApprovalControllerAddRequestAction;
| ApprovalControllerAddRequestAction
| MultichainTransactionsControllerActions;

/**
* Get the permission specifications for Snaps.
Expand Down Expand Up @@ -180,6 +197,91 @@ export function getSnapPermissionSpecifications(
*/
getClientCryptography: () => ({}),

showUniversalTransactionConfirmation: async (
snapId: string,
params: ConfirmTransactionParams,
) => {
if (!USE_UNIVERSAL_MULTICHAIN_CONFIRMATION) {
throw rpcErrors.methodNotSupported();
}

const approvalId = params.id
? getUniversalTransactionConfirmationKey(snapId, params.id)
: nanoid();

messenger.call(
'MultichainTransactionsController:addPendingTransaction',
{
approvalId,
chainId: params.chainId,
accountId: params.accountId,
to: params.to,
amount: params.amount,
assetId: params.assetId,
custom: params.custom,
fee: params.fee,
origin: snapId,
createdAt: Date.now(),
},
);

const addAndShowApprovalRequest = async () =>
messenger.call(
'ApprovalController:addRequest',
{
id: approvalId,
origin: snapId,
type: UNIVERSAL_TRANSACTION_APPROVAL_TYPE as ApprovalType,
requestData: {
approvalId,
chainId: params.chainId,
txParams: {
to: params.to,
value: params.amount,
},
},
},
true,
);

try {
await addAndShowApprovalRequest();
return true;
} catch (_err) {
return false;
} finally {
messenger.call(
'MultichainTransactionsController:removePendingTransaction',
approvalId,
);
}
},

updateUniversalTransactionConfirmation: async (
snapId: string,
params: UpdateConfirmTransactionParams,
) => {
if (!USE_UNIVERSAL_MULTICHAIN_CONFIRMATION) {
throw rpcErrors.methodNotSupported();
}

const approvalId = getUniversalTransactionConfirmationKey(
snapId,
params.id,
);

const patch = {
...(params.custom === undefined ? {} : { custom: params.custom }),
...(params.fee === undefined ? {} : { fee: params.fee }),
};

messenger.call(
'MultichainTransactionsController:updatePendingTransaction',
approvalId,
patch,
);
},

getSnapKeyring: async () => {
// TODO: Use `withKeyring` instead.
const [snapKeyring] = messenger.call(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
ApprovalControllerAcceptRequestAction,
ApprovalControllerRejectRequestAction,
} from '@metamask/approval-controller';
import type { MultichainTransactionsControllerActions } from '@metamask/multichain-transactions-controller';
import type { GetSubjectMetadata } from '@metamask/permission-controller';
import { AccountsControllerListAccountsAction } from '@metamask/accounts-controller';
import {
Expand Down Expand Up @@ -68,6 +69,7 @@ type AllowedInitializationActions =
| AccountsControllerListAccountsAction
| MultichainRoutingServiceGetSupportedAccountsAction
| MultichainRoutingServiceIsSupportedScopeAction
| MultichainTransactionsControllerActions
| NetworkControllerFindNetworkClientIdByChainIdAction
| SnapPermissionSpecificationsActions
| ApprovalControllerAddRequestAction;
Expand Down Expand Up @@ -107,6 +109,9 @@ export function getPermissionControllerInitMessenger(
'KeyringController:addNewKeyring',
'MultichainRoutingService:isSupportedScope',
'MultichainRoutingService:getSupportedAccounts',
'MultichainTransactionsController:addPendingTransaction',
'MultichainTransactionsController:removePendingTransaction',
'MultichainTransactionsController:updatePendingTransaction',
'NetworkController:findNetworkClientIdByChainId',
'PhishingController:maybeUpdateState',
'PhishingController:testOrigin',
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@
"@metamask/multichain-api-client": "^0.10.1",
"@metamask/multichain-api-middleware": "^3.1.0",
"@metamask/multichain-network-controller": "^3.1.0",
"@metamask/multichain-transactions-controller": "^7.1.0",
"@metamask/multichain-transactions-controller": "file:.yalc/@metamask/multichain-transactions-controller",
"@metamask/name-controller": "^9.1.0",
"@metamask/network-controller": "^32.0.0",
"@metamask/network-enablement-controller": "^5.1.0",
Expand Down Expand Up @@ -424,11 +424,11 @@
"@metamask/shield-controller": "^5.0.1",
"@metamask/signature-controller": "^39.2.0",
"@metamask/smart-transactions-controller": "^24.1.0",
"@metamask/snaps-controllers": "^20.0.5",
"@metamask/snaps-controllers": "file:.yalc/@metamask/snaps-controllers",
"@metamask/snaps-execution-environments": "^11.0.2",
"@metamask/snaps-rpc-methods": "^16.0.0",
"@metamask/snaps-sdk": "^11.1.1",
"@metamask/snaps-utils": "^12.2.0",
"@metamask/snaps-rpc-methods": "file:.yalc/@metamask/snaps-rpc-methods",
"@metamask/snaps-sdk": "file:.yalc/@metamask/snaps-sdk",
"@metamask/snaps-utils": "file:.yalc/@metamask/snaps-utils",
"@metamask/solana-wallet-snap": "^2.8.0",
"@metamask/solana-wallet-standard": "^1.0.0",
"@metamask/storage-service": "^1.0.0",
Expand Down
13 changes: 13 additions & 0 deletions shared/constants/confirmations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* POC flag. When true, non-EVM send confirmations triggered via
* `snap_confirmTransaction` render the new universal multichain
* confirmation page instead of the legacy snap-templated dialog.
*/
export const USE_UNIVERSAL_MULTICHAIN_CONFIRMATION = true;

/**
* Approval type used by `snap_confirmTransaction` flows. Cast to
* {@link ApprovalType} at call sites until the canonical enum in
* `@metamask/controller-utils` is updated.
*/
export const UNIVERSAL_TRANSACTION_APPROVAL_TYPE = 'universalTransaction';
10 changes: 8 additions & 2 deletions shared/lib/accounts/solana-wallet-snap.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { SnapId } from '@metamask/snaps-sdk';
// This dependency is still installed as part of the `package.json`, however
// the Snap is being pre-installed only for Flask build (for the moment).
import SolanaWalletSnap from '@metamask/solana-wallet-snap/dist/preinstalled-snap.json';

// export const SOLANA_WALLET_SNAP_ID: SnapId = SolanaWalletSnap.snapId as SnapId;
export const SOLANA_WALLET_SNAP_ID: SnapId =
'npm:@metamask/solana-wallet-snap' as SnapId;
export const SOLANA_WALLET_NAME: string = 'Solana';
'local:http://localhost:8080' as SnapId;

export const SOLANA_WALLET_NAME: string =
SolanaWalletSnap.manifest.proposedName;
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { useEnableShieldCoverageChecks } from '../../../hooks/transactions/useEn
import { useTransactionConfirm } from '../../../hooks/transactions/useTransactionConfirm';
import { useConfirmActions } from '../../../hooks/useConfirmActions';
import { useDappSwapActions } from '../../../hooks/transactions/dapp-swap-comparison/useDappSwapActions';
import { useUniversalTransactionBalanceAlert } from '../../../hooks/alerts/transactions/useUniversalTransactionBalanceAlert';
import { useOriginThrottling } from '../../../hooks/useOriginThrottling';
import {
isAddEthereumChainType,
Expand Down Expand Up @@ -245,6 +246,8 @@ const Footer = () => {
const currentConfirmationId = currentConfirmation?.id;
const t = useI18nContext();
const { isGaslessLoading } = useIsGaslessLoading();
const { isLoading: isUniversalTransactionBalanceLoading } =
useUniversalTransactionBalanceAlert();

const { from: fromAddress } = getConfirmationSender(currentConfirmation);
const { shouldThrottleOrigin } = useOriginThrottling();
Expand Down Expand Up @@ -304,7 +307,9 @@ const Footer = () => {
]);

const isConfirmDisabled =
(!isScrollToBottomCompleted && !isSignature) || isGaslessLoading;
(!isScrollToBottomCompleted && !isSignature) ||
isGaslessLoading ||
isUniversalTransactionBalanceLoading;

const shouldShowReconnectButton =
shouldRunHardwareWalletPreflight &&
Expand Down
10 changes: 8 additions & 2 deletions ui/pages/confirmations/components/confirm/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
} from '@metamask/transaction-controller';
import React from 'react';
import { ORIGIN_METAMASK } from '../../../../../../shared/constants/app';
import { UNIVERSAL_TRANSACTION_APPROVAL_TYPE } from '../../../../../../shared/constants/confirmations';
import { Box, Text } from '../../../../../components/component-library';
import { PreferredAvatar } from '../../../../../components/app/preferred-avatar';
import {
Expand Down Expand Up @@ -90,10 +91,15 @@ const Header = () => {
// back button if it's a wallet initiated confirmation. The default header is
// the original header for the redesigns and includes the sender and recipient
// addresses as well.
const isUniversalTransaction =
(currentConfirmation?.type as string | undefined) ===
UNIVERSAL_TRANSACTION_APPROVAL_TYPE;
const isConfirmationWithNewHeader =
currentConfirmation?.type &&
CONFIRMATIONS_WITH_ALT_HEADER.includes(currentConfirmation.type);
isUniversalTransaction ||
(currentConfirmation?.type &&
CONFIRMATIONS_WITH_ALT_HEADER.includes(currentConfirmation.type));
const isWalletInitiated =
isUniversalTransaction ||
(currentConfirmation as TransactionMeta)?.origin === ORIGIN_METAMASK;

const isSimpleHeader =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import React, { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { MetaMetricsEventLocation } from '../../../../../../shared/constants/metametrics';
import { UNIVERSAL_TRANSACTION_APPROVAL_TYPE } from '../../../../../../shared/constants/confirmations';
import {
Box,
ButtonIcon,
Expand Down Expand Up @@ -35,11 +36,23 @@ export const WalletInitiatedHeader = () => {
const { currentConfirmation } = useConfirmContext<TransactionMeta>();
const navigate = useNavigate();

const isUniversalTransaction =
currentConfirmation?.type === UNIVERSAL_TRANSACTION_APPROVAL_TYPE;

const isSendTransaction =
currentConfirmation?.type &&
SEND_TRANSACTION_TYPES.includes(currentConfirmation.type);
isUniversalTransaction ||
(currentConfirmation?.type &&
SEND_TRANSACTION_TYPES.includes(currentConfirmation.type));

const handleBackButtonClick = useCallback(() => {
if (isUniversalTransaction) {
onCancel({
location: MetaMetricsEventLocation.Confirmation,
navigateBackForSend: true,
});
return;
}

if (
currentConfirmation.type === TransactionType.shieldSubscriptionApprove
) {
Expand Down Expand Up @@ -74,7 +87,7 @@ export const WalletInitiatedHeader = () => {
navigateBackForSend: true,
});
}
}, [currentConfirmation, navigate, onCancel]);
}, [currentConfirmation, isUniversalTransaction, navigate, onCancel]);

const getHeaderTitle = () => {
if (isSendTransaction) {
Expand Down
4 changes: 4 additions & 0 deletions ui/pages/confirmations/components/confirm/info/info.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TransactionType } from '@metamask/transaction-controller';
import { ApprovalType } from '@metamask/controller-utils';
import React, { useMemo } from 'react';
import { UNIVERSAL_TRANSACTION_APPROVAL_TYPE } from '../../../../../../shared/constants/confirmations';
import { useEnabledAdvancedPermissions } from '../../../../../hooks/gator-permissions/useEnabledAdvancedPermissions';
import { useTrustSignalMetrics } from '../../../../trust-signals/hooks/useTrustSignalMetrics';
import { useConfirmContext } from '../../../context/confirm';
Expand Down Expand Up @@ -29,6 +30,7 @@ import TokenTransferInfo from './token-transfer/token-transfer';
import TypedSignV1Info from './typed-sign-v1/typed-sign-v1';
import TypedSignInfo from './typed-sign/typed-sign';
import TypedSignPermissionInfo from './typed-sign/typed-sign-permission';
import UniversalTransactionInfo from './universal-transaction/universal-transaction-info';

// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
// eslint-disable-next-line @typescript-eslint/naming-convention
Expand Down Expand Up @@ -159,6 +161,8 @@ const Info = () => {

[ApprovalType.AddEthereumChain]: () => AddEthereumChain,

[UNIVERSAL_TRANSACTION_APPROVAL_TYPE]: () => UniversalTransactionInfo,

[TransactionType.musdClaim]: () => MusdClaimInfo,
[TransactionType.musdConversion]: () => MusdConversionInfo,
[TransactionType.perpsDeposit]: () => PerpsDepositInfo,
Expand Down
Loading
Loading