Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/snap/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Disable simulation for unsupported transactions ([#186](https://github.com/MetaMask/snap-tron-wallet/pull/186))
- Supported transactions are those single-contract interaction transactions of the following types: `TransferContract`, `TriggerSmartContract`, `TransferAssetContract`.

## [1.20.0]

### Added
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-tron-wallet.git"
},
"source": {
"shasum": "4ePTerkid6qLfLohShjnEntXl74FyR6QOKVMglucJdg=",
"shasum": "Izxk79sRDjnS3yUNx8TV9c9D7azOJxTwVSchsbEToXU=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
/* eslint-disable @typescript-eslint/naming-convention */

import type { Types } from 'tronweb';

import type { SecurityAlertSimulationValidationResponse } from './types';
import {
extractScanParametersFromTransactionData,
isTransactionSupported,
} from './utils';
import type { ConfigProvider } from '../../services/config';
import logger, { createPrefixedLogger, type ILogger } from '../../utils/logger';

Expand Down Expand Up @@ -36,38 +42,35 @@ export class SecurityAlertsApiClient {
*
* @param params - The parameters for the scan.
* @param params.accountAddress - The account address in base58 format.
* @param params.parameters - The parameters for the transaction.
* @param params.parameters.from - The from address.
* @param params.parameters.to - The to address.
* @param params.parameters.data - The data of the transaction.
* @param params.parameters.value - The value of the transaction.
* @param params.transactionRawData - The raw data of the transaction.
* @param params.origin - The origin URL of the request.
* @param params.options - Optional scan options (simulation, validation).
* @returns The security alert response from Security Alerts API.
*/
async scanTransaction({
accountAddress,
parameters,
transactionRawData,
origin,
options = ['simulation', 'validation'],
}: {
accountAddress: string;
parameters: {
from: string | undefined;
to: string | undefined;
data: string | undefined;
value: number | undefined | null;
};
transactionRawData: Types.Transaction['raw_data'];
origin: string;
options?: string[];
}): Promise<SecurityAlertSimulationValidationResponse> {
this.#logger.info('Scanning Tron transaction with Security Alerts API');

if (!isTransactionSupported(transactionRawData)) {
throw new Error('The transaction is not supported for scanning.');
}

const headers: Record<string, string> = {
'Content-Type': 'application/json',
accept: 'application/json',
};

const data = extractScanParametersFromTransactionData(transactionRawData);

const response = await this.#fetch(
`${this.#baseUrl}/tron/transaction/scan`,
{
Expand All @@ -78,7 +81,7 @@ export class SecurityAlertsApiClient {
metadata: {
domain: origin,
},
data: parameters,
data,
options,
}),
},
Expand Down
121 changes: 120 additions & 1 deletion packages/snap/src/clients/security-alerts-api/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { Types } from 'tronweb';

import { extractScanParametersFromTransactionData } from './utils';
import {
extractScanParametersFromTransactionData,
isTransactionSupported,
SUPPORTED_CONTRACT_TYPES,
} from './utils';
import type {
TransferAssetContractParameter,
TransferContractParameter,
Expand Down Expand Up @@ -121,4 +125,119 @@ describe('SecurityAlertsApiClient utils', () => {
expect(result).toBeNull();
});
});

describe('isTransactionSupported', () => {
it('returns false for transactions with multiple contract interactions', () => {
const rawData: Types.Transaction['raw_data'] = {
contract: [
{
type: Types.ContractType.TransferContract,
parameter: {
value: {
owner_address: '41a614f803b6fd780986a42c78ec9c7f77e6ded13c',
contract_address: '4191bba2f3f6e1c4d5c8e8f5b6a7c8d9e0f1a2b3c4',
call_value: 200000,
data: 'abcdef',
},
type_url: 'type.googleapis.com/protocol.TriggerSmartContract',
},
},
{
type: Types.ContractType.TriggerSmartContract,
parameter: {
value: {
owner_address: '41a614f803b6fd780986a42c78ec9c7f77e6ded13c',
contract_address: '4191bba2f3f6e1c4d5c8e8f5b6a7c8d9e0f1a2b3c4',
call_value: 200000,
data: 'abcdef',
},
type_url: 'type.googleapis.com/protocol.TriggerSmartContract',
},
},
],
ref_block_bytes: '',
ref_block_hash: '',
expiration: 0,
timestamp: 0,
};

const result = isTransactionSupported(rawData);

expect(result).toBe(false);
});
});

it.each([
Types.ContractType.AccountUpdateContract,
Types.ContractType.FreezeBalanceContract,
Types.ContractType.UnfreezeBalanceContract,
Types.ContractType.WithdrawBalanceContract,
Types.ContractType.UpdateAssetContract,
Types.ContractType.ParticipateAssetIssueContract,
Types.ContractType.AccountPermissionUpdateContract,
Types.ContractType.ExchangeCreateContract,
Types.ContractType.ExchangeInjectContract,
Types.ContractType.ExchangeWithdrawContract,
Types.ContractType.ExchangeTransactionContract,
])(
'returns false for transactions with unsupported contract types',
(contractType) => {
const rawData: Types.Transaction['raw_data'] = {
contract: [
{
type: contractType,
parameter: {
value: {
owner_address: '41a614f803b6fd780986a42c78ec9c7f77e6ded13c',
contract_address: '4191bba2f3f6e1c4d5c8e8f5b6a7c8d9e0f1a2b3c4',
call_value: 200000,
data: 'abcdef',
},
// This is here only to make TypeScript happy; the actual type_url is irrelevant for the test
type_url: 'type.googleapis.com/protocol.UnknownContract',
},
},
],
ref_block_bytes: '',
ref_block_hash: '',
expiration: 0,
timestamp: 0,
};

const result = isTransactionSupported(rawData);

expect(result).toBe(false);
},
);

it.each(SUPPORTED_CONTRACT_TYPES)(
'returns true for transactions with a single supported contract type',
(contractType) => {
const rawData: Types.Transaction['raw_data'] = {
contract: [
{
type: contractType,
parameter: {
value: {
owner_address: '41a614f803b6fd780986a42c78ec9c7f77e6ded13c',
contract_address: '4191bba2f3f6e1c4d5c8e8f5b6a7c8d9e0f1a2b3c4',
call_value: 200000,
data: 'abcdef',
},
// This is here only to make TypeScript happy; the actual type_url is irrelevant for the test
type_url: 'type.googleapis.com/protocol.UnknownContract',
},
},
],
ref_block_bytes: '',
ref_block_hash: '',
expiration: 0,
timestamp: 0,
};

const result = isTransactionSupported(rawData);

expect(result).toBe(true);
},
);
});
36 changes: 35 additions & 1 deletion packages/snap/src/clients/security-alerts-api/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { add0x } from '@metamask/utils';
import { TronWeb, type Types } from 'tronweb';
import { TronWeb, Types } from 'tronweb';

import type { SecurityScanPayload } from './types';

export const SUPPORTED_CONTRACT_TYPES: Types.ContractType[] = [
Types.ContractType.TransferContract,
Types.ContractType.TransferAssetContract,
Types.ContractType.TriggerSmartContract,
];

/**
* Extracts scan parameters from the raw transaction data. This function
* can be used as adapter between a Tron transaction and the payload
Expand Down Expand Up @@ -42,3 +48,31 @@ export const extractScanParametersFromTransactionData = (

return { from, to, data, value };
};

/**
* Checks if the given transaction is supported for scanning.
*
* @param rawData - The raw transaction data.
* @returns True if the transaction is supported, false otherwise.
*/
export const isTransactionSupported = (
rawData: Types.Transaction['raw_data'],
): boolean => {
if (rawData.contract.length > 1) {
// We only support transactions with a single contract interaction for now
return false;
}

const [contractInteraction] = rawData.contract;
if (!contractInteraction) {
// No contract interaction found
return false;
}

if (!SUPPORTED_CONTRACT_TYPES.includes(contractInteraction.type)) {
// Unsupported contract type
return false;
}

return true;
};
16 changes: 9 additions & 7 deletions packages/snap/src/handlers/cronjob.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { JsonRpcRequest } from '@metamask/snaps-sdk';
import { type Types, utils } from 'tronweb';

import type { PriceApiClient } from '../clients/price-api/PriceApiClient';
import type { SnapClient } from '../clients/snap/SnapClient';
Expand Down Expand Up @@ -357,7 +358,7 @@ export class CronHandler {
return;
}

const { preferences, scope, account, origin, scanParameters } =
const { preferences, scope, account, origin, transaction } =
interfaceContext;

// Determine what needs to be refreshed
Expand Down Expand Up @@ -392,15 +393,16 @@ export class CronHandler {
options.push('validation');
}

const transactionRawData: Types.Transaction['raw_data'] =
utils.deserializeTx.deserializeTransaction(
transaction.type,
transaction.rawDataHex,
);

try {
scan = await this.#transactionScanService.scanTransaction({
accountAddress: account.address,
parameters: {
from: scanParameters?.from ?? undefined,
to: scanParameters?.to ?? undefined,
data: scanParameters?.data ?? undefined,
value: scanParameters?.value ?? undefined,
},
transactionRawData,
origin,
scope,
options,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention */
import type { Types } from 'tronweb';

import type { TransactionScanResult, TransactionScanValidation } from './types';
import { ScanStatus, SecurityAlertResponse } from './types';
import type { SecurityAlertsApiClient } from '../../clients/security-alerts-api/SecurityAlertsApiClient';
Expand Down Expand Up @@ -33,11 +35,7 @@ export class TransactionScanService {
*
* @param params - The parameters for the function.
* @param params.accountAddress - The address of the account.
* @param params.parameters - The parameters for the transaction.
* @param params.parameters.from - The from address.
* @param params.parameters.to - The to address.
* @param params.parameters.data - The data of the transaction.
* @param params.parameters.value - The value of the transaction.
* @param params.transactionRawData - The raw data of the transaction.
* @param params.origin - The origin of the transaction.
* @param params.scope - The network scope.
* @param params.options - The options for the scan (simulation, validation).
Expand All @@ -46,19 +44,14 @@ export class TransactionScanService {
*/
async scanTransaction({
accountAddress,
parameters,
transactionRawData,
origin,
scope,
options = ['simulation', 'validation'],
account,
}: {
accountAddress: string;
parameters: {
from: string | undefined;
to: string | undefined;
data: string | undefined;
value: number | undefined | null;
};
transactionRawData: Types.Transaction['raw_data'];
origin: string;
scope: Network;
options?: string[] | undefined;
Expand All @@ -67,7 +60,7 @@ export class TransactionScanService {
try {
const result = await this.#securityAlertsApiClient.scanTransaction({
accountAddress,
parameters,
transactionRawData,
origin: origin === METAMASK_ORIGIN ? METAMASK_ORIGIN_URL : origin,
options,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,7 @@ describe('ConfirmSignTransaction render', () => {
// Verify security scan was triggered with from/to addresses and value
expect(mockTransactionScanService.scanTransaction).toHaveBeenCalledWith({
accountAddress: mockAccount.address,
parameters: {
from: 'TQkE4s6hQqxym4fYvtVLNEGPsaAChFqxPk',
to: '6wjzKJfLvipffZfji1fGwz4hbhwZrTgF5',
data: undefined,
value: 100000,
},
transactionRawData: mockRawData,
origin: testOrigin,
scope: Network.Mainnet,
options: ['simulation', 'validation'],
Expand Down
Loading
Loading