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
8 changes: 0 additions & 8 deletions eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -321,14 +321,6 @@
"count": 1
}
},
"packages/tron-wallet-snap/src/services/send/FeeCalculatorService.test.ts": {
"@typescript-eslint/explicit-function-return-type": {
"count": 1
},
"@typescript-eslint/no-explicit-any": {
"count": 5
}
},
"packages/tron-wallet-snap/src/services/send/FeeCalculatorService.ts": {
"no-restricted-syntax": {
"count": 1
Expand Down
6 changes: 6 additions & 0 deletions packages/tron-wallet-snap/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Disclose the mandatory 9,999 TRX `WitnessCreateContract` account-upgrade burn on confirmation ([#73](https://github.com/MetaMask/internal-snaps/pull/73))
- Include `getAccountUpgradeCost` in fee calculation so Network fee and insufficient-balance checks reflect the irreversible burn
- Replace the generic "Unsupported contract for simulation" copy with Super Representative candidate disclosure for this contract type

## [2.0.0]

### Changed
Expand Down
3 changes: 3 additions & 0 deletions packages/tron-wallet-snap/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@
"confirmation.estimatedChanges.unsupportedContract": {
"message": "Unsupported contract for simulation"
},
"confirmation.estimatedChanges.witnessCreate": {
"message": "Registers your account as a Super Representative candidate and permanently burns 9,999 TRX."
},
"confirmation.simulationTitleAPIError": {
"message": "Because of an error, we couldn't check for security alerts."
},
Expand Down
3 changes: 3 additions & 0 deletions packages/tron-wallet-snap/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@
"confirmation.estimatedChanges.unsupportedContract": {
"message": "Contrato no compatible con simulación"
},
"confirmation.estimatedChanges.witnessCreate": {
"message": "Registra tu cuenta como candidato a Super Representante y quema permanentemente 9.999 TRX."
},
"confirmation.simulationTitleAPIError": {
"message": "Debido a un error, no pudimos verificar alertas de seguridad."
},
Expand Down
3 changes: 3 additions & 0 deletions packages/tron-wallet-snap/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@
"confirmation.estimatedChanges.unsupportedContract": {
"message": "Unsupported contract for simulation"
},
"confirmation.estimatedChanges.witnessCreate": {
"message": "Registers your account as a Super Representative candidate and permanently burns 9,999 TRX."
},
"confirmation.simulationTitleAPIError": {
"message": "Because of an error, we couldn't check for security alerts."
},
Expand Down
2 changes: 1 addition & 1 deletion packages/tron-wallet-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/internal-snaps.git"
},
"source": {
"shasum": "0TZh63x04Gncx8UgHBoWnQ4RmX6kmTL6RmJTyqgJ7G8=",
"shasum": "9qRM4qCjRIdWCy/txZ3wHH42cMj7UsDKpqyhJ4R69Dk=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
6 changes: 6 additions & 0 deletions packages/tron-wallet-snap/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ export const SUN_IN_TRX = 1_000_000;
export const FALLBACK_GET_TRANSACTION_FEE_SUN = 1000;
export const FALLBACK_GET_ENERGY_FEE_SUN = 100;
export const FALLBACK_ENERGY_PRICE_SUN = 420;
/**
* Default `getAccountUpgradeCost` (WitnessCreateContract burn) in SUN = 9,999 TRX.
*
* @see https://developers.tron.network/docs/super-representatives
*/
export const FALLBACK_ACCOUNT_UPGRADE_COST_SUN = 9_999_000_000;
/**
* 101 TRX
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FeeType } from '@metamask/keyring-api';
import { BigNumber } from 'bignumber.js';
import type { Transaction } from 'tronweb/lib/esm/types';

import type { SnapClient } from '../../clients/snap/SnapClient';
import type { TriggerConstantContractResponse } from '../../clients/tron-http';
Expand All @@ -19,7 +20,9 @@ import { FeeUnavailableError } from './errors';
import { FeeCalculatorService } from './FeeCalculatorService';

// Helper to get transaction examples in the expected format
const getTransactionExample = (type: 'native' | 'trc10' | 'trc20'): any => {
const getTransactionExample = (
type: 'native' | 'trc10' | 'trc20',
): Transaction => {
let mockData;
switch (type) {
case 'native':
Expand All @@ -42,11 +45,11 @@ const getTransactionExample = (type: 'native' | 'trc10' | 'trc20'): any => {
txID: mockData.txID,
raw_data_hex: mockData.raw_data_hex,
raw_data: mockData.raw_data,
};
} as Transaction;
};

// Helper to create a large transaction by modifying the TRC20 example
const createLargeTransaction = (): any => {
const createLargeTransaction = (): Transaction => {
const baseTransaction = getTransactionExample('trc20');
// Modify the data field to be much larger to simulate bandwidth issues
const largeData = 'b'.repeat(2000);
Expand Down Expand Up @@ -238,7 +241,9 @@ describe('FeeCalculatorService', () => {

describe('System contract scenarios (no energy needed)', () => {
// Helper to create a mock transaction with a specific contract type
const createSystemContractTransaction = (contractType: string): any => {
const createSystemContractTransaction = (
contractType: string,
): Transaction => {
const baseTransaction = getTransactionExample('native');
return {
...baseTransaction,
Expand Down Expand Up @@ -2168,12 +2173,197 @@ describe('FeeCalculatorService', () => {
});
});

describe('WitnessCreateContract account upgrade fee scenarios', () => {
const createWitnessCreateTransaction = (): Transaction => {
const base = getTransactionExample('native');
return {
...base,
raw_data: {
...base.raw_data,
contract: [
{
parameter: {
value: {
owner_address: '41a7d8a35b260395c14aa456297662092ba3b76fc0',
url: '68747470733a2f2f6578616d706c652e636f6d',
},
type_url:
'type.googleapis.com/protocol.WitnessCreateContract',
},
type: 'WitnessCreateContract',
},
],
},
};
};

it('adds 9999 TRX account upgrade fee for WitnessCreateContract when chain param is present', async () => {
await withFeeCalculatorService(
async ({ feeCalculatorService, trongridApiClient }) => {
trongridApiClient.getChainParameters.mockResolvedValue([
{ key: 'getTransactionFee', value: 1000 },
{ key: 'getEnergyFee', value: 100 },
{ key: 'getAccountUpgradeCost', value: 9_999_000_000 },
]);

const transaction = createWitnessCreateTransaction();
const availableEnergy = ZERO;
const availableBandwidth = BigNumber(1000000);

const result = await feeCalculatorService.computeFee({
scope: Network.Mainnet,
transaction,
availableEnergy,
availableBandwidth,
});

expect(result).toStrictEqual([
{
type: FeeType.Base,
asset: {
unit: 'TRX',
type: 'tron:728126428/slip44:195',
amount: '9999',
fungible: true,
},
},
{
type: FeeType.Base,
asset: {
unit: 'BANDWIDTH',
type: 'tron:728126428/slip44:bandwidth',
amount: '266',
fungible: true,
},
},
]);
},
);
});

it('falls back to 9999 TRX when getAccountUpgradeCost is missing from chain parameters', async () => {
await withFeeCalculatorService(
async ({ feeCalculatorService, trongridApiClient }) => {
trongridApiClient.getChainParameters.mockResolvedValue([
{ key: 'getTransactionFee', value: 1000 },
{ key: 'getEnergyFee', value: 100 },
]);

const result = await feeCalculatorService.computeFee({
scope: Network.Mainnet,
transaction: createWitnessCreateTransaction(),
availableEnergy: ZERO,
availableBandwidth: BigNumber(1000000),
});

expect(result[0]).toStrictEqual({
type: FeeType.Base,
asset: {
unit: 'TRX',
type: 'tron:728126428/slip44:195',
amount: '9999',
fungible: true,
},
});
},
);
});

it('adds account upgrade fee on top of bandwidth TRX cost', async () => {
await withFeeCalculatorService(
async ({ feeCalculatorService, trongridApiClient }) => {
trongridApiClient.getChainParameters.mockResolvedValue([
{ key: 'getTransactionFee', value: 1000 },
{ key: 'getEnergyFee', value: 100 },
{ key: 'getAccountUpgradeCost', value: 9_999_000_000 },
]);

const result = await feeCalculatorService.computeFee({
scope: Network.Mainnet,
transaction: createWitnessCreateTransaction(),
availableEnergy: ZERO,
availableBandwidth: ZERO,
});

// Bandwidth: 266 * 1000 SUN = 0.266 TRX + 9999 TRX upgrade = 9999.266 TRX
expect(result[0]).toStrictEqual({
type: FeeType.Base,
asset: {
unit: 'TRX',
type: 'tron:728126428/slip44:195',
amount: '9999.266',
fungible: true,
},
});
},
);
});

it('uses the on-chain getAccountUpgradeCost value when it differs from the default', async () => {
await withFeeCalculatorService(
async ({ feeCalculatorService, trongridApiClient }) => {
trongridApiClient.getChainParameters.mockResolvedValue([
{ key: 'getTransactionFee', value: 1000 },
{ key: 'getEnergyFee', value: 100 },
{ key: 'getAccountUpgradeCost', value: 5_000_000_000 }, // 5000 TRX
]);

const result = await feeCalculatorService.computeFee({
scope: Network.Mainnet,
transaction: createWitnessCreateTransaction(),
availableEnergy: ZERO,
availableBandwidth: BigNumber(1000000),
});

expect(result[0]?.asset.amount).toBe('5000');
},
);
});

it('does not add account upgrade fee for non-WitnessCreate contracts', async () => {
await withFeeCalculatorService(async ({ feeCalculatorService }) => {
const result = await feeCalculatorService.computeFee({
scope: Network.Mainnet,
transaction: getTransactionExample('native'),
availableEnergy: ZERO,
availableBandwidth: BigNumber(1000000),
});

expect(result[0]?.asset.amount).toBe('0');
});
});

it('falls back to 9999 TRX when chain parameters are unavailable', async () => {
await withFeeCalculatorService(
async ({ feeCalculatorService, trongridApiClient }) => {
trongridApiClient.getChainParameters.mockRejectedValue(
new Error('TronGrid unavailable'),
);
trongridApiClient.peekCachedChainParameters.mockResolvedValue(
undefined,
);

const result = await feeCalculatorService.computeFee({
scope: Network.Mainnet,
transaction: createWitnessCreateTransaction(),
availableEnergy: ZERO,
availableBandwidth: BigNumber(1000000),
});

// Enough bandwidth so we never need getTransactionFee; upgrade fee
// still discloses the default burn via fallback.
expect(result[0]?.asset.amount).toBe('9999');
},
);
});
});

describe('Memo fee scenarios', () => {
// Helper to add a memo (raw_data.data) to a transaction
const addMemoToTransaction = (
transaction: any,
transaction: Transaction,
memoHex: string,
): any => ({
): Transaction => ({
...transaction,
raw_data: {
...transaction.raw_data,
Expand Down Expand Up @@ -2382,7 +2572,11 @@ describe('FeeCalculatorService', () => {
describe('Graceful failure when TronGrid is unavailable', () => {
// Native transfer with insufficient bandwidth so a TRX fee is owed and
// the chain-params conversion path is exercised.
const buildNativeTxWithBandwidthOverage = () => ({
const buildNativeTxWithBandwidthOverage = (): {
transaction: Transaction;
availableEnergy: BigNumber;
availableBandwidth: BigNumber;
} => ({
transaction: getTransactionExample('native'),
availableEnergy: ZERO,
availableBandwidth: BigNumber(100), // < 266 bytes needed
Expand Down
Loading
Loading