Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions packages/solana-wallet-snap/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Accept non-URL request origins instead of throwing while rendering confirmations, so requests relayed over WalletConnect (whose origin is a per-session channel id) can be confirmed
- Show the dapp URL reported in `originMetadata` in the "Request from" row of the transaction, sign-message, and sign-in confirmations, marked "Not verified", and hide the row entirely when there is nothing verifiable or self-reported to show (previously an unverifiable origin was displayed as if verified, or threw)
- Stop sending unverifiable origins to the security alerts API: a WalletConnect channel id or a self-reported dapp URL can flip a Blockaid verdict, so such requests are now reported as wallet-initiated
- Only run the sign-in (SIWS) domain check against a verifiable origin, instead of comparing the requested domain to a self-reported one
- Migrate `trackError` and `withCatchAndThrowSnapError` to `@metamask/snap-networks-utils` `createSnapErrorHandling`, and add `getSnapProvider` for Snap RPC access
- Extract Snap-owned assets domain logic into `SnapAssetsAdapter`; `AssetsService` is a thin facade that delegates metadata, market data, fetch, persist, and account asset reads through the adapter (no Core routing yet). ([#121](https://github.com/MetaMask/internal-snaps/pull/121))
- Align `AssetsService` read API with `snap-networks-utils` / AssetsController shapes by adding `getAccountAssetByID`, `getAccountAssetsByIDs`, `getAccountAssetsByScope`, and `getAccountAssets`, and routing Keyring and Send through them (still Snap-owned storage). ([#120](https://github.com/MetaMask/internal-snaps/pull/120))
Expand Down
6 changes: 6 additions & 0 deletions packages/solana-wallet-snap/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@
"confirmation.origin.tooltip": {
"message": "This is the site asking for your confirmation."
},
"confirmation.origin.unverified": {
"message": "Not verified"
},
"confirmation.origin.unverified.tooltip": {
"message": "This site is reported by the app that sent the request. MetaMask can't verify it."
},
"confirmation.simulationErrorTitle": {
"message": "This transaction was reverted during simulation."
},
Expand Down
2 changes: 2 additions & 0 deletions packages/solana-wallet-snap/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
"confirmation.feeError": "Unable to estimate fee",
"confirmation.origin": "Request from",
"confirmation.origin.tooltip": "This is the site asking for your confirmation.",
"confirmation.origin.unverified": "Not verified",
"confirmation.origin.unverified.tooltip": "This site is reported by the app that sent the request. MetaMask can't verify it.",
"confirmation.simulationErrorTitle": "This transaction was reverted during simulation.",
"confirmation.simulationErrorSubtitle": "{reason}",
"confirmation.validationErrorTitle": "This is a deceptive request",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export class ConfirmationHandler {
scope,
method,
origin: request.origin,
originMetadata: request.originMetadata ?? null,
transaction: base64EncodedTransaction,
account,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,49 @@ describe('TransactionScan', () => {
});
});

it.each([
['a WalletConnect channel id', '4f3a1b2c-0000-4000-8000-000000000000'],
['the MetaMask origin', 'metamask'],
])('does not forward %s to the scan', async (_, origin) => {
const scanTransactions = jest
.spyOn(mockSecurityAlertsApiClient, 'scanTransactions')
.mockResolvedValue({
status: 'SUCCESS',
} as SecurityAlertSimulationValidationResponse);

await transactionScanService.scanTransaction({
method: 'method',
accountAddress: 'accountAddress',
transaction: 'transaction',
scope: Network.Mainnet,
origin,
});

expect(scanTransactions).toHaveBeenCalledWith(
expect.objectContaining({ origin: 'https://metamask.io' }),
);
});

it('forwards a verifiable origin to the scan', async () => {
const scanTransactions = jest
.spyOn(mockSecurityAlertsApiClient, 'scanTransactions')
.mockResolvedValue({
status: 'SUCCESS',
} as SecurityAlertSimulationValidationResponse);

await transactionScanService.scanTransaction({
method: 'method',
accountAddress: 'accountAddress',
transaction: 'transaction',
scope: Network.Mainnet,
origin: 'https://portfolio.metamask.io',
});

expect(scanTransactions).toHaveBeenCalledWith(
expect.objectContaining({ origin: 'https://portfolio.metamask.io' }),
);
});

it('returns null if the scan fails', async () => {
const error = new Error('Scan failed');
jest
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { Logger } from '@metamask/snap-networks-utils';
import { resolveOrigin } from '@metamask/snap-networks-utils';

import type { SolanaKeyringAccount } from '../../../entities';
import type { SecurityAlertsApiClient } from '../../clients/security-alerts-api/SecurityAlertsApiClient';
import type { SecurityAlertSimulationValidationResponse } from '../../clients/security-alerts-api/types';
import { METAMASK_ORIGIN, METAMASK_ORIGIN_URL } from '../../constants/solana';
import { METAMASK_ORIGIN_URL } from '../../constants/solana';
import type { Network } from '../../constants/solana';
import { trackError } from '../../utils/errors';
import type { AnalyticsService } from '../analytics/AnalyticsService';
Expand Down Expand Up @@ -63,7 +64,12 @@ export class TransactionScanService {
accountAddress,
transactions: [transaction],
scope,
origin: origin === METAMASK_ORIGIN ? METAMASK_ORIGIN_URL : origin,
// Only a verifiable origin may reach the scan: the URL is a core
// heuristic and can flip a verdict, so an unverifiable one (a
// WalletConnect channel id, or a URL self-reported by the requester)
// would let a dapp influence the check meant to catch it. Those are
// reported as wallet-initiated instead.
origin: resolveOrigin(origin).verifiedOrigin ?? METAMASK_ORIGIN_URL,
options,
});

Expand Down
106 changes: 0 additions & 106 deletions packages/solana-wallet-snap/src/core/utils/parseOrigin.test.ts

This file was deleted.

19 changes: 0 additions & 19 deletions packages/solana-wallet-snap/src/core/utils/parseOrigin.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { SnapComponent } from '@metamask/snaps-sdk/jsx';
import { Box, Icon, Text as SnapText, Tooltip } from '@metamask/snaps-sdk/jsx';

import type { Locale } from '../../../../core/utils/i18n';
import { i18n } from '../../../../core/utils/i18n';

type OriginRowProps = {
/** Hostname to display. The row is not rendered when `null`. */
displayOrigin: string | null;
/** Whether the hostname was reported by the requester and can't be verified. */
isSelfReported: boolean;
locale: Locale;
};

/**
* The "Request from" row of a confirmation.
*
* A self-reported origin is displayed with an explicit "not verified" marker:
* it comes from the requesting app over a transport that cannot prove it, so
* showing it bare would imply a verification we never made.
*
* @param props - The component props.
* @param props.displayOrigin - Hostname to display, or `null` to render nothing.
* @param props.isSelfReported - Whether the hostname is unverifiable.
* @param props.locale - The locale used for the labels.
* @returns The origin row, or `null` when there is nothing to display.
*/
export const OriginRow: SnapComponent<OriginRowProps> = ({
displayOrigin,
isSelfReported,
locale,
}) => {
if (!displayOrigin) {
return null;
}

const translate = i18n(locale);

return (
<Box alignment="space-between" direction="horizontal">
<Box alignment="space-between" direction="horizontal" center>
<SnapText fontWeight="medium" color="alternative">
{translate('confirmation.origin')}
</SnapText>
<Tooltip
content={translate(
isSelfReported
? 'confirmation.origin.unverified.tooltip'
: 'confirmation.origin.tooltip',
)}
>
<Icon name="question" color="muted" />
</Tooltip>
</Box>
<Box direction="horizontal" alignment="end" center>
<SnapText>{displayOrigin}</SnapText>
{isSelfReported ? (
<SnapText color="warning">
{translate('confirmation.origin.unverified')}
</SnapText>
) : null}
</Box>
</Box>
);
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { SelfReportedOriginMetadata } from '@metamask/snap-networks-utils';
import { resolveOrigin } from '@metamask/snap-networks-utils';
import type { SnapComponent } from '@metamask/snaps-sdk/jsx';
import {
Address,
Expand All @@ -18,8 +20,8 @@ import { addressToCaip10 } from '../../../../core/utils/addressToCaip10';
import { formatCrypto } from '../../../../core/utils/formatCrypto';
import { formatFiat } from '../../../../core/utils/formatFiat';
import { i18n } from '../../../../core/utils/i18n';
import { parseOrigin } from '../../../../core/utils/parseOrigin';
import { tokenToFiat } from '../../../../core/utils/tokenToFiat';
import { OriginRow } from '../OriginRow/OriginRow';

type TransactionDetailsProps = {
accountAddress: string | null;
Expand All @@ -33,10 +35,12 @@ type TransactionDetailsProps = {
preferences: Preferences;
networkImage: string | null;
origin: string;
originMetadata: SelfReportedOriginMetadata | null;
};

export const TransactionDetails: SnapComponent<TransactionDetailsProps> = ({
origin,
originMetadata,
accountAddress,
accountDomain,
destinationAddress,
Expand All @@ -50,9 +54,12 @@ export const TransactionDetails: SnapComponent<TransactionDetailsProps> = ({
}) => {
const { currency, locale } = preferences;
const translate = i18n(locale);
const isMetaMaskOrigin = origin === METAMASK_ORIGIN;
const originHostname =
origin && !isMetaMaskOrigin ? parseOrigin(origin) : null;
const { displayOrigin, isSelfReported } = resolveOrigin(
origin,
originMetadata,
);
// Wallet-initiated transactions have no origin row to show.
const originToDisplay = origin === METAMASK_ORIGIN ? null : displayOrigin;

const pricesFetching = fetchingPricesStatus === 'fetching';
const pricesError = fetchingPricesStatus === 'error';
Expand All @@ -64,19 +71,13 @@ export const TransactionDetails: SnapComponent<TransactionDetailsProps> = ({

return (
<Section>
{originHostname ? (
{originToDisplay ? (
<Box>
<Box alignment="space-between" direction="horizontal">
<Box alignment="space-between" direction="horizontal" center>
<Text fontWeight="medium" color="alternative">
{translate('confirmation.origin')}
</Text>
<Tooltip content={translate('confirmation.origin.tooltip')}>
<Icon name="question" color="muted" />
</Tooltip>
</Box>
<Text>{originHostname}</Text>
</Box>
<OriginRow
displayOrigin={originToDisplay}
isSelfReported={isSelfReported}
locale={locale}
/>
<Box>{null}</Box>
</Box>
) : null}
Expand Down
Loading
Loading