diff --git a/packages/snap/CHANGELOG.md b/packages/snap/CHANGELOG.md index 447defc4..a8b6860f 100644 --- a/packages/snap/CHANGELOG.md +++ b/packages/snap/CHANGELOG.md @@ -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 diff --git a/packages/snap/jest.config.js b/packages/snap/jest.config.js index 5f9481dd..655a40a5 100644 --- a/packages/snap/jest.config.js +++ b/packages/snap/jest.config.js @@ -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, }, }, diff --git a/packages/snap/locales/en.json b/packages/snap/locales/en.json index ff2835dd..7111ee22 100644 --- a/packages/snap/locales/en.json +++ b/packages/snap/locales/en.json @@ -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." }, diff --git a/packages/snap/locales/es.json b/packages/snap/locales/es.json index 10c56146..7c707a0d 100644 --- a/packages/snap/locales/es.json +++ b/packages/snap/locales/es.json @@ -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." }, diff --git a/packages/snap/messages.json b/packages/snap/messages.json index 52c6e271..1f26af06 100644 --- a/packages/snap/messages.json +++ b/packages/snap/messages.json @@ -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." }, diff --git a/packages/snap/snap.manifest.json b/packages/snap/snap.manifest.json index 82fe14c8..5b71217e 100644 --- a/packages/snap/snap.manifest.json +++ b/packages/snap/snap.manifest.json @@ -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", diff --git a/packages/snap/src/constants/index.ts b/packages/snap/src/constants/index.ts index 7dbb15d0..10d7293a 100644 --- a/packages/snap/src/constants/index.ts +++ b/packages/snap/src/constants/index.ts @@ -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 */ diff --git a/packages/snap/src/services/send/FeeCalculatorService.test.ts b/packages/snap/src/services/send/FeeCalculatorService.test.ts index 830d7a70..1a0cea74 100644 --- a/packages/snap/src/services/send/FeeCalculatorService.test.ts +++ b/packages/snap/src/services/send/FeeCalculatorService.test.ts @@ -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 = ( diff --git a/packages/snap/src/services/send/FeeCalculatorService.ts b/packages/snap/src/services/send/FeeCalculatorService.ts index 4e089945..0966b28d 100644 --- a/packages/snap/src/services/send/FeeCalculatorService.ts +++ b/packages/snap/src/services/send/FeeCalculatorService.ts @@ -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, @@ -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 - The upgrade burn fee in TRX (0 if not WitnessCreate) + */ + async #accountUpgradeFee({ + scope, + transaction, + }: { + scope: Network; + transaction: Transaction; + }): Promise { + 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. @@ -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 */ diff --git a/packages/snap/src/ui/confirmation/views/ConfirmSignTransaction/ConfirmSignTransaction.test.tsx b/packages/snap/src/ui/confirmation/views/ConfirmSignTransaction/ConfirmSignTransaction.test.tsx index 8b0501a6..4a0b7eaa 100644 --- a/packages/snap/src/ui/confirmation/views/ConfirmSignTransaction/ConfirmSignTransaction.test.tsx +++ b/packages/snap/src/ui/confirmation/views/ConfirmSignTransaction/ConfirmSignTransaction.test.tsx @@ -243,4 +243,39 @@ describe('ConfirmSignTransaction', () => { expect(texts).not.toContain(SIMULATION_ERROR_TITLE); }); + + it('shows unsupported-contract copy for skipped simulations of unknown contracts', () => { + const texts = renderTexts( + buildContext({ + transaction: { + rawDataHex: '0a02beef', + type: 'VoteWitnessContract', + }, + scan: buildScanResult({ + simulationStatus: SimulationStatus.Skipped, + }), + }), + ); + + expect(texts).toContain('Unsupported contract for simulation'); + }); + + it('discloses WitnessCreateContract operation and 9,999 TRX burn when simulation is skipped', () => { + const texts = renderTexts( + buildContext({ + transaction: { + rawDataHex: '0a02beef', + type: 'WitnessCreateContract', + }, + scan: buildScanResult({ + simulationStatus: SimulationStatus.Skipped, + }), + }), + ); + + expect(texts).not.toContain('Unsupported contract for simulation'); + expect(texts).toContain( + 'Registers your account as a Super Representative candidate and permanently burns 9,999 TRX.', + ); + }); }); diff --git a/packages/snap/src/ui/confirmation/views/ConfirmSignTransaction/ConfirmSignTransaction.tsx b/packages/snap/src/ui/confirmation/views/ConfirmSignTransaction/ConfirmSignTransaction.tsx index 11e47bd6..0168299e 100644 --- a/packages/snap/src/ui/confirmation/views/ConfirmSignTransaction/ConfirmSignTransaction.tsx +++ b/packages/snap/src/ui/confirmation/views/ConfirmSignTransaction/ConfirmSignTransaction.tsx @@ -62,6 +62,11 @@ export const ConfirmSignTransaction = ({ let estimatedChangesSection: ComponentOrElement | null = null; if (preferences.simulateOnChainActions) { if (scan?.simulationStatus === SimulationStatus.Skipped) { + const skippedChangesMessage = + context.transaction.type === 'WitnessCreateContract' + ? translate('confirmation.estimatedChanges.witnessCreate') + : translate('confirmation.estimatedChanges.unsupportedContract'); + estimatedChangesSection = (
@@ -74,9 +79,7 @@ export const ConfirmSignTransaction = ({ - - {translate('confirmation.estimatedChanges.unsupportedContract')} - + {skippedChangesMessage}
); } else {