Skip to content
Open
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
12 changes: 12 additions & 0 deletions packages/snap/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,18 @@
"confirmation.validationWarningSubtitle": {
"message": "Security Alerts found potential risk. Only continue if you trust this site and every address involved."
},
"confirmation.tokenScanMaliciousTitle": {
"message": "This asset may be malicious"
},
"confirmation.tokenScanMaliciousSubtitle": {
"message": "Security Alerts found risk for {asset}. Only continue if you trust this asset."
},
"confirmation.tokenScanWarningTitle": {
"message": "This asset may be risky"
},
"confirmation.tokenScanWarningSubtitle": {
"message": "Security Alerts found potential risk for {asset}. Only continue if you trust this asset."
},
"confirmation.validationErrorLearnMore": {
"message": "Learn more"
},
Expand Down
12 changes: 12 additions & 0 deletions packages/snap/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,18 @@
"confirmation.validationWarningSubtitle": {
"message": "Security Alerts found potential risk. Only continue if you trust this site and every address involved."
},
"confirmation.tokenScanMaliciousTitle": {
"message": "This asset may be malicious"
},
"confirmation.tokenScanMaliciousSubtitle": {
"message": "Security Alerts found risk for {asset}. Only continue if you trust this asset."
},
"confirmation.tokenScanWarningTitle": {
"message": "This asset may be risky"
},
"confirmation.tokenScanWarningSubtitle": {
"message": "Security Alerts found potential risk for {asset}. Only continue if you trust this asset."
},
"confirmation.validationErrorLearnMore": {
"message": "Learn more"
},
Expand Down
12 changes: 12 additions & 0 deletions packages/snap/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,18 @@
"confirmation.validationWarningSubtitle": {
"message": "Security Alerts found potential risk. Only continue if you trust this site and every address involved."
},
"confirmation.tokenScanMaliciousTitle": {
"message": "This asset may be malicious"
},
"confirmation.tokenScanMaliciousSubtitle": {
"message": "Security Alerts found risk for {asset}. Only continue if you trust this asset."
},
"confirmation.tokenScanWarningTitle": {
"message": "This asset may be risky"
},
"confirmation.tokenScanWarningSubtitle": {
"message": "Security Alerts found potential risk for {asset}. Only continue if you trust this asset."
},
"confirmation.validationErrorLearnMore": {
"message": "Learn more"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snap-stellar-wallet.git"
},
"source": {
"shasum": "+11EcYy1qTviJ3cGJ/8lcvNEVnSaoYVkEb9NKcqpCJ0=",
"shasum": "j03va6Qf2cKvp/SDabpqW5ZSUdJ4/OnFKCwgSmg0baY=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
7 changes: 7 additions & 0 deletions packages/snap/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { BackgroundEventMethod } from './handlers/cronjob/api';
import {
ConfirmationPriceRefresher,
ConfirmationScanRefresher,
ConfirmationTokenScanRefresher,
ConfirmationTransactionRefresher,
RefreshConfirmationContextHandler,
} from './handlers/cronjob/refreshConfirmationContext';
Expand Down Expand Up @@ -188,6 +189,11 @@ const confirmationScanRefresher = new ConfirmationScanRefresher({
transactionScanService,
});

const confirmationTokenScanRefresher = new ConfirmationTokenScanRefresher({
logger,
transactionScanService,
});

const confirmationTransactionRefresher = new ConfirmationTransactionRefresher({
logger,
transactionService,
Expand All @@ -202,6 +208,7 @@ const refreshConfirmationContextHandler = new RefreshConfirmationContextHandler(
refreshers: [
confirmationPriceRefresher,
confirmationScanRefresher,
confirmationTokenScanRefresher,
confirmationTransactionRefresher,
],
},
Expand Down
10 changes: 10 additions & 0 deletions packages/snap/src/handlers/clientRequest/changeTrustOpt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ describe('ChangeTrustOptHandler', () => {
},
renderOptions: {
loadPrice: true,
scanToken: true,
scanTxn: true,
validateTxn: true,
},
Expand All @@ -269,6 +270,10 @@ describe('ChangeTrustOptHandler', () => {
method: ClientRequestMethod.ChangeTrustOpt,
}),
},
tokenScanRequest: {
assetReference:
'USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN',
},
}),
);
const signedTransaction = signTransactionSpy.mock.calls[0]?.[0];
Expand Down Expand Up @@ -376,6 +381,7 @@ describe('ChangeTrustOptHandler', () => {
interfaceKey: ConfirmationInterfaceKey.ChangeTrustlineOptOut,
renderOptions: {
loadPrice: true,
scanToken: true,
scanTxn: true,
validateTxn: true,
},
Expand All @@ -390,6 +396,10 @@ describe('ChangeTrustOptHandler', () => {
method: ClientRequestMethod.ChangeTrustOpt,
}),
},
tokenScanRequest: {
assetReference:
'USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN',
},
}),
);
expect(sendTransaction).toHaveBeenCalled();
Expand Down
9 changes: 7 additions & 2 deletions packages/snap/src/handlers/clientRequest/changeTrustOpt.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { UserRejectedRequestError } from '@metamask/snaps-sdk';
import { ensureError } from '@metamask/utils';
import { ensureError, parseCaipAssetType } from '@metamask/utils';

import type {
ChangeTrustOptJsonRpcRequest,
Expand Down Expand Up @@ -252,8 +252,9 @@ export class ChangeTrustOptHandler extends BaseClientRequestHandler<
transaction,
confirmationInterfaceKey,
} = params;
const { scope } = request.params;
const { assetId, scope } = request.params;
const xdr = transaction.getRaw().toXDR();
const { assetReference } = parseCaipAssetType(assetId);

return (
(await this.#confirmationUIController.renderConfirmationDialog({
Expand All @@ -267,6 +268,7 @@ export class ChangeTrustOptHandler extends BaseClientRequestHandler<
interfaceKey: confirmationInterfaceKey,
renderOptions: {
loadPrice: true,
scanToken: true,
scanTxn: true,
validateTxn: true,
},
Expand All @@ -279,6 +281,9 @@ export class ChangeTrustOptHandler extends BaseClientRequestHandler<
transaction: xdr,
request,
},
tokenScanRequest: {
assetReference,
},
})) === true
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { ContextWithPrices } from '../../../ui/confirmation/api';
export enum ConfirmationContextRefresherKey {
Prices = 'prices',
Scan = 'scan',
TokenScan = 'tokenScan',
Transaction = 'transaction',
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { RefreshConfirmationContextHandler } from './handler';
export { ConfirmationPriceRefresher } from './priceRefresher';
export { ConfirmationScanRefresher } from './scanRefresher';
export { ConfirmationTokenScanRefresher } from './tokenScanRefresher';
export { ConfirmationTransactionRefresher } from './transactionRefresher';
export {
ConfirmationContextRefresherKey,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { createConfirmationDataContext } from './__fixtures__/context.fixtures';
import { ConfirmationContextRefresherKey } from './api';
import { ConfirmationTokenScanRefresher } from './tokenScanRefresher';
import { KnownCaip2ChainId } from '../../../api';
import type { TransactionScanService } from '../../../services/transaction-scan';
import { TokenScanResultType } from '../../../services/transaction-scan';
import { FetchStatus } from '../../../ui/confirmation/api';
import { logger } from '../../../utils/logger';

describe('ConfirmationTokenScanRefresher', () => {
const scope = KnownCaip2ChainId.Mainnet;
const tokenScanRequest = {
assetReference:
'USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN',
origin: 'https://example.com',
scope,
};
const tokenScanResult = {
resultType: TokenScanResultType.Malicious,
isMalicious: true,
isWarning: false,
name: 'USD Coin',
symbol: 'USDC',
};

function setup() {
const transactionScanService: jest.Mocked<
Pick<TransactionScanService, 'scanToken'>
> = {
scanToken: jest.fn().mockResolvedValue(tokenScanResult),
};
const refresher = new ConfirmationTokenScanRefresher({
logger,
transactionScanService:
transactionScanService as unknown as TransactionScanService,
});

return { refresher, transactionScanService };
}

function createTokenScanContext(
overrides: Parameters<typeof createConfirmationDataContext>[0] = {},
) {
return createConfirmationDataContext({
preferences: {
useSecurityAlerts: true,
simulateOnChainActions: true,
},
tokenScanRequest,
tokenScan: null,
tokenScanFetchStatus: FetchStatus.Fetching,
...overrides,
});
}

it('returns fetched token scan data and reschedules on success', async () => {
const { refresher, transactionScanService } = setup();

const result = await refresher.refresh(createTokenScanContext());

expect(transactionScanService.scanToken).toHaveBeenCalledWith({
assetReference: tokenScanRequest.assetReference,
origin: tokenScanRequest.origin,
});
expect(result).toStrictEqual({
result: {
tokenScan: tokenScanResult,
tokenScanFetchStatus: FetchStatus.Fetched,
},
reschedule: true,
});
});

it('returns error status and stops rescheduling when scan returns null', async () => {
const { refresher, transactionScanService } = setup();
transactionScanService.scanToken.mockResolvedValueOnce(null);

const result = await refresher.refresh(createTokenScanContext());

expect(result).toStrictEqual({
result: {
tokenScan: null,
tokenScanFetchStatus: FetchStatus.Error,
},
reschedule: false,
});
});

it('does not fetch when Security Alerts are disabled', () => {
const { refresher } = setup();

expect(
refresher.shouldFetch(
createTokenScanContext({
preferences: {
useSecurityAlerts: false,
simulateOnChainActions: true,
},
}),
),
).toBe(false);
});

it('does not fetch when tokenScanRequest is missing', () => {
const { refresher } = setup();

expect(
refresher.shouldFetch(
createTokenScanContext({
tokenScanRequest: undefined,
}),
),
).toBe(false);
});

it('writes fetched recovery when Security Alerts are disabled', () => {
const { refresher } = setup();

expect(
refresher.recoveryResult(
createTokenScanContext({
preferences: {
useSecurityAlerts: false,
simulateOnChainActions: true,
},
}),
),
).toStrictEqual({
result: {
tokenScan: null,
tokenScanFetchStatus: FetchStatus.Fetched,
},
reschedule: false,
});
});

it('writes recovery error when token scan prefs are enabled but request is missing', () => {
const { refresher } = setup();

expect(
refresher.recoveryResult(
createTokenScanContext({
tokenScanRequest: undefined,
}),
),
).toStrictEqual({
result: {
tokenScan: null,
tokenScanFetchStatus: FetchStatus.Error,
},
reschedule: false,
});
});

it('uses the token scan refresher key', () => {
const { refresher } = setup();
expect(refresher.key).toBe(ConfirmationContextRefresherKey.TokenScan);
});
});
Loading
Loading