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
1 change: 1 addition & 0 deletions packages/keyring-api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **BREAKING**: Add required `rawAmount` field to `Balance` and `FungibleAssetAmount` types for precision in balance calculations ([#426](https://github.com/MetaMask/accounts/pull/426))
- Refine `EthAddressStruct` in order to make it compatible with the `Hex` type from `@metamask/utils` ([#405](https://github.com/MetaMask/accounts/pull/405))

## [21.3.0]
Expand Down
17 changes: 17 additions & 0 deletions packages/keyring-api/src/api/asset.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,54 @@ import { expectAssignable, expectNotAssignable } from 'tsd';

import type { Asset } from './asset';

// Valid fungible asset
expectAssignable<Asset>({
fungible: true,
type: 'eip155:1/slip44:60',
unit: 'ETH',
amount: '0.01',
rawAmount: '10000000000000000',
});

// Valid non-fungible asset
expectAssignable<Asset>({
fungible: false,
id: 'hedera:mainnet/nft:0.0.55492/12',
});

// Missing rawAmount for fungible asset
expectNotAssignable<Asset>({
fungible: true,
type: 'eip155:1/slip44:60',
unit: 'ETH',
amount: '0.01',
});

// Fungible asset with non-fungible id field
expectNotAssignable<Asset>({
fungible: true,
type: 'eip155:1/slip44:60',
unit: 'ETH',
amount: '0.01',
rawAmount: '10000000000000000',
id: 'hedera:mainnet/nft:0.0.55492/12',
});

// Non-fungible asset with unit field
expectNotAssignable<Asset>({
fungible: false,
id: 'hedera:mainnet/nft:0.0.55492/12',
unit: 'ETH',
});

// Non-fungible asset with type and unit
expectNotAssignable<Asset>({
fungible: false,
type: 'eip155:1/slip44:60',
unit: 'ETH',
});

// Fungible asset with id instead of type
expectNotAssignable<Asset>({
fungible: true,
id: 'hedera:mainnet/nft:0.0.55492/12',
Expand Down
21 changes: 16 additions & 5 deletions packages/keyring-api/src/api/asset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,45 @@ describe('AssetStruct', () => {
it.each([
// Missing `fungible`
{ asset: {}, expected: false },
// Valid
// Valid non-fungible asset
{
asset: {
fungible: false,
id: 'eip155:1/erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d/771769',
},
expected: true,
},
// Missing `unit` and `amount`
// Missing `unit`, `amount`, and `rawAmount`
{ asset: { fungible: true, type: 'eip155:1/slip44:60' }, expected: false },
// Missing `amount`
// Missing `amount` and `rawAmount`
{
asset: { fungible: true, type: 'eip155:1/slip44:60', unit: 'ETH' },
expected: false,
},
// Missing `unit`
// Missing `unit` and `rawAmount`
{
asset: { fungible: true, type: 'eip155:1/slip44:60', amount: '0.01' },
expected: false,
},
// Valid
// Missing `rawAmount`
{
asset: {
fungible: true,
type: 'eip155:1/slip44:60',
unit: 'ETH',
amount: '0.01',
},
expected: false,
},
// Valid fungible asset
{
asset: {
fungible: true,
type: 'eip155:1/slip44:60',
unit: 'ETH',
amount: '0.01',
rawAmount: '10000000000000000',
},
expected: true,
},
])('returns $expected for is($asset, AssetStruct)', ({ asset, expected }) => {
Expand Down
12 changes: 10 additions & 2 deletions packages/keyring-api/src/api/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,21 @@ import {
*/
export const FungibleAssetAmountStruct = object({
/**
* Asset unit.
* Asset unit (symbol).
*/
unit: string(),

/**
* Asset amount.
* The human-readable asset amount with decimals applied (e.g., "1.5" for 1.5 SOL).
* This is kept for backward compatibility with existing consumers.
*/
amount: StringNumberStruct,

/**
* The raw blockchain balance without decimals applied (e.g., "1500000000" for 1.5 SOL).
* This provides precision for calculations using BigInt/BigNumber.
*/
rawAmount: string(),
});

/**
Expand Down Expand Up @@ -83,6 +90,7 @@ export const NonFungibleAssetStruct = object({
* type: 'eip155:1/slip44:60',
* unit: 'ETH',
* amount: '0.01',
* rawAmount: '10000000000000000',
* }
* ```
*
Expand Down
94 changes: 81 additions & 13 deletions packages/keyring-api/src/api/balance.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,86 @@ import { expectAssignable, expectNotAssignable } from 'tsd';

import type { Balance } from './balance';

expectAssignable<Balance>({ amount: '1.0', unit: 'ETH' });
expectAssignable<Balance>({ amount: '0.1', unit: 'BTC' });
expectAssignable<Balance>({ amount: '.1', unit: 'gwei' });
expectAssignable<Balance>({ amount: '.1', unit: 'wei' });
expectAssignable<Balance>({ amount: '1.', unit: 'sat' });
// Valid balances with all required fields
expectAssignable<Balance>({
amount: '1.0',
unit: 'ETH',
rawAmount: '1000000000000000000',
});
expectAssignable<Balance>({
amount: '0.1',
unit: 'BTC',
rawAmount: '10000000',
});
expectAssignable<Balance>({
amount: '.1',
unit: 'gwei',
rawAmount: '100000000',
});
expectAssignable<Balance>({
amount: '.1',
unit: 'wei',
rawAmount: '100000000',
});
expectAssignable<Balance>({
amount: '1.',
unit: 'sat',
rawAmount: '100000000',
});

expectNotAssignable<Balance>({ amount: 1, unit: 'ETH' });
expectNotAssignable<Balance>({ amount: true, unit: 'ETH' });
expectNotAssignable<Balance>({ amount: undefined, unit: 'ETH' });
expectNotAssignable<Balance>({ amount: null, unit: 'ETH' });
// Missing rawAmount
expectNotAssignable<Balance>({ amount: '1.0', unit: 'ETH' });

expectNotAssignable<Balance>({ amount: '1.0', unit: 1 });
expectNotAssignable<Balance>({ amount: '1.0', unit: true });
expectNotAssignable<Balance>({ amount: '1.0', unit: undefined });
expectNotAssignable<Balance>({ amount: '1.0', unit: null });
// Invalid amount types
expectNotAssignable<Balance>({
amount: 1,
unit: 'ETH',
rawAmount: '1000000000000000000',
});
expectNotAssignable<Balance>({
amount: true,
unit: 'ETH',
rawAmount: '1000000000000000000',
});
expectNotAssignable<Balance>({
amount: undefined,
unit: 'ETH',
rawAmount: '1000000000000000000',
});
expectNotAssignable<Balance>({
amount: null,
unit: 'ETH',
rawAmount: '1000000000000000000',
});

// Invalid unit types
expectNotAssignable<Balance>({
amount: '1.0',
unit: 1,
rawAmount: '1000000000000000000',
});
expectNotAssignable<Balance>({
amount: '1.0',
unit: true,
rawAmount: '1000000000000000000',
});
expectNotAssignable<Balance>({
amount: '1.0',
unit: undefined,
rawAmount: '1000000000000000000',
});
expectNotAssignable<Balance>({
amount: '1.0',
unit: null,
rawAmount: '1000000000000000000',
});

// Invalid rawAmount types
expectNotAssignable<Balance>({ amount: '1.0', unit: 'ETH', rawAmount: 1 });
expectNotAssignable<Balance>({ amount: '1.0', unit: 'ETH', rawAmount: true });
expectNotAssignable<Balance>({
amount: '1.0',
unit: 'ETH',
rawAmount: undefined,
});
expectNotAssignable<Balance>({ amount: '1.0', unit: 'ETH', rawAmount: null });
62 changes: 52 additions & 10 deletions packages/keyring-api/src/api/balance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,62 @@ import { BalanceStruct } from './balance';
describe('BalanceStruct', () => {
it.each([
// Valid
{ balance: { amount: '1.0', unit: 'ETH' }, expected: true },
{ balance: { amount: '0.1', unit: 'BTC' }, expected: true },
{
balance: { amount: '1.0', unit: 'ETH', rawAmount: '1000000000000000000' },
expected: true,
},
{
balance: { amount: '0.1', unit: 'BTC', rawAmount: '10000000' },
expected: true,
},
// Missing amount
{ balance: { unit: 'ETH' }, expected: false },
{
balance: { unit: 'ETH', rawAmount: '1000000000000000000' },
expected: false,
},
// Missing unit
{ balance: { amount: '1.0' }, expected: false },
{
balance: { amount: '1.0', rawAmount: '1000000000000000000' },
expected: false,
},
// Missing rawAmount
{ balance: { amount: '1.0', unit: 'ETH' }, expected: false },
// Invalid amount type
{ balance: { amount: 1, unit: 'ETH' }, expected: false },
{ balance: { amount: true, unit: 'ETH' }, expected: false },
{ balance: { amount: null, unit: 'ETH' }, expected: false },
{
balance: { amount: 1, unit: 'ETH', rawAmount: '1000000000000000000' },
expected: false,
},
{
balance: { amount: true, unit: 'ETH', rawAmount: '1000000000000000000' },
expected: false,
},
{
balance: { amount: null, unit: 'ETH', rawAmount: '1000000000000000000' },
expected: false,
},
// Invalid unit type
{ balance: { amount: '1.0', unit: 1 }, expected: false },
{ balance: { amount: '1.0', unit: true }, expected: false },
{ balance: { amount: '1.0', unit: null }, expected: false },
{
balance: { amount: '1.0', unit: 1, rawAmount: '1000000000000000000' },
expected: false,
},
{
balance: { amount: '1.0', unit: true, rawAmount: '1000000000000000000' },
expected: false,
},
{
balance: { amount: '1.0', unit: null, rawAmount: '1000000000000000000' },
expected: false,
},
// Invalid rawAmount type
{ balance: { amount: '1.0', unit: 'ETH', rawAmount: 1 }, expected: false },
{
balance: { amount: '1.0', unit: 'ETH', rawAmount: true },
expected: false,
},
{
balance: { amount: '1.0', unit: 'ETH', rawAmount: null },
expected: false,
},
])(
'returns $expected for is($balance, BalanceStruct)',
({ balance, expected }) => {
Expand Down
12 changes: 12 additions & 0 deletions packages/keyring-api/src/api/balance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,20 @@ import type { Infer } from '@metamask/superstruct';
import { string } from '@metamask/superstruct';

export const BalanceStruct = object({
/**
* The human-readable balance amount with decimals applied (e.g., "1.5" for 1.5 SOL).
* This is kept for backward compatibility with existing consumers.
*/
amount: StringNumberStruct,
/**
* The token/asset symbol or unit (e.g., "SOL", "TRX").
*/
unit: string(),
/**
* The raw blockchain balance without decimals applied (e.g., "1500000000" for 1.5 SOL).
* This provides precision for calculations using BigInt/BigNumber.
*/
rawAmount: string(),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rawAmount lacks numeric validation unlike amount field

Medium Severity

The rawAmount field uses string() for validation while amount uses StringNumberStruct which validates the string is numeric. This inconsistency allows non-numeric values like "hello" or "" to pass validation for rawAmount. Since the PR states rawAmount is intended for "calculations using BigInt/BigNumber", invalid non-numeric strings would cause runtime errors when consumers attempt BigInt(rawAmount). Using the same StringNumberStruct (or a stricter integer-only pattern) would provide consistent validation and catch invalid values at the API boundary rather than during calculations.

Additional Locations (1)

Fix in Cursor Fix in Web

});

export type Balance = Infer<typeof BalanceStruct>;
5 changes: 5 additions & 0 deletions packages/keyring-api/src/api/transaction.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ expectAssignable<Transaction>({
type: 'eip155:1/slip44:60',
unit: 'ETH',
amount: '0.0001',
rawAmount: '100000000000000',
},
},
{
Expand All @@ -41,6 +42,7 @@ expectAssignable<Transaction>({
type: 'eip155:1/slip44:60',
unit: 'ETH',
amount: '0.0001',
rawAmount: '100000000000000',
},
},
],
Expand All @@ -62,6 +64,7 @@ expectAssignable<Transaction>({
type: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
unit: 'BTC',
amount: '0.002',
rawAmount: '200000',
},
},
{
Expand All @@ -80,6 +83,7 @@ expectAssignable<Transaction>({
type: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
unit: 'BTC',
amount: '0.001',
rawAmount: '100000',
},
},
{
Expand All @@ -98,6 +102,7 @@ expectAssignable<Transaction>({
type: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
unit: 'BTC',
amount: '0.001',
rawAmount: '100000',
},
},
],
Expand Down
Loading
Loading