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
2 changes: 2 additions & 0 deletions packages/stellar-wallet-snap/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Show the dapp URL reported in `originMetadata` in the "Request from" row of the confirmations, marked "Not verified", and keep hiding the row when there is nothing verifiable or self-reported to show. The self-reported URL is display-only and never reaches the security alerts API, which keeps reporting non-URL origins as in-app
- Stop labelling the literal `wallet-connect` origin as "WalletConnect"; the clients now pass a per-session channel id instead
- Display transaction error message in ChangeTrustOpt and ConfirmSend confirmation dialogs ([#220](https://github.com/MetaMask/internal-snaps/pull/220))
- Skip destination validation in `onAmountInput` ([#220](https://github.com/MetaMask/internal-snaps/pull/220))
- `createValidatedSendTransaction` now throws `InvalidAssetForCreateAccountException` instead of `AccountNotActivatedException` when sending a non-native asset to an unfunded destination ([#185](https://github.com/MetaMask/internal-snaps/pull/185))
Expand Down
6 changes: 6 additions & 0 deletions packages/stellar-wallet-snap/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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.from": {
"message": "From"
},
Expand Down
6 changes: 6 additions & 0 deletions packages/stellar-wallet-snap/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,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.from": {
"message": "From"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export class SignAuthEntryHandler extends BaseSep43KeyringHandler<
readableAuthEntry,
},
origin: request.origin,
originMetadata: request.originMetadata,
interfaceKey: ConfirmationInterfaceKey.SignAuthEntry,
})) === true
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export class SignMessageHandler extends BaseSep43KeyringHandler<
message,
},
origin: request.origin,
originMetadata: request.originMetadata,
interfaceKey: ConfirmationInterfaceKey.SignMessage,
})) === true
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export class SignTransactionHandler extends BaseSep43KeyringHandler<
(await this.#confirmationUIController.renderConfirmationDialog({
scope: request.scope,
origin: request.origin,
originMetadata: request.originMetadata,
interfaceKey: ConfirmationInterfaceKey.SignTransaction,
fee: readableTransaction.feeStroops,
renderContext: {
Expand Down
6 changes: 6 additions & 0 deletions packages/stellar-wallet-snap/src/ui/confirmation/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ export type ConfirmationBaseProps = Partial<ContextWithPrices> & {
scope: KnownCaip2ChainId;
networkImage: string | null;
origin: string;
/**
* True when `origin` was reported by the requester over a transport that
* cannot prove it (WalletConnect, SDK), so the UI must frame it as
* unverified. See `resolveOrigin` in `@metamask/snap-networks-utils`.
*/
isSelfReportedOrigin?: boolean;
feeData?: FeeData;
// Locale key for the validation banner subtitle. When omitted, the banner
// falls back to `confirmation.txnError.generic`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { ComponentOrElement } from '@metamask/snaps-sdk';
import { Box, Icon, Text as SnapText, Tooltip } from '@metamask/snaps-sdk/jsx';

import { i18n } from '../../../utils';

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

/**
* 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.origin - Hostname to display, or an empty string 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 = ({
origin,
isSelfReported = false,
locale,
}: OriginRowProps): ComponentOrElement | null => {
if (!origin) {
return null;
}

const translate = i18n(locale);

return (
<Box alignment="space-between" direction="horizontal">
<Box direction="horizontal" alignment="start">
<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">
<SnapText>{origin}</SnapText>
{isSelfReported ? (
<SnapText color="warning">
{translate('confirmation.origin.unverified')}
</SnapText>
) : null}
</Box>
</Box>
);
};
23 changes: 17 additions & 6 deletions packages/stellar-wallet-snap/src/ui/confirmation/controller.tsx
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 { DialogResult } from '@metamask/snaps-sdk';

import type { KnownCaip2ChainId } from '../../api';
Expand Down Expand Up @@ -26,11 +28,7 @@ import {
import { xlmIcon } from '../images';
import { ConfirmationInterfaceKey, FetchStatus } from './api';
import type { ContextWithPrices } from './api';
import {
formatFeeData,
formatOrigin,
getPreferencesWithFallback,
} from './utils';
import { formatFeeData, getPreferencesWithFallback } from './utils';
import { renderConfirmationView } from './views/render';
import type { ConfirmationViewProps } from './views/render';

Expand Down Expand Up @@ -61,6 +59,8 @@ type RenderConfirmationDialogCommon<Props extends ConfirmationViewProps> = {
scope: KnownCaip2ChainId;
renderContext: Props;
origin?: string;
/** Metadata for origins the client could not verify (WalletConnect, SDK). */
originMetadata?: SelfReportedOriginMetadata | null;
renderOptions?: ConfirmationRenderOptions;
securityScanRequest?: Omit<SecurityScanRequest, 'origin' | 'scope'>;
transactionValidationRequest?: TransactionValidationRequest;
Expand Down Expand Up @@ -126,8 +126,18 @@ export class ConfirmationUXController {
scope,
renderContext,
origin = METAMASK_ORIGIN,
originMetadata,
fee,
} = params;

// `origin` is only displayable when it is verifiable or self-reported; a
// WalletConnect channel id has nothing to show. A self-reported origin is
// display-only: it never reaches the security scan below, which keeps
// receiving the raw origin (non-URLs are reported as in-app).
const { displayOrigin, isSelfReported } = resolveOrigin(
origin,
originMetadata,
);
const renderOptions = {
...this.#defaultRenderOptions,
...params.renderOptions,
Expand Down Expand Up @@ -204,7 +214,8 @@ export class ConfirmationUXController {
preferences,
locale: preferences.locale as Locale,
networkImage: xlmIcon,
origin: formatOrigin(origin),
origin: displayOrigin ?? '',
isSelfReportedOrigin: isSelfReported,
currency: preferences.currency,
scope,
feeData: fee ? formatFeeData(scope, fee) : {},
Expand Down
68 changes: 0 additions & 68 deletions packages/stellar-wallet-snap/src/ui/confirmation/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
ConfirmationBanner,
getParam,
isFetchInProgress,
formatOrigin,
isLocalTransactionValidationFailed,
isRemoteTransactionScanLoading,
requiresMaliciousAcknowledgement,
Expand Down Expand Up @@ -44,73 +43,6 @@ describe('confirmation utils', () => {
);
});

describe('formatOrigin', () => {
it.each([
{
testcase: '"Unknown" for an undefined origin',
input: undefined,
expected: 'Unknown',
},
{
testcase: '"Unknown" for an empty origin',
input: '',
expected: 'Unknown',
},
{
testcase: '"MetaMask" for the internal metamask origin',
input: 'metamask',
expected: 'MetaMask',
},
{
testcase: '"WalletConnect" for the wallet-connect origin',
input: 'wallet-connect',
expected: 'WalletConnect',
},
{
testcase: 'known origins case-insensitively for metamask',
input: 'MetaMask',
expected: 'MetaMask',
},
{
testcase: 'known origins case-insensitively for wallet-connect',
input: 'Wallet-Connect',
expected: 'WalletConnect',
},
{
testcase: 'the hostname for an https URL',
input: 'https://example.com',
expected: 'example.com',
},
{
testcase: 'the hostname for an https URL with path and query',
input: 'https://app.example.com/path?q=1',
expected: 'app.example.com',
},
{
testcase: 'the hostname for an http URL',
input: 'http://example.com',
expected: 'example.com',
},
{
testcase: 'an empty string for a non-URL string',
input: '1234abcd-channel-id',
expected: '',
},
{
testcase: 'an empty string for a non-http URL',
input: 'ftp://example.com',
expected: '',
},
{
testcase: 'an empty string for an invalid value',
input: 'not a url',
expected: '',
},
])('returns $expected for $testcase', ({ input, expected }) => {
expect(formatOrigin(input)).toStrictEqual(expected);
});
});

describe('isRemoteTransactionScanLoading', () => {
it('disables confirm while scan is fetching', () => {
expect(
Expand Down
48 changes: 0 additions & 48 deletions packages/stellar-wallet-snap/src/ui/confirmation/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,54 +36,6 @@ export function getNetworkName(scope: KnownCaip2ChainId): string {
return NetworkName[scope] ?? 'Unknown';
}

/**
* Display labels for known, non-URL origins, keyed by their lowercased raw value.
* Lets us show a friendly name for the internal MetaMask origin and WalletConnect
* channels instead of an empty/hidden origin row.
*/
const KNOWN_ORIGIN_LABELS: Record<string, string> = {
metamask: 'MetaMask',
'wallet-connect': 'WalletConnect',
};

/**
* Formats an origin for display purposes.
*
* @param origin - The origin string to format (e.g., 'metamask', 'https://example.com').
* @returns The formatted origin string. `'Unknown'` for undefined/empty origins, a
* friendly label for known origins (e.g. `'MetaMask'`, `'WalletConnect'`), the
* hostname for http(s) URLs, or an empty string for any other value (e.g. a
* WalletConnect channelId or a non-http URL) so the UI hides the origin row.
*/
export function formatOrigin(origin: string | undefined): string {
if (!origin) {
return 'Unknown';
}

const knownLabel = KNOWN_ORIGIN_LABELS[origin.toLowerCase()];
if (knownLabel) {
return knownLabel;
}

// Try to extract hostname from URL
try {
const url = new URL(origin);
return isHttpOrHttpsUrl(url) ? url.hostname : '';
} catch {
return '';
}
}

/**
* Checks whether a parsed URL uses an HTTP(S) protocol.
*
* @param url - The parsed URL to check.
* @returns Whether the URL uses HTTP or HTTPS.
*/
function isHttpOrHttpsUrl(url: URL): boolean {
return url.protocol === 'http:' || url.protocol === 'https:';
}

/**
* Gets the locale from the preferences.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ import {
Box,
Container,
Heading,
Icon,
Link,
Section,
Text as SnapText,
Tooltip,
} from '@metamask/snaps-sdk/jsx';

import type { StellarKeyringAccount } from '../../../../services/account';
Expand All @@ -27,6 +25,7 @@ import {
FeeRow,
} from '../../components';
import { NetworkRow } from '../../components/Network';
import { OriginRow } from '../../components/OriginRow';
import {
getAccountExplorerUrl,
getAccountName,
Expand All @@ -51,6 +50,7 @@ export const ConfirmSendTransaction = ({
feeData,
tokenPrices,
origin,
isSelfReportedOrigin,
preferences,
tokenPricesFetchStatus = FetchStatus.Initial,
scan,
Expand Down Expand Up @@ -92,19 +92,11 @@ export const ConfirmSendTransaction = ({
/>

<Section>
{origin ? (
<Box alignment="space-between" direction="horizontal">
<Box direction="horizontal" alignment="start">
<SnapText fontWeight="medium" color="alternative">
{t('confirmation.origin')}
</SnapText>
<Tooltip content={t('confirmation.origin.tooltip')}>
<Icon name="question" color="muted" />
</Tooltip>
</Box>
<SnapText>{origin}</SnapText>
</Box>
) : null}
<OriginRow
origin={origin}
isSelfReported={isSelfReportedOrigin}
locale={locale}
/>
{/* From */}
<Box alignment="space-between" direction="horizontal">
<SnapText fontWeight="medium" color="alternative">
Expand Down
Loading
Loading