Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ jest.mock('../../Views/AccountConnect', () => ({
}));

jest.mock('../../hooks/useOriginSource');
jest.mock('../../hooks/useSDKV2Connection', () => ({
useSDKV2Connection: jest.fn(() => undefined),
}));

jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { getApiAnalyticsProperties } from '../../../util/metrics/MultichainAPI/getApiAnalyticsProperties';
import { selectPendingApprovals } from '../../../selectors/approvalController';
import { isEqual } from 'lodash';
import { useSDKV2Connection } from '../../hooks/useSDKV2Connection';

export interface PermissionApprovalProps {
// TODO: Replace "any" with type
Expand All @@ -30,9 +31,12 @@ const PermissionApproval = (props: PermissionApprovalProps) => {
// Prevents re-navigation for the same approval when pendingApprovals changes.
const lastNavigatedApprovalIdRef = useRef<string | null>(null);

const eventSource = useOriginSource({
origin: approvalRequest?.requestData?.metadata?.origin,
});
const origin = approvalRequest?.requestData?.metadata?.origin;

const eventSource = useOriginSource({ origin });

const sdkV2Connection = useSDKV2Connection(origin);
const anonId = sdkV2Connection?.originatorInfo?.anonId;

useEffect(() => {
if (
Expand Down Expand Up @@ -74,6 +78,7 @@ const PermissionApproval = (props: PermissionApprovalProps) => {
source: eventSource,
chain_id_list: chainIds,
...getApiAnalyticsProperties(isMultichainRequest),
...(anonId ? { anon_id: anonId } : {}),
})
.build(),
);
Expand All @@ -91,6 +96,7 @@ const PermissionApproval = (props: PermissionApprovalProps) => {
trackEvent,
createEventBuilder,
eventSource,
anonId,
// Re-run when the queue changes so new approvals are picked up.
// The ref guard above prevents re-navigation for the same approval.
pendingApprovals,
Expand Down
9 changes: 8 additions & 1 deletion app/components/Views/AccountConnect/AccountConnect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ import { getApiAnalyticsProperties } from '../../../util/metrics/MultichainAPI/g
import { isSnapId } from '@metamask/snaps-utils';
import { HardwareDeviceTypes } from '../../../constants/keyringTypes';
import { getConnectedDevicesCount } from '../../../core/HardwareWallets/analytics';
import { useSDKV2Connection } from '../../hooks/useSDKV2Connection';

const AccountConnect = (props: AccountConnectProps) => {
const { colors } = useTheme();
Expand Down Expand Up @@ -161,6 +162,9 @@ const AccountConnect = (props: AccountConnectProps) => {

const { origin: channelIdOrHostname, isEip1193Request } = hostInfo.metadata;

const sdkV2Connection = useSDKV2Connection(channelIdOrHostname);
const anonId = sdkV2Connection?.originatorInfo?.anonId;

const isChannelId = isUUID(channelIdOrHostname);

const sdkConnection = SDKConnect.getInstance().getConnection({
Expand Down Expand Up @@ -413,13 +417,15 @@ const AccountConnect = (props: AccountConnectProps) => {
chain_id_list: chainIds,
referrer: channelIdOrHostname,
...getApiAnalyticsProperties(isMultichainRequest),
...(anonId ? { anon_id: anonId } : {}),
Copy link
Copy Markdown
Contributor

@adonesky1 adonesky1 Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe in all of these cases it should be added as a sensitive property as it is done in all of the SDKv1 instances?

.addSensitiveProperties({ anon_id: anonId })

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

})
.build(),
);
},
[
accountsLength,
channelIdOrHostname,
anonId,
trackEvent,
createEventBuilder,
eventSource,
Expand Down Expand Up @@ -516,12 +522,12 @@ const AccountConnect = (props: AccountConnectProps) => {
.addProperties({
number_of_accounts: accountsLength,
number_of_accounts_connected: connectedAccountLength,
// TODO: Fix this. Not accurate
account_type: getAddressAccountType(activeAddress),
source: eventSource,
chain_id_list: selectedChainIds,
referrer,
...getApiAnalyticsProperties(isMultichainRequest),
...(anonId ? { anon_id: anonId } : {}),
})
.build(),
);
Expand All @@ -545,6 +551,7 @@ const AccountConnect = (props: AccountConnectProps) => {
setIsLoading(false);
}
}, [
anonId,
eventSource,
selectedAddresses,
hostInfo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
getUrlObj,
prefixUrlWithProtocol,
} from '../../../../util/browser/index.ts';
import { getAddressAccountType } from '../../../../util/address/index.ts';

// Internal dependencies.
import { PermissionsRequest } from '@metamask/permission-controller';
Expand Down Expand Up @@ -258,6 +259,7 @@ const MultichainAccountConnect = (props: AccountConnectProps) => {
const { origin: channelIdOrHostname, isEip1193Request } = hostInfo.metadata;

const sdkV2Connection = useSDKV2Connection(channelIdOrHostname);
const anonId = sdkV2Connection?.originatorInfo?.anonId;
const isOriginMMSDKV2RemoteConn = useMemo(
() => Boolean(sdkV2Connection?.isV2),
[sdkV2Connection?.isV2],
Expand Down Expand Up @@ -601,13 +603,15 @@ const MultichainAccountConnect = (props: AccountConnectProps) => {
chain_id_list: chainIds,
referrer: channelIdOrHostname,
...getApiAnalyticsProperties(isMultichainRequest),
...(anonId ? { anon_id: anonId } : {}),
})
.build(),
);
},
[
accountsLength,
channelIdOrHostname,
anonId,
trackEvent,
createEventBuilder,
eventSource,
Expand Down Expand Up @@ -694,17 +698,24 @@ const MultichainAccountConnect = (props: AccountConnectProps) => {

triggerDappViewedEvent(connectedAccountLength);

let accountType: string;
try {
accountType = getAddressAccountType(selectedCaipAccountIds[0]);
} catch {
accountType = 'unknown';
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated behavior change bundled without test coverage

Medium Severity

getAddressAccountType receives selectedCaipAccountIds[0], which is a CAIP account ID (e.g. a Solana eip155:… or solana:… format). Internally, the function parses the address then calls toFormattedAddress, which uses toChecksumAddress — an EVM-only operation that will throw for non-EVM chain addresses. While the try/catch gracefully defaults to 'unknown', the previous hard-coded 'multichain' value was semantically more accurate for this multichain flow. This behavior change (from 'multichain' to a per-address keyring lookup) is unrelated to the anon_id plumbing described in the PR and silently degrades the account_type analytics property to 'unknown' for all non-EVM account selections.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3771262. Configure here.


trackEvent(
createEventBuilder(MetaMetricsEvents.CONNECT_REQUEST_COMPLETED)
.addProperties({
number_of_accounts: accountsLength,
number_of_accounts_connected: connectedAccountLength,
// TODO: Fix this. Not accurate
account_type: 'multichain',
account_type: accountType,
source: eventSource,
chain_id_list: selectedChainIds,
referrer,
...getApiAnalyticsProperties(isMultichainRequest),
...(anonId ? { anon_id: anonId } : {}),
})
.build(),
);
Expand All @@ -728,6 +739,7 @@ const MultichainAccountConnect = (props: AccountConnectProps) => {
setIsLoading(false);
}
}, [
anonId,
hostInfo,
channelIdOrHostname,
requestedRequestWithExistingPermissions,
Expand Down
40 changes: 40 additions & 0 deletions app/core/SDKConnectV2/adapters/host-application-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ describe('HostApplicationAdapter', () => {
dappId: mockConnectionInfo.metadata.dapp.name,
apiVersion: mockConnectionInfo.metadata.sdk.version,
platform: mockConnectionInfo.metadata.sdk.platform,
anonId: undefined,
},
isV2: true,
},
Expand Down Expand Up @@ -324,6 +325,7 @@ describe('HostApplicationAdapter', () => {
dappId: mockConnectionInfo1.metadata.dapp.name,
apiVersion: mockConnectionInfo1.metadata.sdk.version,
platform: mockConnectionInfo1.metadata.sdk.platform,
anonId: undefined,
},
isV2: true,
},
Expand All @@ -338,6 +340,7 @@ describe('HostApplicationAdapter', () => {
dappId: mockConnectionInfo2.metadata.dapp.name,
apiVersion: mockConnectionInfo2.metadata.sdk.version,
platform: mockConnectionInfo2.metadata.sdk.platform,
anonId: undefined,
},
isV2: true,
},
Expand All @@ -346,6 +349,43 @@ describe('HostApplicationAdapter', () => {
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(setSdkV2Connections).toHaveBeenCalledWith(expectedSessions);
});

it('includes anonId in originatorInfo when analytics.anon_id is present', () => {
const connInfoWithAnon: ConnectionInfo = {
...createMockConnectionInfo('conn-anon', 'AnonTest'),
metadata: {
...createMockConnectionInfo('conn-anon', 'AnonTest').metadata,
analytics: {
anon_id: 'aabbccdd-1122-3344-5566-778899aabbcc',
},
},
};
const connections = [
{ id: connInfoWithAnon.id, info: connInfoWithAnon },
] as unknown as Connection[];

adapter.syncConnectionList(connections);

const expectedSessions = {
[connInfoWithAnon.id]: {
id: connInfoWithAnon.id,
otherPublicKey: '',
origin: connInfoWithAnon.metadata.dapp.url,
originatorInfo: {
title: connInfoWithAnon.metadata.dapp.name,
url: connInfoWithAnon.metadata.dapp.url,
icon: connInfoWithAnon.metadata.dapp.icon,
dappId: connInfoWithAnon.metadata.dapp.name,
apiVersion: connInfoWithAnon.metadata.sdk.version,
platform: connInfoWithAnon.metadata.sdk.platform,
anonId: 'aabbccdd-1122-3344-5566-778899aabbcc',
},
isV2: true,
},
} as unknown as SDKSessions;

expect(setSdkV2Connections).toHaveBeenCalledWith(expectedSessions);
});
});

describe('revokePermissions', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export class HostApplicationAdapter implements IHostApplicationAdapter {
dappId: conn.info.metadata.dapp.name,
apiVersion: conn.info.metadata.sdk.version,
platform: conn.info.metadata.sdk.platform,
anonId: conn.info.metadata.analytics?.anon_id,
},
isV2: true, // Flag to identify this as a V2 connection
};
Expand Down
59 changes: 59 additions & 0 deletions app/core/SDKConnectV2/types/connection-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,4 +334,63 @@ describe('isConnectionRequest', () => {
(req.metadata.sdk as Record<string, unknown>).platform = 'p'.repeat(65);
expect(isConnectionRequest(req)).toBe(false);
});

// ──────────────────────────────────────────
// metadata.analytics (optional)
// ──────────────────────────────────────────
it('accepts a request without metadata.analytics', () => {
const req = validRequest();
delete (req.metadata as unknown as Record<string, unknown>).analytics;
expect(isConnectionRequest(req)).toBe(true);
});

it('preserves valid metadata.analytics.anon_id (UUID)', () => {
const req = validRequest();
(req.metadata as unknown as Record<string, unknown>).analytics = {
anon_id: 'aabbccdd-1122-3344-5566-778899aabbcc',
};
expect(isConnectionRequest(req)).toBe(true);
expect(req.metadata.analytics).toEqual({
anon_id: 'aabbccdd-1122-3344-5566-778899aabbcc',
});
});

it('strips analytics when it is not an object', () => {
const req = validRequest();
(req.metadata as unknown as Record<string, unknown>).analytics = 'bad';
expect(isConnectionRequest(req)).toBe(true);
expect(req.metadata.analytics).toBeUndefined();
});

it('strips analytics when it is null', () => {
const req = validRequest();
(req.metadata as unknown as Record<string, unknown>).analytics = null;
expect(isConnectionRequest(req)).toBe(true);
expect(req.metadata.analytics).toBeUndefined();
});

it('strips analytics when anon_id is missing', () => {
const req = validRequest();
(req.metadata as unknown as Record<string, unknown>).analytics = {};
expect(isConnectionRequest(req)).toBe(true);
expect(req.metadata.analytics).toBeUndefined();
});

it('strips analytics when anon_id is not a string', () => {
const req = validRequest();
(req.metadata as unknown as Record<string, unknown>).analytics = {
anon_id: 42,
};
expect(isConnectionRequest(req)).toBe(true);
expect(req.metadata.analytics).toBeUndefined();
});

it('strips analytics when anon_id is not a valid UUID', () => {
const req = validRequest();
(req.metadata as unknown as Record<string, unknown>).analytics = {
anon_id: 'not-a-uuid',
};
expect(isConnectionRequest(req)).toBe(true);
expect(req.metadata.analytics).toBeUndefined();
});
});
13 changes: 13 additions & 0 deletions app/core/SDKConnectV2/types/connection-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,18 @@ export function isConnectionRequest(data: unknown): data is ConnectionRequest {
return false;
}

// analytics is purely telemetry — strip it when malformed rather than
// rejecting the connection, so a dapp-side bug can't break connectivity.
if (metadata.analytics !== undefined) {
if (
typeof metadata.analytics !== 'object' ||
metadata.analytics === null ||
typeof metadata.analytics.anon_id !== 'string' ||
!isUUID(metadata.analytics.anon_id)
) {
delete (metadata as unknown as Record<string, unknown>).analytics;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type guard mutates its input argument silently

Low Severity

The isConnectionRequest type guard mutates its input by deleting metadata.analytics when malformed. Type guards are conventionally pure predicate functions — callers don't expect them to have side effects on the checked object. Since data is passed by reference, the deletion silently alters the caller's object. Extracting the sanitization into a separate step (e.g., a sanitizeConnectionRequest function called after the guard) would make the mutation explicit and keep the type guard free of side effects.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ec0f6c9. Configure here.

}

return true;
}
3 changes: 3 additions & 0 deletions app/core/SDKConnectV2/types/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ export interface Metadata {
version: string;
platform: string;
};
analytics?: {
anon_id: string;
};
}
Loading