Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ node_modules/
# dotenv environment variables file
.env
.env.test
.env.development

# Stores VSCode versions used for testing VSCode extensions
.vscode-test
Expand Down
1 change: 0 additions & 1 deletion packages/site/.env.development

This file was deleted.

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": "No0nYkGzUlrCP0uk+f5GacSgu57myPr0DPSIWFhznsw=",
"shasum": "ct7vH/ez4b5r2wHjF4c999g8zHLsNUTf1CQPDGeSCVs=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
1 change: 1 addition & 0 deletions packages/snap/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ const transactionService = new TransactionService({
networkService,
transactionBuilder,
accountService,
assetMetadataService,
});

const priceService = new PriceService({
Expand Down
19 changes: 17 additions & 2 deletions packages/snap/src/handlers/clientRequest/confirmSend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,7 @@ describe('ConfirmSendHandler', () => {
origin: METAMASK_ORIGIN,
renderContext: {
account,
assetMetadata,
toAddress: destinationAddress,
amount: '1',
},
renderOptions: {
loadPrice: true,
Expand All @@ -303,6 +301,23 @@ describe('ConfirmSendHandler', () => {
accountAddress: account.address,
transaction: unsignedScanXdr,
},
initialScan: {
status: 'SUCCESS',
estimatedChanges: {
assets: [
{
type: 'out',
value: 1,
price: null,
symbol: assetMetadata.symbol,
name: assetMetadata.name,
logo: assetMetadata.iconUrl,
},
],
},
validation: null,
error: null,
},
transactionValidationRequest: {
accountId: account.id,
transaction: unsignedScanXdr,
Expand Down
38 changes: 36 additions & 2 deletions packages/snap/src/handlers/clientRequest/confirmSend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import type { ContextWithPrices } from '../../ui/confirmation/api';
import { ConfirmationInterfaceKey } from '../../ui/confirmation/api';
import {
hasDecimals,
isSlip44Id,
toSmallestUnit,
trackTransactionAdded,
trackTransactionApproved,
Expand All @@ -46,6 +47,7 @@ import type {
} from '../accountResolver';
import { BaseClientRequestHandler } from './base';
import { AccountNotActivatedException } from '../../services/network';
import type { TransactionScanEstimatedChanges } from '../../services/transaction-scan';
import type { ConfirmationUXController } from '../../ui/confirmation/controller';
import { TrackTransactionHandler } from '../cronjob/trackTransaction';

Expand Down Expand Up @@ -294,16 +296,18 @@ export class ConfirmSendHandler extends BaseClientRequestHandler<
const { request, account, assetMetadata, fee, scope, transaction } = params;
const { toAddress, amount, assetId } = request.params;
const xdr = transaction.getRaw().toXDR();
const estimatedChanges = this.#buildEstimatedChangesFallback({
amount,
assetMetadata,
});

return (
(await this.#confirmationUIController.renderConfirmationDialog({
scope,
origin: METAMASK_ORIGIN,
renderContext: {
account,
assetMetadata,
toAddress,
amount,
},
fee: fee.toString(),
interfaceKey: ConfirmationInterfaceKey.ConfirmSendTransaction,
Expand All @@ -316,6 +320,12 @@ export class ConfirmSendHandler extends BaseClientRequestHandler<
accountAddress: account.address,
transaction: xdr,
},
initialScan: {
status: 'SUCCESS',
estimatedChanges,
validation: null,
error: null,
},
transactionValidationRequest: {
accountId: account.id,
transaction: xdr,
Expand All @@ -328,6 +338,30 @@ export class ConfirmSendHandler extends BaseClientRequestHandler<
);
}

#buildEstimatedChangesFallback({
amount,
assetMetadata,
}: {
amount: string;
assetMetadata: StellarAssetMetadata;
}): TransactionScanEstimatedChanges {
const { assetId, symbol, iconUrl, name } = assetMetadata;
const logo = isSlip44Id(assetId) ? null : (iconUrl ?? null);

return {
assets: [
{
type: 'out',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can we have a enum for out

value: Number(amount),
price: null,
symbol,
name: name ?? symbol,
logo,
},
],
};
}

/**
* Override the base handler to return invalid when the account is not activated.
* Instead of showing the account not activated alert, it returns an invalid response.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,18 @@ describe('ConfirmationScanRefresher', () => {
};
const scanResult = {
status: 'SUCCESS' as const,
estimatedChanges: { assets: [] },
estimatedChanges: {
assets: [
{
type: 'out' as const,
value: 2,
price: null,
symbol: 'USDC',
name: 'USD Coin',
logo: null,
},
],
},
validation: {
type: TransactionScanValidationType.Benign,
reason: null,
Expand Down Expand Up @@ -63,7 +74,7 @@ describe('ConfirmationScanRefresher', () => {
});
}

it('includes simulation and validation for sign-transaction', async () => {
it('requests simulation and validation for sign transaction when both scan preferences are enabled', async () => {
const { refresher, transactionScanService } = setup();

const result = await refresher.refresh(createScanContext());
Expand All @@ -85,63 +96,200 @@ describe('ConfirmationScanRefresher', () => {
});

it.each([
ConfirmationInterfaceKey.SignTransaction,
ConfirmationInterfaceKey.ConfirmSendTransaction,
ConfirmationInterfaceKey.ChangeTrustlineOptIn,
ConfirmationInterfaceKey.ChangeTrustlineOptOut,
])(
'skips remote simulation for %s even when simulateOnChainActions is enabled',
'requests simulation for %s when estimated changes are enabled',
async (interfaceKey) => {
const { refresher, transactionScanService } = setup();

await refresher.refresh(createScanContext({ interfaceKey }));
await refresher.refresh(
createScanContext({
interfaceKey,
preferences: {
useSecurityAlerts: false,
simulateOnChainActions: true,
},
}),
);

expect(transactionScanService.scanTransaction).toHaveBeenCalledWith({
...securityScanRequest,
options: [TransactionScanOption.Validation],
options: [TransactionScanOption.Simulation],
});
},
);

it('omits simulation for sign-transaction when simulateOnChainActions is disabled', async () => {
it.each([
ConfirmationInterfaceKey.ChangeTrustlineOptIn,
ConfirmationInterfaceKey.ChangeTrustlineOptOut,
])('does not request simulation for %s', async (interfaceKey) => {
const { refresher, transactionScanService } = setup();

await refresher.refresh(
await refresher.refresh(createScanContext({ interfaceKey }));

expect(transactionScanService.scanTransaction).toHaveBeenCalledWith({
...securityScanRequest,
options: [TransactionScanOption.Validation],
});
});

it('uses Blockaid estimated changes when remote simulation returns asset rows', async () => {
const { refresher } = setup();
const localEstimatedChanges = {
assets: [
{
type: 'out' as const,
value: 1,
price: null,
symbol: 'XLM',
name: 'Stellar Lumens',
logo: null,
},
],
};

const result = await refresher.refresh(
createScanContext({
preferences: {
useSecurityAlerts: true,
simulateOnChainActions: false,
scan: {
status: 'SUCCESS',
estimatedChanges: localEstimatedChanges,
validation: null,
error: null,
},
}),
);

expect(transactionScanService.scanTransaction).toHaveBeenCalledWith({
...securityScanRequest,
options: [TransactionScanOption.Validation],
expect(result).toStrictEqual({
result: {
scan: scanResult,
scanFetchStatus: FetchStatus.Fetched,
},
reschedule: true,
});
});

it('falls back to locally-derived estimated changes when Blockaid returns no asset rows', async () => {
const { refresher, transactionScanService } = setup();
const localEstimatedChanges = {
assets: [
{
type: 'out' as const,
value: 12.5,
price: null,
symbol: 'XLM',
name: 'Stellar Lumens',
logo: null,
},
],
};
const emptyRemoteScan = {
...scanResult,
estimatedChanges: { assets: [] },
};
transactionScanService.scanTransaction.mockResolvedValueOnce(
emptyRemoteScan,
);

const result = await refresher.refresh(
createScanContext({
scan: {
status: 'SUCCESS',
estimatedChanges: localEstimatedChanges,
validation: null,
error: null,
},
}),
);

expect(result).toStrictEqual({
result: {
scan: {
...emptyRemoteScan,
estimatedChanges: localEstimatedChanges,
},
scanFetchStatus: FetchStatus.Fetched,
},
reschedule: true,
});
});

it('returns error status when scan returns null', async () => {
it('returns error status preserving estimated changes when scan returns null', async () => {
const { refresher, transactionScanService } = setup();
transactionScanService.scanTransaction.mockResolvedValueOnce(null);
const localEstimatedChanges = {
assets: [
{
type: 'out' as const,
value: 1,
price: null,
symbol: 'XLM',
name: 'Stellar Lumens',
logo: null,
},
],
};

const result = await refresher.refresh(
createScanContext({
preferences: {
useSecurityAlerts: true,
simulateOnChainActions: false,
},
scan: {
status: 'SUCCESS',
estimatedChanges: localEstimatedChanges,
validation: null,
error: null,
},
}),
);

expect(result).toStrictEqual({
result: {
scan: null,
scan: {
status: 'ERROR',
estimatedChanges: localEstimatedChanges,
validation: null,
error: null,
},
scanFetchStatus: FetchStatus.Error,
},
reschedule: false,
});
});

it('fetches when only simulateOnChainActions is enabled for sign transaction', () => {
const { refresher } = setup();

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

it('does not fetch when only simulateOnChainActions is enabled for change trust', () => {
const { refresher } = setup();

expect(
refresher.shouldFetch(
createScanContext({
interfaceKey: ConfirmationInterfaceKey.ChangeTrustlineOptIn,
preferences: {
useSecurityAlerts: false,
simulateOnChainActions: true,
},
}),
),
).toBe(false);
});

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

Expand Down
Loading
Loading