From 06f19d6c85834932bfcb1361fd12ea46b8836324 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Mon, 30 Jun 2025 17:03:34 +0100 Subject: [PATCH 01/15] chore: adds two events --- .../refreshConfirmationEstimation.tsx | 1 + .../services/analytics/AnalyticsService.ts | 73 ++++++++++++++++++- .../transaction-scan/TransactionScan.ts | 53 +++++++++++++- .../core/services/transaction-scan/types.ts | 11 +++ packages/snap/src/snapContext.ts | 1 + 5 files changed, 137 insertions(+), 2 deletions(-) diff --git a/packages/snap/src/core/handlers/onCronjob/cronjobs/refreshConfirmationEstimation.tsx b/packages/snap/src/core/handlers/onCronjob/cronjobs/refreshConfirmationEstimation.tsx index b52f10022..dc394a37d 100644 --- a/packages/snap/src/core/handlers/onCronjob/cronjobs/refreshConfirmationEstimation.tsx +++ b/packages/snap/src/core/handlers/onCronjob/cronjobs/refreshConfirmationEstimation.tsx @@ -72,6 +72,7 @@ export const refreshConfirmationEstimation: OnCronjobHandler = async () => { transaction: interfaceContext.transaction, scope: interfaceContext.scope, origin: interfaceContext.origin, + account: interfaceContext.account, }); const updatedInterfaceContextFinal = diff --git a/packages/snap/src/core/services/analytics/AnalyticsService.ts b/packages/snap/src/core/services/analytics/AnalyticsService.ts index 053038d07..61f4ff1a0 100644 --- a/packages/snap/src/core/services/analytics/AnalyticsService.ts +++ b/packages/snap/src/core/services/analytics/AnalyticsService.ts @@ -3,9 +3,10 @@ import type { Transaction } from '@metamask/keyring-api'; import { assert } from '@metamask/superstruct'; import type { SolanaKeyringAccount } from '../../../entities'; -import type { TransactionMetadata } from '../../constants/solana'; +import type { Network, TransactionMetadata } from '../../constants/solana'; import logger from '../../utils/logger'; import { Base64Struct } from '../../validation/structs'; +import { ScanStatus, SecurityAlertResponse } from '../transaction-scan/types'; /** * Service for tracking events related to transactions. @@ -160,4 +161,74 @@ export class AnalyticsService { }, }); } + + async trackEventSecurityAlertDetected( + account: SolanaKeyringAccount, + base64EncodedTransaction: string, + origin: string, + scope: Network, + securityAlertResponse: SecurityAlertResponse, + securityAlertReason: string | null, + securityAlertDescription: string, + ): Promise { + this.#logger.log( + `[📣 AnalyticsService] Tracking event security alert detected`, + ); + + assert(base64EncodedTransaction, Base64Struct); + + await snap.request({ + method: 'snap_trackEvent', + params: { + event: { + event: 'Security Alert Detected', + properties: { + message: 'Snap security alert detected', + origin, + account_id: account.id, + account_address: account.address, + account_type: account.type, + chain_id: scope, + security_alert_response: securityAlertResponse, + security_alert_reason: securityAlertReason, + security_alert_description: securityAlertDescription, + }, + }, + }, + }); + } + + async trackEventSecurityScanCompleted( + account: SolanaKeyringAccount, + base64EncodedTransaction: string, + origin: string, + scope: Network, + scanStatus: ScanStatus, + hasSecurityAlerts: boolean, + ): Promise { + this.#logger.log( + `[📣 AnalyticsService] Tracking event security scan completed`, + ); + + assert(base64EncodedTransaction, Base64Struct); + + await snap.request({ + method: 'snap_trackEvent', + params: { + event: { + event: 'Security Scan Completed', + properties: { + message: 'Snap security scan completed', + origin, + account_id: account.id, + account_address: account.address, + account_type: account.type, + chain_id: scope, + scan_status: scanStatus, + has_security_alerts: hasSecurityAlerts, + }, + }, + }, + }); + } } diff --git a/packages/snap/src/core/services/transaction-scan/TransactionScan.ts b/packages/snap/src/core/services/transaction-scan/TransactionScan.ts index f126b9119..d13f5f137 100644 --- a/packages/snap/src/core/services/transaction-scan/TransactionScan.ts +++ b/packages/snap/src/core/services/transaction-scan/TransactionScan.ts @@ -3,7 +3,10 @@ import type { SecurityAlertSimulationValidationResponse } from '../../clients/se import type { Network } from '../../constants/solana'; import type { ILogger } from '../../utils/logger'; import type { TokenMetadataService } from '../token-metadata/TokenMetadata'; -import type { TransactionScanResult } from './types'; +import type { TransactionScanResult, TransactionScanValidation } from './types'; +import type { AnalyticsService } from '../analytics/AnalyticsService'; +import type { SolanaKeyringAccount } from '../../../entities'; +import { ScanStatus, SecurityAlertResponse } from './types'; const ICON_SIZE = 16; @@ -14,13 +17,17 @@ export class TransactionScanService { readonly #tokenMetadataService: TokenMetadataService; + readonly #analyticsService: AnalyticsService; + constructor( securityAlertsApiClient: SecurityAlertsApiClient, tokenMetadataService: TokenMetadataService, + analyticsService: AnalyticsService, logger: ILogger, ) { this.#securityAlertsApiClient = securityAlertsApiClient; this.#tokenMetadataService = tokenMetadataService; + this.#analyticsService = analyticsService; this.#logger = logger; } @@ -33,6 +40,7 @@ export class TransactionScanService { * @param params.scope - The scope of the transaction. * @param params.origin - The origin of the transaction. * @param params.options - The options for the scan. + * @param params.account - The account for analytics tracking. * @returns The result of the scan. */ async scanTransaction({ @@ -42,6 +50,7 @@ export class TransactionScanService { scope, origin, options = ['simulation', 'validation'], + account, }: { method: string; accountAddress: string; @@ -49,6 +58,7 @@ export class TransactionScanService { scope: Network; origin: string; options?: string[]; + account?: SolanaKeyringAccount; }): Promise { try { const result = await this.#securityAlertsApiClient.scanTransactions({ @@ -62,6 +72,31 @@ export class TransactionScanService { const scan = this.#mapScan(result); + // The security scan is completed + if (account) { + await this.#analyticsService.trackEventSecurityScanCompleted( + account, + transaction, + origin, + scope, + scan?.status as ScanStatus, + scan?.validation?.type !== SecurityAlertResponse.Benign, + ); + + // And the alert is detected + if (scan?.validation?.type !== SecurityAlertResponse.Benign) { + await this.#analyticsService.trackEventSecurityAlertDetected( + account, + transaction, + origin, + scope, + scan?.validation?.type as SecurityAlertResponse, + scan?.validation?.reason, + this.#getSecurityAlertDescription(scan?.validation), + ); + } + } + if (!scan?.estimatedChanges?.assets) { return null; } @@ -94,10 +129,26 @@ export class TransactionScanService { return updatedScan; } catch (error) { this.#logger.error(error); + + if (account) { + await this.#analyticsService.trackEventSecurityScanCompleted( + account, + transaction, + origin, + scope, + ScanStatus.ERROR, + false, + ); + } + return null; } } + #getSecurityAlertDescription(validation: TransactionScanValidation): string { + return `Security alert: ${validation?.reason}`; + } + #mapScan( result: SecurityAlertSimulationValidationResponse, ): TransactionScanResult { diff --git a/packages/snap/src/core/services/transaction-scan/types.ts b/packages/snap/src/core/services/transaction-scan/types.ts index 96aa46115..6a50d21b5 100644 --- a/packages/snap/src/core/services/transaction-scan/types.ts +++ b/packages/snap/src/core/services/transaction-scan/types.ts @@ -32,3 +32,14 @@ export type TransactionScanResult = { validation: TransactionScanValidation; error: TransactionScanError | null; }; + +export enum SecurityAlertResponse { + Benign = 'Benign', + Warning = 'Warning', + Malicious = 'Malicious', +} + +export enum ScanStatus { + SUCCESS = 'SUCCESS', + ERROR = 'ERROR', +} diff --git a/packages/snap/src/snapContext.ts b/packages/snap/src/snapContext.ts index f4f2198f7..eda7b2e9c 100644 --- a/packages/snap/src/snapContext.ts +++ b/packages/snap/src/snapContext.ts @@ -100,6 +100,7 @@ const walletService = new WalletService(connection, transactionHelper, logger); const transactionScanService = new TransactionScanService( new SecurityAlertsApiClient(configProvider), tokenMetadataService, + analyticsService, logger, ); From 5d1e2a7ba7e5ba989c92e7505ad07853af7a1475 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Tue, 1 Jul 2025 11:25:18 +0100 Subject: [PATCH 02/15] chore: adds descriptions for blockaid validations --- .../services/transaction-scan/TransactionScan.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/snap/src/core/services/transaction-scan/TransactionScan.ts b/packages/snap/src/core/services/transaction-scan/TransactionScan.ts index d13f5f137..0a8d6b0ed 100644 --- a/packages/snap/src/core/services/transaction-scan/TransactionScan.ts +++ b/packages/snap/src/core/services/transaction-scan/TransactionScan.ts @@ -146,7 +146,21 @@ export class TransactionScanService { } #getSecurityAlertDescription(validation: TransactionScanValidation): string { - return `Security alert: ${validation?.reason}`; + // Reference: https://docs.blockaid.io/reference/response-reference-solana + const reasonDescriptions: Record = { + unfair_trade: 'Unfair trade of assets, without adequate compensation to the owner\'s account', + transfer_farming: 'Substantial transfer of the account\'s assets to untrusted entities', + writable_accounts_farming: 'Transaction exposes unused writable account, can be utilized in BIT-FLIP attacks patterns', + native_ownership_change: 'The account transferred ownership of its native SOL to untrusted entities', + spl_token_ownership_change: 'The account transferred ownership of its SPL tokens to untrusted entities', + exposure_farming: 'The account delegates ownership, thereby exposing its assets to untrusted spenders', + known_attacker: 'A known attacker\'s account is involved in the transaction', + invalid_signature: 'One of the transactions provided contains non valid signatures, that can lead misleading simulation results', + honeypot: 'The account invests funds in a token that is part of an orchestrated honeypot scheme', + other: 'The transaction was marked as malicious for other reason, further details would be described in features field', + }; + + return reasonDescriptions[validation.reason] || `Security alert: ${validation.reason}`; } #mapScan( From 4390b0e31d5e781970eeb8c871931f9fbda84a10 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Tue, 1 Jul 2025 14:01:58 +0100 Subject: [PATCH 03/15] chore: adds tests --- .../analytics/AnalyticsService.test.ts | 115 +++++++ .../transaction-scan/TransactionScan.test.ts | 288 ++++++++++++++++++ 2 files changed, 403 insertions(+) diff --git a/packages/snap/src/core/services/analytics/AnalyticsService.test.ts b/packages/snap/src/core/services/analytics/AnalyticsService.test.ts index 44368788a..3d6f67ead 100644 --- a/packages/snap/src/core/services/analytics/AnalyticsService.test.ts +++ b/packages/snap/src/core/services/analytics/AnalyticsService.test.ts @@ -6,6 +6,7 @@ import { Network } from '../../constants/solana'; import { MOCK_SOLANA_KEYRING_ACCOUNT_0 } from '../../test/mocks/solana-keyring-accounts'; import logger from '../../utils/logger'; import { AnalyticsService } from './AnalyticsService'; +import { ScanStatus, SecurityAlertResponse } from '../transaction-scan/types'; const mockSnapRequest = jest.fn(); const snap = { @@ -293,6 +294,120 @@ describe('AnalyticsService', () => { }); }); + describe('trackEventSecurityAlertDetected', () => { + it('tracks security alert detected event', async () => { + const securityAlertResponse = SecurityAlertResponse.Warning; + const securityAlertReason = 'transfer_farming'; + const securityAlertDescription = 'Substantial transfer of the account\'s assets to untrusted entities'; + + await analyticsService.trackEventSecurityAlertDetected( + mockAccount, + mockBase64Transaction, + mockOrigin, + mockScope, + securityAlertResponse, + securityAlertReason, + securityAlertDescription, + ); + + expect(loggerSpy).toHaveBeenCalledWith( + '[📣 AnalyticsService] Tracking event security alert detected', + ); + + expect(mockSnapRequest).toHaveBeenCalledWith({ + method: 'snap_trackEvent', + params: { + event: { + event: 'Security Alert Detected', + properties: { + message: 'Snap security alert detected', + origin: mockOrigin, + account_id: mockAccount.id, + account_address: mockAccount.address, + account_type: mockAccount.type, + chain_id: mockScope, + security_alert_response: securityAlertResponse, + security_alert_reason: securityAlertReason, + security_alert_description: securityAlertDescription, + }, + }, + }, + }); + }); + }); + + describe('trackEventSecurityScanCompleted', () => { + it('tracks security scan completed event with alerts detected', async () => { + const scanStatus = ScanStatus.SUCCESS; + const hasSecurityAlerts = true; + + await analyticsService.trackEventSecurityScanCompleted( + mockAccount, + mockBase64Transaction, + mockOrigin, + mockScope, + scanStatus, + hasSecurityAlerts, + ); + + expect(loggerSpy).toHaveBeenCalledWith( + '[📣 AnalyticsService] Tracking event security scan completed', + ); + + expect(mockSnapRequest).toHaveBeenCalledWith({ + method: 'snap_trackEvent', + params: { + event: { + event: 'Security Scan Completed', + properties: { + message: 'Snap security scan completed', + origin: mockOrigin, + account_id: mockAccount.id, + account_address: mockAccount.address, + account_type: mockAccount.type, + chain_id: mockScope, + scan_status: scanStatus, + has_security_alerts: hasSecurityAlerts, + }, + }, + }, + }); + }); + + it('tracks security scan completed event without alerts', async () => { + const scanStatus = ScanStatus.SUCCESS; + const hasSecurityAlerts = false; + + await analyticsService.trackEventSecurityScanCompleted( + mockAccount, + mockBase64Transaction, + mockOrigin, + mockScope, + scanStatus, + hasSecurityAlerts, + ); + + expect(mockSnapRequest).toHaveBeenCalledWith({ + method: 'snap_trackEvent', + params: { + event: { + event: 'Security Scan Completed', + properties: { + message: 'Snap security scan completed', + origin: mockOrigin, + account_id: mockAccount.id, + account_address: mockAccount.address, + account_type: mockAccount.type, + chain_id: mockScope, + scan_status: scanStatus, + has_security_alerts: hasSecurityAlerts, + }, + }, + }, + }); + }); + }); + describe('error handling', () => { it('handles snap.request errors gracefully', async () => { const error = new Error('Snap request failed'); diff --git a/packages/snap/src/core/services/transaction-scan/TransactionScan.test.ts b/packages/snap/src/core/services/transaction-scan/TransactionScan.test.ts index eb994f930..59a713086 100644 --- a/packages/snap/src/core/services/transaction-scan/TransactionScan.test.ts +++ b/packages/snap/src/core/services/transaction-scan/TransactionScan.test.ts @@ -1,9 +1,12 @@ import type { SecurityAlertsApiClient } from '../../clients/security-alerts-api/SecurityAlertsApiClient'; import type { SecurityAlertSimulationValidationResponse } from '../../clients/security-alerts-api/types'; import { Network } from '../../constants/solana'; +import { MOCK_SOLANA_KEYRING_ACCOUNT_0 } from '../../test/mocks/solana-keyring-accounts'; import type { ILogger } from '../../utils/logger'; +import type { AnalyticsService } from '../analytics/AnalyticsService'; import type { TokenMetadataService } from '../token-metadata/TokenMetadata'; import { TransactionScanService } from './TransactionScan'; +import { ScanStatus, SecurityAlertResponse } from './types'; describe('TransactionScan', () => { describe('scanTransaction', () => { @@ -11,6 +14,7 @@ describe('TransactionScan', () => { let mockSecurityAlertsApiClient: SecurityAlertsApiClient; let mockLogger: ILogger; let mockTokenMetadataService: TokenMetadataService; + let mockAnalyticsService: AnalyticsService; beforeEach(() => { mockTokenMetadataService = { @@ -25,9 +29,15 @@ describe('TransactionScan', () => { error: jest.fn(), } as unknown as ILogger; + mockAnalyticsService = { + trackEventSecurityScanCompleted: jest.fn().mockResolvedValue(undefined), + trackEventSecurityAlertDetected: jest.fn().mockResolvedValue(undefined), + } as unknown as AnalyticsService; + transactionScanService = new TransactionScanService( mockSecurityAlertsApiClient, mockTokenMetadataService, + mockAnalyticsService, mockLogger, ); }); @@ -67,5 +77,283 @@ describe('TransactionScan', () => { expect(result).toBeNull(); }); + + it('tracks security scan completion when account is provided', async () => { + const mockAccount = MOCK_SOLANA_KEYRING_ACCOUNT_0; + + jest + .spyOn(mockSecurityAlertsApiClient, 'scanTransactions') + .mockResolvedValue({ + status: ScanStatus.SUCCESS, + encoding: 'base58', + error: null, + error_details: null, + request_id: 'test-request-id', + result: { + validation: { + result_type: SecurityAlertResponse.Benign, + reason: '', + features: [], + }, + simulation: { + account_summary: { + account_assets_diff: [], + account_delegations: [], + account_ownerships_diff: [], + total_usd_diff: { in: 0, out: 0, total: 0 }, + }, + }, + }, + } as unknown as SecurityAlertSimulationValidationResponse); + + await transactionScanService.scanTransaction({ + method: 'method', + accountAddress: 'accountAddress', + transaction: 'transaction', + scope: Network.Mainnet, + origin: 'https://metamask.io', + account: mockAccount, + }); + + // hasSecurityAlerts = false for Benign response + expect( + mockAnalyticsService.trackEventSecurityScanCompleted, + ).toHaveBeenCalledWith( + mockAccount, + 'transaction', + 'https://metamask.io', + Network.Mainnet, + ScanStatus.SUCCESS, + false, + ); + }); + + it('tracks security alert when malicious transaction is detected', async () => { + const mockAccount = MOCK_SOLANA_KEYRING_ACCOUNT_0; + + jest + .spyOn(mockSecurityAlertsApiClient, 'scanTransactions') + .mockResolvedValue({ + status: ScanStatus.SUCCESS, + encoding: 'base58', + error: null, + error_details: null, + request_id: 'test-request-id', + result: { + validation: { + result_type: SecurityAlertResponse.Warning, + reason: 'transfer_farming', + features: [], + }, + simulation: { + account_summary: { + account_assets_diff: [], + account_delegations: [], + account_ownerships_diff: [], + total_usd_diff: { in: 0, out: 0, total: 0 }, + }, + }, + }, + } as unknown as SecurityAlertSimulationValidationResponse); + + await transactionScanService.scanTransaction({ + method: 'method', + accountAddress: 'accountAddress', + transaction: 'transaction', + scope: Network.Mainnet, + origin: 'https://metamask.io', + account: mockAccount, + }); + + // hasSecurityAlerts = true for Warning response + expect( + mockAnalyticsService.trackEventSecurityScanCompleted, + ).toHaveBeenCalledWith( + mockAccount, + 'transaction', + 'https://metamask.io', + Network.Mainnet, + ScanStatus.SUCCESS, + true, + ); + + expect( + mockAnalyticsService.trackEventSecurityAlertDetected, + ).toHaveBeenCalledWith( + mockAccount, + 'transaction', + 'https://metamask.io', + Network.Mainnet, + SecurityAlertResponse.Warning, + 'transfer_farming', + "Substantial transfer of the account's assets to untrusted entities", + ); + }); + + it('tracks error when scan fails and account is provided', async () => { + const mockAccount = MOCK_SOLANA_KEYRING_ACCOUNT_0; + + jest + .spyOn(mockSecurityAlertsApiClient, 'scanTransactions') + .mockRejectedValue(new Error('Scan failed')); + + await transactionScanService.scanTransaction({ + method: 'method', + accountAddress: 'accountAddress', + transaction: 'transaction', + scope: Network.Mainnet, + origin: 'https://metamask.io', + account: mockAccount, + }); + + expect( + mockAnalyticsService.trackEventSecurityScanCompleted, + ).toHaveBeenCalledWith( + mockAccount, + 'transaction', + 'https://metamask.io', + Network.Mainnet, + ScanStatus.ERROR, + false, + ); + }); + }); + + describe('getSecurityAlertDescription', () => { + let transactionScanService: TransactionScanService; + let mockSecurityAlertsApiClient: SecurityAlertsApiClient; + let mockLogger: ILogger; + let mockTokenMetadataService: TokenMetadataService; + let mockAnalyticsService: AnalyticsService; + + beforeEach(() => { + mockTokenMetadataService = { + generateImageComponent: jest.fn().mockResolvedValue(null), + } as unknown as TokenMetadataService; + + mockSecurityAlertsApiClient = { + scanTransactions: jest.fn().mockResolvedValue({}), + } as unknown as SecurityAlertsApiClient; + + mockLogger = { + error: jest.fn(), + } as unknown as ILogger; + + mockAnalyticsService = { + trackEventSecurityScanCompleted: jest.fn().mockResolvedValue(undefined), + trackEventSecurityAlertDetected: jest.fn().mockResolvedValue(undefined), + } as unknown as AnalyticsService; + + transactionScanService = new TransactionScanService( + mockSecurityAlertsApiClient, + mockTokenMetadataService, + mockAnalyticsService, + mockLogger, + ); + }); + + it('returns correct description for known reasons', () => { + const mockAccount = MOCK_SOLANA_KEYRING_ACCOUNT_0; + + jest + .spyOn(mockSecurityAlertsApiClient, 'scanTransactions') + .mockResolvedValue({ + status: ScanStatus.SUCCESS, + encoding: 'base58', + error: null, + error_details: null, + request_id: 'test-request-id', + result: { + validation: { + result_type: SecurityAlertResponse.Warning, + reason: 'transfer_farming', + features: [], + }, + simulation: { + account_summary: { + account_assets_diff: [], + account_delegations: [], + account_ownerships_diff: [], + total_usd_diff: { in: 0, out: 0, total: 0 }, + }, + }, + }, + } as unknown as SecurityAlertSimulationValidationResponse); + + return transactionScanService + .scanTransaction({ + method: 'method', + accountAddress: 'accountAddress', + transaction: 'transaction', + scope: Network.Mainnet, + origin: 'https://metamask.io', + account: mockAccount, + }) + .then(() => { + expect( + mockAnalyticsService.trackEventSecurityAlertDetected, + ).toHaveBeenCalledWith( + mockAccount, + 'transaction', + 'https://metamask.io', + Network.Mainnet, + SecurityAlertResponse.Warning, + 'transfer_farming', + "Substantial transfer of the account's assets to untrusted entities", + ); + }); + }); + + it('returns fallback description for unknown reasons', () => { + const mockAccount = MOCK_SOLANA_KEYRING_ACCOUNT_0; + + jest + .spyOn(mockSecurityAlertsApiClient, 'scanTransactions') + .mockResolvedValue({ + status: ScanStatus.SUCCESS, + encoding: 'base58', + error: null, + error_details: null, + request_id: 'test-request-id', + result: { + validation: { + result_type: SecurityAlertResponse.Warning, + reason: 'unknown_reason', + features: [], + }, + simulation: { + account_summary: { + account_assets_diff: [], + account_delegations: [], + account_ownerships_diff: [], + total_usd_diff: { in: 0, out: 0, total: 0 }, + }, + }, + }, + } as unknown as SecurityAlertSimulationValidationResponse); + + return transactionScanService + .scanTransaction({ + method: 'method', + accountAddress: 'accountAddress', + transaction: 'transaction', + scope: Network.Mainnet, + origin: 'https://metamask.io', + account: mockAccount, + }) + .then(() => { + expect( + mockAnalyticsService.trackEventSecurityAlertDetected, + ).toHaveBeenCalledWith( + mockAccount, + 'transaction', + 'https://metamask.io', + Network.Mainnet, + SecurityAlertResponse.Warning, + 'unknown_reason', + 'Security alert: unknown_reason', + ); + }); + }); }); }); From 898960f4ed1eb7c39c84a7cb363cd033d55247b9 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Tue, 1 Jul 2025 15:39:10 +0100 Subject: [PATCH 04/15] chore: lint --- .../analytics/AnalyticsService.test.ts | 5 ++- .../services/analytics/AnalyticsService.ts | 15 ++++--- .../transaction-scan/TransactionScan.test.ts | 1 + .../transaction-scan/TransactionScan.ts | 42 ++++++++++++------- 4 files changed, 41 insertions(+), 22 deletions(-) diff --git a/packages/snap/src/core/services/analytics/AnalyticsService.test.ts b/packages/snap/src/core/services/analytics/AnalyticsService.test.ts index 3d6f67ead..4d25430b2 100644 --- a/packages/snap/src/core/services/analytics/AnalyticsService.test.ts +++ b/packages/snap/src/core/services/analytics/AnalyticsService.test.ts @@ -5,8 +5,8 @@ import type { Transaction } from '@metamask/keyring-api'; import { Network } from '../../constants/solana'; import { MOCK_SOLANA_KEYRING_ACCOUNT_0 } from '../../test/mocks/solana-keyring-accounts'; import logger from '../../utils/logger'; -import { AnalyticsService } from './AnalyticsService'; import { ScanStatus, SecurityAlertResponse } from '../transaction-scan/types'; +import { AnalyticsService } from './AnalyticsService'; const mockSnapRequest = jest.fn(); const snap = { @@ -298,7 +298,8 @@ describe('AnalyticsService', () => { it('tracks security alert detected event', async () => { const securityAlertResponse = SecurityAlertResponse.Warning; const securityAlertReason = 'transfer_farming'; - const securityAlertDescription = 'Substantial transfer of the account\'s assets to untrusted entities'; + const securityAlertDescription = + "Substantial transfer of the account's assets to untrusted entities"; await analyticsService.trackEventSecurityAlertDetected( mockAccount, diff --git a/packages/snap/src/core/services/analytics/AnalyticsService.ts b/packages/snap/src/core/services/analytics/AnalyticsService.ts index 61f4ff1a0..f093b6155 100644 --- a/packages/snap/src/core/services/analytics/AnalyticsService.ts +++ b/packages/snap/src/core/services/analytics/AnalyticsService.ts @@ -4,9 +4,12 @@ import { assert } from '@metamask/superstruct'; import type { SolanaKeyringAccount } from '../../../entities'; import type { Network, TransactionMetadata } from '../../constants/solana'; +import type { + ScanStatus, + SecurityAlertResponse, +} from '../transaction-scan/types'; import logger from '../../utils/logger'; import { Base64Struct } from '../../validation/structs'; -import { ScanStatus, SecurityAlertResponse } from '../transaction-scan/types'; /** * Service for tracking events related to transactions. @@ -174,9 +177,9 @@ export class AnalyticsService { this.#logger.log( `[📣 AnalyticsService] Tracking event security alert detected`, ); - + assert(base64EncodedTransaction, Base64Struct); - + await snap.request({ method: 'snap_trackEvent', params: { @@ -197,7 +200,7 @@ export class AnalyticsService { }, }); } - + async trackEventSecurityScanCompleted( account: SolanaKeyringAccount, base64EncodedTransaction: string, @@ -209,9 +212,9 @@ export class AnalyticsService { this.#logger.log( `[📣 AnalyticsService] Tracking event security scan completed`, ); - + assert(base64EncodedTransaction, Base64Struct); - + await snap.request({ method: 'snap_trackEvent', params: { diff --git a/packages/snap/src/core/services/transaction-scan/TransactionScan.test.ts b/packages/snap/src/core/services/transaction-scan/TransactionScan.test.ts index 59a713086..388626370 100644 --- a/packages/snap/src/core/services/transaction-scan/TransactionScan.test.ts +++ b/packages/snap/src/core/services/transaction-scan/TransactionScan.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import type { SecurityAlertsApiClient } from '../../clients/security-alerts-api/SecurityAlertsApiClient'; import type { SecurityAlertSimulationValidationResponse } from '../../clients/security-alerts-api/types'; import { Network } from '../../constants/solana'; diff --git a/packages/snap/src/core/services/transaction-scan/TransactionScan.ts b/packages/snap/src/core/services/transaction-scan/TransactionScan.ts index 0a8d6b0ed..50d296b70 100644 --- a/packages/snap/src/core/services/transaction-scan/TransactionScan.ts +++ b/packages/snap/src/core/services/transaction-scan/TransactionScan.ts @@ -1,11 +1,12 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { SolanaKeyringAccount } from '../../../entities'; import type { SecurityAlertsApiClient } from '../../clients/security-alerts-api/SecurityAlertsApiClient'; import type { SecurityAlertSimulationValidationResponse } from '../../clients/security-alerts-api/types'; import type { Network } from '../../constants/solana'; import type { ILogger } from '../../utils/logger'; +import type { AnalyticsService } from '../analytics/AnalyticsService'; import type { TokenMetadataService } from '../token-metadata/TokenMetadata'; import type { TransactionScanResult, TransactionScanValidation } from './types'; -import type { AnalyticsService } from '../analytics/AnalyticsService'; -import type { SolanaKeyringAccount } from '../../../entities'; import { ScanStatus, SecurityAlertResponse } from './types'; const ICON_SIZE = 16; @@ -148,19 +149,32 @@ export class TransactionScanService { #getSecurityAlertDescription(validation: TransactionScanValidation): string { // Reference: https://docs.blockaid.io/reference/response-reference-solana const reasonDescriptions: Record = { - unfair_trade: 'Unfair trade of assets, without adequate compensation to the owner\'s account', - transfer_farming: 'Substantial transfer of the account\'s assets to untrusted entities', - writable_accounts_farming: 'Transaction exposes unused writable account, can be utilized in BIT-FLIP attacks patterns', - native_ownership_change: 'The account transferred ownership of its native SOL to untrusted entities', - spl_token_ownership_change: 'The account transferred ownership of its SPL tokens to untrusted entities', - exposure_farming: 'The account delegates ownership, thereby exposing its assets to untrusted spenders', - known_attacker: 'A known attacker\'s account is involved in the transaction', - invalid_signature: 'One of the transactions provided contains non valid signatures, that can lead misleading simulation results', - honeypot: 'The account invests funds in a token that is part of an orchestrated honeypot scheme', - other: 'The transaction was marked as malicious for other reason, further details would be described in features field', + unfair_trade: + "Unfair trade of assets, without adequate compensation to the owner's account", + transfer_farming: + "Substantial transfer of the account's assets to untrusted entities", + writable_accounts_farming: + 'Transaction exposes unused writable account, can be utilized in BIT-FLIP attacks patterns', + native_ownership_change: + 'The account transferred ownership of its native SOL to untrusted entities', + spl_token_ownership_change: + 'The account transferred ownership of its SPL tokens to untrusted entities', + exposure_farming: + 'The account delegates ownership, thereby exposing its assets to untrusted spenders', + known_attacker: + "A known attacker's account is involved in the transaction", + invalid_signature: + 'One of the transactions provided contains non valid signatures, that can lead misleading simulation results', + honeypot: + 'The account invests funds in a token that is part of an orchestrated honeypot scheme', + other: + 'The transaction was marked as malicious for other reason, further details would be described in features field', }; - - return reasonDescriptions[validation.reason] || `Security alert: ${validation.reason}`; + + return ( + reasonDescriptions[validation.reason] ?? + `Security alert: ${validation.reason}` + ); } #mapScan( From 2358c1a37950e7435ea820a0246af9e3c35ebcee Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Tue, 1 Jul 2025 15:46:07 +0100 Subject: [PATCH 05/15] chore: clean up --- .../services/analytics/AnalyticsService.ts | 4 +- .../transaction-scan/TransactionScan.test.ts | 88 +++++++++---------- 2 files changed, 44 insertions(+), 48 deletions(-) diff --git a/packages/snap/src/core/services/analytics/AnalyticsService.ts b/packages/snap/src/core/services/analytics/AnalyticsService.ts index f093b6155..1f1a69281 100644 --- a/packages/snap/src/core/services/analytics/AnalyticsService.ts +++ b/packages/snap/src/core/services/analytics/AnalyticsService.ts @@ -4,12 +4,12 @@ import { assert } from '@metamask/superstruct'; import type { SolanaKeyringAccount } from '../../../entities'; import type { Network, TransactionMetadata } from '../../constants/solana'; +import logger from '../../utils/logger'; +import { Base64Struct } from '../../validation/structs'; import type { ScanStatus, SecurityAlertResponse, } from '../transaction-scan/types'; -import logger from '../../utils/logger'; -import { Base64Struct } from '../../validation/structs'; /** * Service for tracking events related to transactions. diff --git a/packages/snap/src/core/services/transaction-scan/TransactionScan.test.ts b/packages/snap/src/core/services/transaction-scan/TransactionScan.test.ts index 388626370..d34182627 100644 --- a/packages/snap/src/core/services/transaction-scan/TransactionScan.test.ts +++ b/packages/snap/src/core/services/transaction-scan/TransactionScan.test.ts @@ -253,7 +253,7 @@ describe('TransactionScan', () => { ); }); - it('returns correct description for known reasons', () => { + it('returns correct description for known reasons', async () => { const mockAccount = MOCK_SOLANA_KEYRING_ACCOUNT_0; jest @@ -281,31 +281,29 @@ describe('TransactionScan', () => { }, } as unknown as SecurityAlertSimulationValidationResponse); - return transactionScanService - .scanTransaction({ - method: 'method', - accountAddress: 'accountAddress', - transaction: 'transaction', - scope: Network.Mainnet, - origin: 'https://metamask.io', - account: mockAccount, - }) - .then(() => { - expect( - mockAnalyticsService.trackEventSecurityAlertDetected, - ).toHaveBeenCalledWith( - mockAccount, - 'transaction', - 'https://metamask.io', - Network.Mainnet, - SecurityAlertResponse.Warning, - 'transfer_farming', - "Substantial transfer of the account's assets to untrusted entities", - ); - }); + await transactionScanService.scanTransaction({ + method: 'method', + accountAddress: 'accountAddress', + transaction: 'transaction', + scope: Network.Mainnet, + origin: 'https://metamask.io', + account: mockAccount, + }); + + expect( + mockAnalyticsService.trackEventSecurityAlertDetected, + ).toHaveBeenCalledWith( + mockAccount, + 'transaction', + 'https://metamask.io', + Network.Mainnet, + SecurityAlertResponse.Warning, + 'transfer_farming', + "Substantial transfer of the account's assets to untrusted entities", + ); }); - it('returns fallback description for unknown reasons', () => { + it('returns fallback description for unknown reasons', async () => { const mockAccount = MOCK_SOLANA_KEYRING_ACCOUNT_0; jest @@ -333,28 +331,26 @@ describe('TransactionScan', () => { }, } as unknown as SecurityAlertSimulationValidationResponse); - return transactionScanService - .scanTransaction({ - method: 'method', - accountAddress: 'accountAddress', - transaction: 'transaction', - scope: Network.Mainnet, - origin: 'https://metamask.io', - account: mockAccount, - }) - .then(() => { - expect( - mockAnalyticsService.trackEventSecurityAlertDetected, - ).toHaveBeenCalledWith( - mockAccount, - 'transaction', - 'https://metamask.io', - Network.Mainnet, - SecurityAlertResponse.Warning, - 'unknown_reason', - 'Security alert: unknown_reason', - ); - }); + await transactionScanService.scanTransaction({ + method: 'method', + accountAddress: 'accountAddress', + transaction: 'transaction', + scope: Network.Mainnet, + origin: 'https://metamask.io', + account: mockAccount, + }); + + expect( + mockAnalyticsService.trackEventSecurityAlertDetected, + ).toHaveBeenCalledWith( + mockAccount, + 'transaction', + 'https://metamask.io', + Network.Mainnet, + SecurityAlertResponse.Warning, + 'unknown_reason', + 'Security alert: unknown_reason', + ); }); }); }); From dcf57d287fcebec8bc81c00dd6f81484207f0b1e Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Tue, 1 Jul 2025 16:05:35 +0100 Subject: [PATCH 06/15] chore: passing missing param --- .../confirmation/views/ConfirmTransactionRequest/render.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.tsx b/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.tsx index 02df2f025..7d8e3d4ba 100644 --- a/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.tsx +++ b/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.tsx @@ -10,7 +10,10 @@ import { showDialog, updateInterface, } from '../../../../core/utils/interface'; -import { extractInstructionsFromUnknownBase64String } from '../../../../entities'; +import { + extractInstructionsFromUnknownBase64String, + SolanaKeyringAccount, +} from '../../../../entities'; import { connection, priceApiClient, @@ -181,6 +184,7 @@ export async function render( scope: updatedContext2.scope, origin: updatedContext2.origin, options, + account: updatedContext2.account as SolanaKeyringAccount, }) .then(async (scan) => { updatedContext2.scan = scan; From db4e8bc75e0d6bf3ebcf2ae0de15c78f57d1fb5b Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Tue, 1 Jul 2025 16:19:43 +0100 Subject: [PATCH 07/15] chore: parallel run --- .../transaction-scan/TransactionScan.ts | 37 +++++++++++-------- .../ConfirmTransactionRequest/render.tsx | 6 +-- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/packages/snap/src/core/services/transaction-scan/TransactionScan.ts b/packages/snap/src/core/services/transaction-scan/TransactionScan.ts index 50d296b70..acde8bff0 100644 --- a/packages/snap/src/core/services/transaction-scan/TransactionScan.ts +++ b/packages/snap/src/core/services/transaction-scan/TransactionScan.ts @@ -75,27 +75,34 @@ export class TransactionScanService { // The security scan is completed if (account) { - await this.#analyticsService.trackEventSecurityScanCompleted( - account, - transaction, - origin, - scope, - scan?.status as ScanStatus, - scan?.validation?.type !== SecurityAlertResponse.Benign, - ); - - // And the alert is detected - if (scan?.validation?.type !== SecurityAlertResponse.Benign) { - await this.#analyticsService.trackEventSecurityAlertDetected( + const analyticsPromises = [ + this.#analyticsService.trackEventSecurityScanCompleted( account, transaction, origin, scope, - scan?.validation?.type as SecurityAlertResponse, - scan?.validation?.reason, - this.#getSecurityAlertDescription(scan?.validation), + scan?.status as ScanStatus, + scan?.validation?.type !== SecurityAlertResponse.Benign, + ), + ]; + + // And the alert is detected + if (scan?.validation?.type !== SecurityAlertResponse.Benign) { + analyticsPromises.push( + this.#analyticsService.trackEventSecurityAlertDetected( + account, + transaction, + origin, + scope, + scan?.validation?.type as SecurityAlertResponse, + scan?.validation?.reason, + this.#getSecurityAlertDescription(scan?.validation), + ), ); } + + // Run all analytics calls in parallel + await Promise.all(analyticsPromises); } if (!scan?.estimatedChanges?.assets) { diff --git a/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.tsx b/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.tsx index 7d8e3d4ba..4326f89d3 100644 --- a/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.tsx +++ b/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.tsx @@ -10,10 +10,8 @@ import { showDialog, updateInterface, } from '../../../../core/utils/interface'; -import { - extractInstructionsFromUnknownBase64String, - SolanaKeyringAccount, -} from '../../../../entities'; +import type { SolanaKeyringAccount } from '../../../../entities'; +import { extractInstructionsFromUnknownBase64String } from '../../../../entities'; import { connection, priceApiClient, From 1c7e1620e30985a312ef602c14cd8b0c0df9793f Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Tue, 1 Jul 2025 16:31:23 +0100 Subject: [PATCH 08/15] chore: update --- .../transaction-scan/TransactionScan.test.ts | 96 +++++++------------ .../ConfirmTransactionRequest/render.test.tsx | 7 +- 2 files changed, 38 insertions(+), 65 deletions(-) diff --git a/packages/snap/src/core/services/transaction-scan/TransactionScan.test.ts b/packages/snap/src/core/services/transaction-scan/TransactionScan.test.ts index d34182627..94e326716 100644 --- a/packages/snap/src/core/services/transaction-scan/TransactionScan.test.ts +++ b/packages/snap/src/core/services/transaction-scan/TransactionScan.test.ts @@ -10,39 +10,39 @@ import { TransactionScanService } from './TransactionScan'; import { ScanStatus, SecurityAlertResponse } from './types'; describe('TransactionScan', () => { - describe('scanTransaction', () => { - let transactionScanService: TransactionScanService; - let mockSecurityAlertsApiClient: SecurityAlertsApiClient; - let mockLogger: ILogger; - let mockTokenMetadataService: TokenMetadataService; - let mockAnalyticsService: AnalyticsService; - - beforeEach(() => { - mockTokenMetadataService = { - generateImageComponent: jest.fn().mockResolvedValue(null), - } as unknown as TokenMetadataService; - - mockSecurityAlertsApiClient = { - scanTransactions: jest.fn().mockResolvedValue({}), - } as unknown as SecurityAlertsApiClient; - - mockLogger = { - error: jest.fn(), - } as unknown as ILogger; - - mockAnalyticsService = { - trackEventSecurityScanCompleted: jest.fn().mockResolvedValue(undefined), - trackEventSecurityAlertDetected: jest.fn().mockResolvedValue(undefined), - } as unknown as AnalyticsService; - - transactionScanService = new TransactionScanService( - mockSecurityAlertsApiClient, - mockTokenMetadataService, - mockAnalyticsService, - mockLogger, - ); - }); + let transactionScanService: TransactionScanService; + let mockSecurityAlertsApiClient: SecurityAlertsApiClient; + let mockLogger: ILogger; + let mockTokenMetadataService: TokenMetadataService; + let mockAnalyticsService: AnalyticsService; + + beforeEach(() => { + mockTokenMetadataService = { + generateImageComponent: jest.fn().mockResolvedValue(null), + } as unknown as TokenMetadataService; + + mockSecurityAlertsApiClient = { + scanTransactions: jest.fn().mockResolvedValue({}), + } as unknown as SecurityAlertsApiClient; + + mockLogger = { + error: jest.fn(), + } as unknown as ILogger; + + mockAnalyticsService = { + trackEventSecurityScanCompleted: jest.fn().mockResolvedValue(undefined), + trackEventSecurityAlertDetected: jest.fn().mockResolvedValue(undefined), + } as unknown as AnalyticsService; + + transactionScanService = new TransactionScanService( + mockSecurityAlertsApiClient, + mockTokenMetadataService, + mockAnalyticsService, + mockLogger, + ); + }); + describe('scanTransaction', () => { it('scans a transaction', async () => { jest .spyOn(mockSecurityAlertsApiClient, 'scanTransactions') @@ -221,38 +221,6 @@ describe('TransactionScan', () => { }); describe('getSecurityAlertDescription', () => { - let transactionScanService: TransactionScanService; - let mockSecurityAlertsApiClient: SecurityAlertsApiClient; - let mockLogger: ILogger; - let mockTokenMetadataService: TokenMetadataService; - let mockAnalyticsService: AnalyticsService; - - beforeEach(() => { - mockTokenMetadataService = { - generateImageComponent: jest.fn().mockResolvedValue(null), - } as unknown as TokenMetadataService; - - mockSecurityAlertsApiClient = { - scanTransactions: jest.fn().mockResolvedValue({}), - } as unknown as SecurityAlertsApiClient; - - mockLogger = { - error: jest.fn(), - } as unknown as ILogger; - - mockAnalyticsService = { - trackEventSecurityScanCompleted: jest.fn().mockResolvedValue(undefined), - trackEventSecurityAlertDetected: jest.fn().mockResolvedValue(undefined), - } as unknown as AnalyticsService; - - transactionScanService = new TransactionScanService( - mockSecurityAlertsApiClient, - mockTokenMetadataService, - mockAnalyticsService, - mockLogger, - ); - }); - it('returns correct description for known reasons', async () => { const mockAccount = MOCK_SOLANA_KEYRING_ACCOUNT_0; diff --git a/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.test.tsx b/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.test.tsx index 867d5456f..235d30098 100644 --- a/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.test.tsx +++ b/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.test.tsx @@ -158,8 +158,13 @@ describe('render', () => { const screen1 = await (response as any).getInterface(); + const expectedContext = { + ...mockConfirmationContext, + preferences: mockPreferences, + }; + expect(screen1).toRender( - , + , ); }); }); From 0ceb0f90d36360ba8553dcde11c30a61b6a9995e Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Tue, 1 Jul 2025 16:58:19 +0100 Subject: [PATCH 09/15] chore: update --- .../src/core/services/mocks/scanResponses.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/packages/snap/src/core/services/mocks/scanResponses.ts b/packages/snap/src/core/services/mocks/scanResponses.ts index baa1cc4cb..eb3e45cbc 100644 --- a/packages/snap/src/core/services/mocks/scanResponses.ts +++ b/packages/snap/src/core/services/mocks/scanResponses.ts @@ -9,6 +9,59 @@ export const MOCK_SECURITY_ALERTS_API_SCAN_TRANSACTIONS_RESPONSE = { result: { simulation: { assets_diff: { + BLw3RweJmfbTapJRgnPRvd962YDjFYAnVGd1p5hmZ5tP: [ + { + asset_type: 'SOL', + asset: { + type: 'SOL', + decimals: 9, + logo: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png', + }, + in: null, + out: { + usd_price: 0.37655198219999997, + summary: 'Lost approximately 0.38$', + value: 0.002160615, + raw_value: 2160615, + }, + }, + { + asset_type: 'TOKEN', + asset: { + address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + symbol: 'USDC', + name: 'USD Coin', + logo: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png', + type: 'TOKEN', + decimals: 6, + }, + in: null, + out: { + usd_price: 0.9998669999999998, + summary: 'Lost approximately 1.0$', + value: 1, + raw_value: 1000000, + }, + }, + { + asset_type: 'TOKEN', + asset: { + address: 'HaMv3cdfDW6357yjpDur6kb6w52BUPJrMJpR76tjpump', + symbol: 'COBIE', + name: 'COBIE THE BUILDER', + logo: 'https://ipfs.io/ipfs/QmdWCVHoMvtDCAbgajnzAP7ZBNeBo9RgtkbqE4rJZWGYGq', + type: 'TOKEN', + decimals: 6, + }, + in: { + usd_price: null, + summary: 'Gained approximately 202344.646927', + value: 202344.646927, + raw_value: 202344646927, + }, + out: null, + }, + ], '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P': [ { asset_type: 'SOL', From 1de93bbabcbe32f1e06f1f9ad55b37aa9d24c832 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Tue, 1 Jul 2025 17:04:38 +0100 Subject: [PATCH 10/15] chore: clean up --- .../src/core/services/mocks/scanResponses.ts | 53 ------------------- .../ConfirmTransactionRequest/render.test.tsx | 7 +-- .../ConfirmTransactionRequest/render.tsx | 2 - 3 files changed, 1 insertion(+), 61 deletions(-) diff --git a/packages/snap/src/core/services/mocks/scanResponses.ts b/packages/snap/src/core/services/mocks/scanResponses.ts index eb3e45cbc..baa1cc4cb 100644 --- a/packages/snap/src/core/services/mocks/scanResponses.ts +++ b/packages/snap/src/core/services/mocks/scanResponses.ts @@ -9,59 +9,6 @@ export const MOCK_SECURITY_ALERTS_API_SCAN_TRANSACTIONS_RESPONSE = { result: { simulation: { assets_diff: { - BLw3RweJmfbTapJRgnPRvd962YDjFYAnVGd1p5hmZ5tP: [ - { - asset_type: 'SOL', - asset: { - type: 'SOL', - decimals: 9, - logo: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png', - }, - in: null, - out: { - usd_price: 0.37655198219999997, - summary: 'Lost approximately 0.38$', - value: 0.002160615, - raw_value: 2160615, - }, - }, - { - asset_type: 'TOKEN', - asset: { - address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', - symbol: 'USDC', - name: 'USD Coin', - logo: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png', - type: 'TOKEN', - decimals: 6, - }, - in: null, - out: { - usd_price: 0.9998669999999998, - summary: 'Lost approximately 1.0$', - value: 1, - raw_value: 1000000, - }, - }, - { - asset_type: 'TOKEN', - asset: { - address: 'HaMv3cdfDW6357yjpDur6kb6w52BUPJrMJpR76tjpump', - symbol: 'COBIE', - name: 'COBIE THE BUILDER', - logo: 'https://ipfs.io/ipfs/QmdWCVHoMvtDCAbgajnzAP7ZBNeBo9RgtkbqE4rJZWGYGq', - type: 'TOKEN', - decimals: 6, - }, - in: { - usd_price: null, - summary: 'Gained approximately 202344.646927', - value: 202344.646927, - raw_value: 202344646927, - }, - out: null, - }, - ], '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P': [ { asset_type: 'SOL', diff --git a/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.test.tsx b/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.test.tsx index 235d30098..867d5456f 100644 --- a/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.test.tsx +++ b/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.test.tsx @@ -158,13 +158,8 @@ describe('render', () => { const screen1 = await (response as any).getInterface(); - const expectedContext = { - ...mockConfirmationContext, - preferences: mockPreferences, - }; - expect(screen1).toRender( - , + , ); }); }); diff --git a/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.tsx b/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.tsx index 4326f89d3..02df2f025 100644 --- a/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.tsx +++ b/packages/snap/src/features/confirmation/views/ConfirmTransactionRequest/render.tsx @@ -10,7 +10,6 @@ import { showDialog, updateInterface, } from '../../../../core/utils/interface'; -import type { SolanaKeyringAccount } from '../../../../entities'; import { extractInstructionsFromUnknownBase64String } from '../../../../entities'; import { connection, @@ -182,7 +181,6 @@ export async function render( scope: updatedContext2.scope, origin: updatedContext2.origin, options, - account: updatedContext2.account as SolanaKeyringAccount, }) .then(async (scan) => { updatedContext2.scan = scan; From ed87bb59f170667203759da3325a220011db7f88 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Wed, 2 Jul 2025 09:49:25 +0100 Subject: [PATCH 11/15] chore: review updaate --- .../transaction-scan/TransactionScan.ts | 50 +++++++++++++++---- .../core/services/transaction-scan/types.ts | 4 +- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/packages/snap/src/core/services/transaction-scan/TransactionScan.ts b/packages/snap/src/core/services/transaction-scan/TransactionScan.ts index acde8bff0..c64ee53ac 100644 --- a/packages/snap/src/core/services/transaction-scan/TransactionScan.ts +++ b/packages/snap/src/core/services/transaction-scan/TransactionScan.ts @@ -73,6 +73,24 @@ export class TransactionScanService { const scan = this.#mapScan(result); + // Validate scan object before proceeding + if (!scan || !scan.status) { + this.#logger.warn('Invalid scan result received from security alerts API'); + + if (account) { + await this.#analyticsService.trackEventSecurityScanCompleted( + account, + transaction, + origin, + scope, + ScanStatus.ERROR, + false, + ); + } + + return null; + } + // The security scan is completed if (account) { const analyticsPromises = [ @@ -81,22 +99,22 @@ export class TransactionScanService { transaction, origin, scope, - scan?.status as ScanStatus, - scan?.validation?.type !== SecurityAlertResponse.Benign, + scan.status as ScanStatus, + scan.validation?.type !== SecurityAlertResponse.Benign, ), ]; - // And the alert is detected - if (scan?.validation?.type !== SecurityAlertResponse.Benign) { + // And the alert is detected - only if validation exists and is not benign + if (scan.validation?.type && scan.validation.type !== SecurityAlertResponse.Benign) { analyticsPromises.push( this.#analyticsService.trackEventSecurityAlertDetected( account, transaction, origin, scope, - scan?.validation?.type as SecurityAlertResponse, - scan?.validation?.reason, - this.#getSecurityAlertDescription(scan?.validation), + scan.validation.type as SecurityAlertResponse, + scan.validation.reason || 'unknown', + this.#getSecurityAlertDescription(scan.validation), ), ); } @@ -154,6 +172,11 @@ export class TransactionScanService { } #getSecurityAlertDescription(validation: TransactionScanValidation): string { + // Add null/undefined check for validation and reason + if (!validation || !validation.reason) { + return 'Security alert: Unknown reason'; + } + // Reference: https://docs.blockaid.io/reference/response-reference-solana const reasonDescriptions: Record = { unfair_trade: @@ -186,9 +209,14 @@ export class TransactionScanService { #mapScan( result: SecurityAlertSimulationValidationResponse, - ): TransactionScanResult { + ): TransactionScanResult | null { + // Validate that we have a basic result structure + if (!result) { + return null; + } + return { - status: result?.status, + status: result.status || 'ERROR', estimatedChanges: { assets: result.result?.simulation?.account_summary?.account_assets_diff?.map( @@ -205,8 +233,8 @@ export class TransactionScanService { ) ?? [], }, validation: { - type: result.result?.validation?.result_type, - reason: result.result?.validation?.reason, + type: result.result?.validation?.result_type || null, + reason: result.result?.validation?.reason || null, }, error: result?.error_details ? { diff --git a/packages/snap/src/core/services/transaction-scan/types.ts b/packages/snap/src/core/services/transaction-scan/types.ts index 6a50d21b5..95191de92 100644 --- a/packages/snap/src/core/services/transaction-scan/types.ts +++ b/packages/snap/src/core/services/transaction-scan/types.ts @@ -17,8 +17,8 @@ export type TransactionScanEstimatedChanges = { }; export type TransactionScanValidation = { - type: SecurityAlertSimulationValidationResponse['result']['validation']['result_type']; - reason: SecurityAlertSimulationValidationResponse['result']['validation']['reason']; + type: SecurityAlertSimulationValidationResponse['result']['validation']['result_type'] | null; + reason: SecurityAlertSimulationValidationResponse['result']['validation']['reason'] | null; }; export type TransactionScanError = { From 372231cd34a539332033605f2f5cf91b489ef4e6 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Wed, 2 Jul 2025 09:57:13 +0100 Subject: [PATCH 12/15] chore: prettier --- .../services/transaction-scan/TransactionScan.ts | 13 +++++++++---- .../src/core/services/transaction-scan/types.ts | 8 ++++++-- .../TransactionAlert/TransactionAlert.tsx | 9 +++++++-- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/snap/src/core/services/transaction-scan/TransactionScan.ts b/packages/snap/src/core/services/transaction-scan/TransactionScan.ts index c64ee53ac..c006cd1f6 100644 --- a/packages/snap/src/core/services/transaction-scan/TransactionScan.ts +++ b/packages/snap/src/core/services/transaction-scan/TransactionScan.ts @@ -75,8 +75,10 @@ export class TransactionScanService { // Validate scan object before proceeding if (!scan || !scan.status) { - this.#logger.warn('Invalid scan result received from security alerts API'); - + this.#logger.warn( + 'Invalid scan result received from security alerts API', + ); + if (account) { await this.#analyticsService.trackEventSecurityScanCompleted( account, @@ -87,7 +89,7 @@ export class TransactionScanService { false, ); } - + return null; } @@ -105,7 +107,10 @@ export class TransactionScanService { ]; // And the alert is detected - only if validation exists and is not benign - if (scan.validation?.type && scan.validation.type !== SecurityAlertResponse.Benign) { + if ( + scan.validation?.type && + scan.validation.type !== SecurityAlertResponse.Benign + ) { analyticsPromises.push( this.#analyticsService.trackEventSecurityAlertDetected( account, diff --git a/packages/snap/src/core/services/transaction-scan/types.ts b/packages/snap/src/core/services/transaction-scan/types.ts index 95191de92..76d3146a5 100644 --- a/packages/snap/src/core/services/transaction-scan/types.ts +++ b/packages/snap/src/core/services/transaction-scan/types.ts @@ -17,8 +17,12 @@ export type TransactionScanEstimatedChanges = { }; export type TransactionScanValidation = { - type: SecurityAlertSimulationValidationResponse['result']['validation']['result_type'] | null; - reason: SecurityAlertSimulationValidationResponse['result']['validation']['reason'] | null; + type: + | SecurityAlertSimulationValidationResponse['result']['validation']['result_type'] + | null; + reason: + | SecurityAlertSimulationValidationResponse['result']['validation']['reason'] + | null; }; export type TransactionScanError = { diff --git a/packages/snap/src/features/confirmation/components/TransactionAlert/TransactionAlert.tsx b/packages/snap/src/features/confirmation/components/TransactionAlert/TransactionAlert.tsx index 6c72d836c..1d39ccb41 100644 --- a/packages/snap/src/features/confirmation/components/TransactionAlert/TransactionAlert.tsx +++ b/packages/snap/src/features/confirmation/components/TransactionAlert/TransactionAlert.tsx @@ -24,7 +24,10 @@ type TransactionAlertProps = { }; const VALIDATION_TYPE_TO_SEVERITY: Partial< - Record + Record< + NonNullable, + BannerProps['severity'] + > > = { Malicious: 'danger', Warning: 'warning', @@ -84,7 +87,9 @@ export const TransactionAlert: SnapComponent = ({ return {null}; } - const severity = VALIDATION_TYPE_TO_SEVERITY[validation?.type]; + const severity = validation?.type + ? VALIDATION_TYPE_TO_SEVERITY[validation.type] + : undefined; /** * Displays a banner if the validation there is a validation. From d3cf217da1fd9bfc94e6065f11e63a90bce407c2 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Wed, 2 Jul 2025 10:03:03 +0100 Subject: [PATCH 13/15] chore: review fix --- .../transaction-scan/TransactionScan.ts | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/snap/src/core/services/transaction-scan/TransactionScan.ts b/packages/snap/src/core/services/transaction-scan/TransactionScan.ts index c006cd1f6..23bc2f69f 100644 --- a/packages/snap/src/core/services/transaction-scan/TransactionScan.ts +++ b/packages/snap/src/core/services/transaction-scan/TransactionScan.ts @@ -73,7 +73,6 @@ export class TransactionScanService { const scan = this.#mapScan(result); - // Validate scan object before proceeding if (!scan || !scan.status) { this.#logger.warn( 'Invalid scan result received from security alerts API', @@ -95,22 +94,31 @@ export class TransactionScanService { // The security scan is completed if (account) { + const isValidScanStatus = Object.values(ScanStatus).includes( + scan.status as ScanStatus, + ); + const scanStatus = isValidScanStatus + ? (scan.status as ScanStatus) + : ScanStatus.ERROR; + + const hasSecurityAlert = Boolean( + scan.validation?.type && + scan.validation.type !== SecurityAlertResponse.Benign, + ); + const analyticsPromises = [ this.#analyticsService.trackEventSecurityScanCompleted( account, transaction, origin, scope, - scan.status as ScanStatus, - scan.validation?.type !== SecurityAlertResponse.Benign, + scanStatus, + hasSecurityAlert, ), ]; // And the alert is detected - only if validation exists and is not benign - if ( - scan.validation?.type && - scan.validation.type !== SecurityAlertResponse.Benign - ) { + if (hasSecurityAlert) { analyticsPromises.push( this.#analyticsService.trackEventSecurityAlertDetected( account, @@ -221,7 +229,10 @@ export class TransactionScanService { } return { - status: result.status || 'ERROR', + status: + result.status === 'SUCCESS' || result.status === 'ERROR' + ? result.status + : 'ERROR', estimatedChanges: { assets: result.result?.simulation?.account_summary?.account_assets_diff?.map( From 644b53b220090a1ff63f230755d8db6061f02a9d Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Wed, 2 Jul 2025 10:08:06 +0100 Subject: [PATCH 14/15] chore: review fix --- .../core/services/transaction-scan/TransactionScan.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/snap/src/core/services/transaction-scan/TransactionScan.ts b/packages/snap/src/core/services/transaction-scan/TransactionScan.ts index 23bc2f69f..fb38e0e73 100644 --- a/packages/snap/src/core/services/transaction-scan/TransactionScan.ts +++ b/packages/snap/src/core/services/transaction-scan/TransactionScan.ts @@ -117,15 +117,21 @@ export class TransactionScanService { ), ]; - // And the alert is detected - only if validation exists and is not benign if (hasSecurityAlert) { + const isValidSecurityAlertType = Object.values( + SecurityAlertResponse, + ).includes(scan.validation.type as SecurityAlertResponse); + const securityAlertType = isValidSecurityAlertType + ? (scan.validation.type as SecurityAlertResponse) + : SecurityAlertResponse.Warning; + analyticsPromises.push( this.#analyticsService.trackEventSecurityAlertDetected( account, transaction, origin, scope, - scan.validation.type as SecurityAlertResponse, + securityAlertType, scan.validation.reason || 'unknown', this.#getSecurityAlertDescription(scan.validation), ), From 0eedbf7c4c939b20b51282392b68a9f6bd462705 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Wed, 2 Jul 2025 10:10:56 +0100 Subject: [PATCH 15/15] chore: lint issues --- .../core/services/transaction-scan/TransactionScan.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/snap/src/core/services/transaction-scan/TransactionScan.ts b/packages/snap/src/core/services/transaction-scan/TransactionScan.ts index fb38e0e73..ed000a792 100644 --- a/packages/snap/src/core/services/transaction-scan/TransactionScan.ts +++ b/packages/snap/src/core/services/transaction-scan/TransactionScan.ts @@ -73,7 +73,7 @@ export class TransactionScanService { const scan = this.#mapScan(result); - if (!scan || !scan.status) { + if (!scan?.status) { this.#logger.warn( 'Invalid scan result received from security alerts API', ); @@ -132,7 +132,7 @@ export class TransactionScanService { origin, scope, securityAlertType, - scan.validation.reason || 'unknown', + scan.validation.reason ?? 'unknown', this.#getSecurityAlertDescription(scan.validation), ), ); @@ -191,8 +191,7 @@ export class TransactionScanService { } #getSecurityAlertDescription(validation: TransactionScanValidation): string { - // Add null/undefined check for validation and reason - if (!validation || !validation.reason) { + if (!validation?.reason) { return 'Security alert: Unknown reason'; } @@ -255,8 +254,8 @@ export class TransactionScanService { ) ?? [], }, validation: { - type: result.result?.validation?.result_type || null, - reason: result.result?.validation?.reason || null, + type: result.result?.validation?.result_type ?? null, + reason: result.result?.validation?.reason ?? null, }, error: result?.error_details ? {