Skip to content
This repository was archived by the owner on Jul 31, 2026. It is now read-only.
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
6 changes: 6 additions & 0 deletions packages/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 ([#382](https://github.com/MetaMask/snap-tron-wallet/pull/382))
- 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

## [1.33.1]

### Fixed
Expand Down
8 changes: 4 additions & 4 deletions packages/snap/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ const config = {
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 70,
functions: 75.86,
lines: 82.65,
statements: 82.62,
branches: 70.2,
functions: 75.95,
lines: 82.73,
statements: 82.7,
},
},

Expand Down
3 changes: 3 additions & 0 deletions packages/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/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/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/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": "SoFlTE0hiN9rFDWlAiCCSyn1iSBvJwvlfi1vK/qrufo=",
"shasum": "QRJNeaBFBweU9jNbYqZCYxxnQ4LNoVDEl8WTBlNvqZw=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
6 changes: 6 additions & 0 deletions packages/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
185 changes: 185 additions & 0 deletions packages/snap/src/services/send/FeeCalculatorService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2169,6 +2169,191 @@ describe('FeeCalculatorService', () => {
});
});

describe('WitnessCreateContract account upgrade fee scenarios', () => {
const createWitnessCreateTransaction = (): any => {
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 = (
Expand Down
71 changes: 71 additions & 0 deletions packages/snap/src/services/send/FeeCalculatorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { type TrongridApiClient } from '../../clients/trongrid/TrongridApiClient
import type { Network } from '../../constants';
import {
ACCOUNT_ACTIVATION_FEE_TRX,
FALLBACK_ACCOUNT_UPGRADE_COST_SUN,
FALLBACK_ENERGY_PRICE_SUN,
FALLBACK_GET_ENERGY_FEE_SUN,
FALLBACK_GET_TRANSACTION_FEE_SUN,
Expand Down Expand Up @@ -707,6 +708,64 @@ export class FeeCalculatorService {
return ZERO;
}

/**
* Calculate the irreversible account-upgrade burn for WitnessCreateContract.
*
* Registering as a Super Representative candidate burns
* `getAccountUpgradeCost` TRX (default 9,999). This is a protocol-level debit
* separate from bandwidth/energy, and must be disclosed before signing.
*
* @see https://developers.tron.network/docs/super-representatives
* @param options - The options object
* @param options.scope - The network scope to resolve chain parameters for
* @param options.transaction - The transaction to inspect
* @returns Promise<BigNumber> - The upgrade burn fee in TRX (0 if not WitnessCreate)
*/
async #accountUpgradeFee({
scope,
transaction,
}: {
scope: Network;
transaction: Transaction;
}): Promise<BigNumber> {
const contracts = transaction.raw_data.contract;

if (!contracts || contracts.length === 0) {
return ZERO;
}

const hasWitnessCreate = contracts.some(
(contract) => (contract.type as string) === 'WitnessCreateContract',
);

if (!hasWitnessCreate) {
return ZERO;
}

let upgradeCostSun = FALLBACK_ACCOUNT_UPGRADE_COST_SUN;

try {
const chainParameters = await this.#getChainParameters(scope);
upgradeCostSun =
chainParameters.find((param) => param.key === 'getAccountUpgradeCost')
?.value ?? FALLBACK_ACCOUNT_UPGRADE_COST_SUN;
} catch (error) {
// Still disclose the default burn when chain parameters are unavailable.
this.#logger.warn(
{ error },
'Failed to fetch account upgrade cost, using fallback 9999 TRX',
);
}

const upgradeCostTrx = BigNumber(upgradeCostSun).div(SUN_IN_TRX);

this.#logger.log(
`WitnessCreateContract detected, adding ${upgradeCostTrx.toString()} TRX account upgrade fee`,
);

return upgradeCostTrx;
}

/**
* Calculate complete fee breakdown for a TRON transaction.
* Supports both signed and unsigned transactions.
Expand Down Expand Up @@ -813,6 +872,18 @@ export class FeeCalculatorService {
totalTrxCost = totalTrxCost.plus(memoFee);
}

/**
* Fourth, WitnessCreateContract account-upgrade burn (default 9,999 TRX)
*/
const accountUpgradeFee = await this.#accountUpgradeFee({
scope,
transaction,
});

if (accountUpgradeFee.isGreaterThan(0)) {
totalTrxCost = totalTrxCost.plus(accountUpgradeFee);
}

/**
* Build result array - TRX MUST always be first element, even if 0
*/
Expand Down
Loading
Loading