Skip to content
Merged
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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"@tanstack/react-query": "^5.90.12",
"@tanstack/react-router": "^1.140.0",
"@tanstack/react-store": "^0.8.0",
"big.js": "^7.0.1",
"bip39": "^3.1.0",
"buffer": "^6.0.3",
"class-variance-authority": "^0.7.1",
Expand Down Expand Up @@ -100,6 +101,7 @@
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/big.js": "^6.2.2",
"@types/node": "^24.10.1",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

192 changes: 187 additions & 5 deletions src/types/amount.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
import { Amount, amountFromAsset } from './amount'
import { Amount, amountFromAsset, isAmount, toAmount } from './amount'

describe('Amount', () => {
describe('fromRaw', () => {
Expand All @@ -14,6 +14,11 @@ describe('Amount', () => {
const amount = Amount.fromRaw('1000000000000000000', 18)
expect(amount.raw).toBe(1000000000000000000n)
})

it('should create from number', () => {
const amount = Amount.fromRaw(1000000, 6)
expect(amount.raw).toBe(1000000n)
})
})

describe('fromFormatted', () => {
Expand All @@ -32,6 +37,11 @@ describe('Amount', () => {
expect(amount.raw).toBe(1000000000000000n)
})

it('should parse negative number', () => {
const amount = Amount.fromFormatted('-1.5', 18)
expect(amount.raw).toBe(-1500000000000000000n)
})

it('should throw on invalid format', () => {
expect(() => Amount.fromFormatted('abc', 18)).toThrow('Invalid amount format')
})
Expand All @@ -51,6 +61,12 @@ describe('Amount', () => {
it('should return null on invalid input', () => {
expect(Amount.tryFromFormatted('abc', 18)).toBeNull()
expect(Amount.tryFromFormatted('', 18)).toBeNull()
expect(Amount.tryFromFormatted('.', 18)).toBeNull()
expect(Amount.tryFromFormatted('1.2.3', 18)).toBeNull()
})

it('should return null when exceeding decimals', () => {
expect(Amount.tryFromFormatted('1.123456789', 8)).toBeNull()
})
})

Expand All @@ -61,12 +77,11 @@ describe('Amount', () => {
})

it('should parse formatted value (with decimal point)', () => {
const amount = Amount.parse('102531.02649070', 8) // 问题中的实际值
const amount = Amount.parse('102531.02649070', 8)
expect(amount.toFormatted()).toBe('102531.0264907')
})

it('should handle the SendPage error case', () => {
// 这是导致 BUG 的实际值
const amount = Amount.parse('102531.02649070', 8)
expect(amount.isPositive()).toBe(true)
expect(amount.raw).toBe(10253102649070n)
Expand Down Expand Up @@ -94,6 +109,23 @@ describe('Amount', () => {
})
})

describe('toDisplayString', () => {
it('should format with thousands separator', () => {
const amount = Amount.fromFormatted('1234567.89', 8)
expect(amount.toDisplayString({ locale: 'en-US' })).toBe('1,234,567.89')
})

it('should respect maximumFractionDigits', () => {
const amount = Amount.fromFormatted('1.23456789', 8)
expect(amount.toDisplayString({ maximumFractionDigits: 2 })).toBe('1.23')
})

it('should use different locale', () => {
const amount = Amount.fromFormatted('1234.56', 8)
expect(amount.toDisplayString({ locale: 'de-DE' })).toBe('1.234,56')
})
})

describe('comparison', () => {
const a = Amount.fromFormatted('1.5', 18)
const b = Amount.fromFormatted('2.0', 18)
Expand Down Expand Up @@ -146,7 +178,7 @@ describe('Amount', () => {

it('mul by integer', () => {
const a = Amount.fromFormatted('1.5', 18)
expect(a.mul(2n).toFormatted()).toBe('3')
expect(a.mul(2).toFormatted()).toBe('3')
})

it('mul by float', () => {
Expand All @@ -156,7 +188,33 @@ describe('Amount', () => {

it('div by integer', () => {
const a = Amount.fromFormatted('10', 18)
expect(a.div(2n).toFormatted()).toBe('5')
expect(a.div(2).toFormatted()).toBe('5')
})

it('div rounds down', () => {
const a = Amount.fromFormatted('10', 18)
expect(a.div(3).toFormatted()).toBe('3.333333333333333333')
})

it('mod', () => {
// mod 操作在原始值上进行,10 raw % 3 = 1 raw
const a = Amount.fromRaw('10', 18)
expect(a.mod(3).toRawString()).toBe('1')
})

it('abs', () => {
const a = Amount.fromFormatted('-5', 18)
expect(a.abs().toFormatted()).toBe('5')
})

it('neg', () => {
const a = Amount.fromFormatted('5', 18)
expect(a.neg().toFormatted()).toBe('-5')
})

it('percent', () => {
const a = Amount.fromFormatted('100', 18)
expect(a.percent(5).toFormatted()).toBe('5')
})

it('min/max', () => {
Expand All @@ -167,6 +225,63 @@ describe('Amount', () => {
})
})

describe('decimals conversion', () => {
it('should convert to higher decimals', () => {
const amount = Amount.fromRaw('1000000', 6) // 1 USDC
const converted = amount.toDecimals(18)
expect(converted.decimals).toBe(18)
expect(converted.toFormatted()).toBe('1')
})

it('should convert to lower decimals', () => {
const amount = Amount.fromRaw('1000000000000000000', 18) // 1 ETH
const converted = amount.toDecimals(6)
expect(converted.decimals).toBe(6)
expect(converted.toFormatted()).toBe('1')
})

it('should preserve value during conversion', () => {
const amount = Amount.fromFormatted('123.456789', 8)
const converted = amount.toDecimals(18).toDecimals(8)
expect(converted.toFormatted()).toBe('123.456789')
})
})

describe('serialization', () => {
it('toJSON', () => {
const amount = Amount.fromFormatted('1.5', 18, 'ETH')
const json = amount.toJSON()
expect(json).toEqual({
raw: '1500000000000000000',
decimals: 18,
symbol: 'ETH',
})
})

it('toJSON without symbol', () => {
const amount = Amount.fromFormatted('1.5', 18)
const json = amount.toJSON()
expect(json).toEqual({
raw: '1500000000000000000',
decimals: 18,
})
})

it('fromJSON', () => {
const json = { raw: '1500000000000000000', decimals: 18, symbol: 'ETH' }
const amount = Amount.fromJSON(json)
expect(amount.toFormatted()).toBe('1.5')
expect(amount.symbol).toBe('ETH')
})

it('roundtrip', () => {
const original = Amount.fromFormatted('123.456789', 18, 'TOKEN')
const restored = Amount.fromJSON(original.toJSON())
expect(restored.eq(original)).toBe(true)
expect(restored.symbol).toBe(original.symbol)
})
})

describe('utility methods', () => {
it('isZero', () => {
expect(Amount.zero(18).isZero()).toBe(true)
Expand All @@ -176,6 +291,13 @@ describe('Amount', () => {
it('isPositive', () => {
expect(Amount.fromFormatted('1', 18).isPositive()).toBe(true)
expect(Amount.zero(18).isPositive()).toBe(false)
expect(Amount.fromFormatted('-1', 18).isPositive()).toBe(false)
})

it('isNegative', () => {
expect(Amount.fromFormatted('-1', 18).isNegative()).toBe(true)
expect(Amount.zero(18).isNegative()).toBe(false)
expect(Amount.fromFormatted('1', 18).isNegative()).toBe(false)
})

it('toNumber', () => {
Expand All @@ -192,6 +314,21 @@ describe('Amount', () => {
const amount = Amount.fromFormatted('1.5', 18)
expect(amount.withSymbol('ETH').symbol).toBe('ETH')
})

it('withRaw', () => {
const amount = Amount.fromFormatted('1', 18, 'ETH')
const newAmount = amount.withRaw('2000000000000000000')
expect(newAmount.toFormatted()).toBe('2')
expect(newAmount.symbol).toBe('ETH')
})

it('withAsset', () => {
const amount = Amount.fromRaw('1000000', 6)
const newAmount = amount.withAsset({ decimals: 18, assetType: 'ETH' })
expect(newAmount.decimals).toBe(18)
expect(newAmount.symbol).toBe('ETH')
expect(newAmount.toFormatted()).toBe('1')
})
})

describe('amountFromAsset', () => {
Expand All @@ -208,4 +345,49 @@ describe('Amount', () => {
expect(amount.toFormatted()).toBe('102531.0264907')
})
})

describe('isAmount', () => {
it('should return true for Amount', () => {
expect(isAmount(Amount.zero(18))).toBe(true)
})

it('should return false for non-Amount', () => {
expect(isAmount('123')).toBe(false)
expect(isAmount(123)).toBe(false)
expect(isAmount(null)).toBe(false)
expect(isAmount({})).toBe(false)
})
})

describe('toAmount', () => {
it('should return same instance if already Amount', () => {
const amount = Amount.fromFormatted('1', 18)
expect(toAmount(amount, 18)).toBe(amount)
})

it('should parse string', () => {
const amount = toAmount('1.5', 18, 'ETH')
expect(amount.toFormatted()).toBe('1.5')
expect(amount.symbol).toBe('ETH')
})
})

describe('precision edge cases', () => {
it('should handle very large numbers', () => {
const amount = Amount.fromRaw('999999999999999999999999999999999999', 18)
expect(amount.toRawString()).toBe('999999999999999999999999999999999999')
})

it('should handle very small decimals', () => {
const amount = Amount.fromFormatted('0.000000000000000001', 18)
expect(amount.raw).toBe(1n)
})

it('should maintain precision in calculations', () => {
const a = Amount.fromFormatted('0.1', 18)
const b = Amount.fromFormatted('0.2', 18)
const sum = a.add(b)
expect(sum.toFormatted()).toBe('0.3')
})
})
})
Loading