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
5 changes: 4 additions & 1 deletion packages/tron-wallet-snap/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Extract shared asset util functions and inject `SnapAssetsAdapter` from `context` into `AssetsService` ([#143](https://github.com/MetaMask/internal-snaps/pull/143))
- Rename `getByKeyringAccountId` to `getAccountAssets` (with essential-asset synthesis) and update keyring callers ([#143](https://github.com/MetaMask/internal-snaps/pull/143))

- Bump `@metamask/utils` from `^11.9.0` to `^11.11.9` ([#161](https://github.com/MetaMask/internal-snaps/pull/161))

### Fixed

- Estimate native TRX/TRC-10 sends that activate a new account as 1 TRX plus 100 Bandwidth (or 0.1 TRX when staked Bandwidth is insufficient), instead of TransferContract byte size ([#175](https://github.com/MetaMask/internal-snaps/pull/175))

## [3.1.0]

### Added
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": "EoI9ohkqKuwXmnIAWTRB1z2UGIpDq8PDtYGVtX1GouI=",
"shasum": "nE7qHH2jaCdpQdlledFzcSRBzWn4jB9qNEmOpi282gk=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
13 changes: 13 additions & 0 deletions packages/tron-wallet-snap/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ export const TRACK_TX_INTERVAL = 'PT3S';
*/
export const TRACK_TX_MAX_ATTEMPTS = 5;

/**
* Default `getCreateAccountFee` in SUN (0.1 TRX). Burned when the sender
* lacks enough *staked* Bandwidth to activate a new account.
*
* @see https://developers.tron.network/docs/account#activating-an-account
*/
export const FALLBACK_CREATE_ACCOUNT_FEE_SUN = 100_000;

/**
* Default `getCreateNewAccountFeeInSystemContract` in SUN (1 TRX).
*/
export const FALLBACK_CREATE_NEW_ACCOUNT_FEE_SUN = 1_000_000;

export enum Network {
Mainnet = 'tron:728126428',
Nile = 'tron:3448148188',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ async function withFeeCalculatorService<ReturnValue>(
getChainParameters: jest.fn().mockResolvedValue([
{ key: 'getTransactionFee', value: 1000 },
{ key: 'getEnergyFee', value: 100 },
{ key: 'getCreateAccountFee', value: 100_000 },
{ key: 'getCreateNewAccountFeeInSystemContract', value: 1_000_000 },
]),
getAccountInfoByAddress: jest.fn(),
peekCachedChainParameters: jest.fn().mockResolvedValue(undefined),
Expand Down Expand Up @@ -190,6 +192,29 @@ describe('FeeCalculatorService', () => {
fungible: true,
},
};
const expectedMainnetActivationBandwidthFee = {
type: FeeType.Base,
asset: {
unit: 'BANDWIDTH',
type: 'tron:728126428/slip44:bandwidth',
amount: '100',
fungible: true,
},
};

const mockStakedBandwidth = (
tronHttpClient: MockTronHttpClient,
netLimit: number,
netUsed = 0,
freeNetLimit = 600,
): void => {
tronHttpClient.getAccountResources.mockResolvedValue({
NetLimit: netLimit,
NetUsed: netUsed,
freeNetLimit,
freeNetUsed: 0,
});
};
const expectedMainnetContractBandwidthFee = {
type: FeeType.Base,
asset: {
Expand Down Expand Up @@ -805,17 +830,23 @@ describe('FeeCalculatorService', () => {
});

describe('Account activation fee scenarios', () => {
it('adds 1 TRX activation fee when recipient account is not activated', async () => {
it('adds 1 TRX activation fee and 100 Bandwidth when recipient is not activated and sender has staked Bandwidth', async () => {
await withFeeCalculatorService(
async ({ feeCalculatorService, trongridApiClient }) => {
// Mock the account check to throw (account not found)
async ({
feeCalculatorService,
trongridApiClient,
tronHttpClient,
}) => {
trongridApiClient.getAccountInfoByAddress.mockRejectedValue(
new TrongridAccountNotFoundError(),
);
mockStakedBandwidth(tronHttpClient, 1000);

const transaction = getTransactionExample('native');
const availableEnergy = ZERO;
const availableBandwidth = BigNumber(1000000); // More than needed
// Combined free+staked quota is intentionally large; activation
// must use staked Bandwidth (100), not tx-size (~266).
const availableBandwidth = BigNumber(1000000);

const result = await feeCalculatorService.computeFee({
scope: Network.Mainnet,
Expand All @@ -824,7 +855,6 @@ describe('FeeCalculatorService', () => {
availableBandwidth,
});

// Should have TRX first (1 TRX activation fee), then bandwidth consumption
expect(result).toStrictEqual([
{
type: FeeType.Base,
Expand All @@ -835,12 +865,38 @@ describe('FeeCalculatorService', () => {
fungible: true,
},
},
expectedMainnetActivationBandwidthFee,
]);
},
);
});

it('ignores free Bandwidth and burns 0.1 TRX when sender has no staked Bandwidth', async () => {
await withFeeCalculatorService(
async ({
feeCalculatorService,
trongridApiClient,
tronHttpClient,
}) => {
trongridApiClient.getAccountInfoByAddress.mockRejectedValue(
new TrongridAccountNotFoundError(),
);
mockStakedBandwidth(tronHttpClient, 0, 0, 600);

const result = await feeCalculatorService.computeFee({
scope: Network.Mainnet,
transaction: getTransactionExample('native'),
availableEnergy: ZERO,
availableBandwidth: BigNumber(600),
});

expect(result).toStrictEqual([
{
type: FeeType.Base,
asset: {
unit: 'BANDWIDTH',
type: 'tron:728126428/slip44:bandwidth',
amount: '266',
unit: 'TRX',
type: 'tron:728126428/slip44:195',
amount: '1.1',
fungible: true,
},
},
Expand All @@ -851,10 +907,16 @@ describe('FeeCalculatorService', () => {

it('does not track error when recipient account is not activated', async () => {
await withFeeCalculatorService(
async ({ feeCalculatorService, trongridApiClient, snapClient }) => {
async ({
feeCalculatorService,
trongridApiClient,
tronHttpClient,
snapClient,
}) => {
trongridApiClient.getAccountInfoByAddress.mockRejectedValue(
new TrongridAccountNotFoundError(),
);
mockStakedBandwidth(tronHttpClient, 1000);

await feeCalculatorService.computeFee({
scope: Network.Mainnet,
Expand All @@ -870,10 +932,16 @@ describe('FeeCalculatorService', () => {

it('tracks unexpected errors when account activation check fails', async () => {
await withFeeCalculatorService(
async ({ feeCalculatorService, trongridApiClient, snapClient }) => {
async ({
feeCalculatorService,
trongridApiClient,
tronHttpClient,
snapClient,
}) => {
const error = new Error('Account activation check failed');

trongridApiClient.getAccountInfoByAddress.mockRejectedValue(error);
mockStakedBandwidth(tronHttpClient, 1000);

await feeCalculatorService.computeFee({
scope: Network.Mainnet,
Expand All @@ -887,17 +955,21 @@ describe('FeeCalculatorService', () => {
);
});

it('adds activation fee to existing TRX cost when recipient is not activated', async () => {
it('adds 0.1 TRX Bandwidth shortfall instead of tx-size burn when recipient is not activated', async () => {
await withFeeCalculatorService(
async ({ feeCalculatorService, trongridApiClient }) => {
// Mock the account check to throw (account not found)
async ({
feeCalculatorService,
trongridApiClient,
tronHttpClient,
}) => {
trongridApiClient.getAccountInfoByAddress.mockRejectedValue(
new TrongridAccountNotFoundError(),
);
mockStakedBandwidth(tronHttpClient, 0);

const transaction = getTransactionExample('native');
const availableEnergy = ZERO;
const availableBandwidth = ZERO; // Not enough bandwidth, triggers TRX cost
const availableBandwidth = ZERO;

const result = await feeCalculatorService.computeFee({
scope: Network.Mainnet,
Expand All @@ -906,18 +978,51 @@ describe('FeeCalculatorService', () => {
availableBandwidth,
});

// Should have TRX cost for bandwidth (0.266) + activation fee (1) = 1.266 TRX
expect(result).toStrictEqual([
{
type: FeeType.Base,
asset: {
unit: 'TRX',
type: 'tron:728126428/slip44:195',
amount: '1.266',
amount: '1.1',
fungible: true,
},
},
expectedMainnetBandwidthFee,
]);
},
);
});

it('uses create-account Bandwidth for TRC10 transfers to an unactivated recipient', async () => {
await withFeeCalculatorService(
async ({
feeCalculatorService,
trongridApiClient,
tronHttpClient,
}) => {
trongridApiClient.getAccountInfoByAddress.mockRejectedValue(
new TrongridAccountNotFoundError(),
);
mockStakedBandwidth(tronHttpClient, 1000);

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

expect(result).toStrictEqual([
{
type: FeeType.Base,
asset: {
unit: 'TRX',
type: 'tron:728126428/slip44:195',
amount: '1',
fungible: true,
},
},
expectedMainnetActivationBandwidthFee,
]);
},
);
Expand Down Expand Up @@ -2479,11 +2584,15 @@ describe('FeeCalculatorService', () => {

it('adds memo fee combined with account activation fee', async () => {
await withFeeCalculatorService(
async ({ feeCalculatorService, trongridApiClient }) => {
// Account not activated
async ({
feeCalculatorService,
trongridApiClient,
tronHttpClient,
}) => {
trongridApiClient.getAccountInfoByAddress.mockRejectedValue(
new TrongridAccountNotFoundError(),
);
mockStakedBandwidth(tronHttpClient, 1000);

const transaction = addMemoToTransaction(
getTransactionExample('native'),
Expand All @@ -2499,7 +2608,7 @@ describe('FeeCalculatorService', () => {
availableBandwidth,
});

// 1 TRX activation + 1 TRX memo = 2 TRX
// 1 TRX activation + 1 TRX memo = 2 TRX, 100 create-account Bandwidth
expect(result).toStrictEqual([
{
type: FeeType.Base,
Expand All @@ -2510,15 +2619,7 @@ describe('FeeCalculatorService', () => {
fungible: true,
},
},
{
type: FeeType.Base,
asset: {
unit: 'BANDWIDTH',
type: 'tron:728126428/slip44:bandwidth',
amount: '266',
fungible: true,
},
},
expectedMainnetActivationBandwidthFee,
]);
},
);
Expand Down
Loading