DexNumber is a numeric class extending BigNumber.js that manages decimal arithmetic with state-aware precision, primarily for blockchain and DeFi applications. It handles automatic decimal scaling, maintains atomic/decimal state tracking, and provides consistent conversion between common blockchain numeric representations (wei/ether, decimal/atomic, etc). Built for scenarios requiring high-precision decimal math with proper decimal place management.
- Features
- Installation
- Core Concepts
- Basic Usage
- Advanced Features
- Format Conversions
- Mathematical Operations
- Comparison Methods
- Best Practices
- TypeScript Support
- Contributing
- 🔢 Decimal Inheritance: First operand's decimals determine result precision, critical for token calculations
- ⚡ High-Precision Core: 60 decimal place precision with configurable rounding modes
- ⚖️ Cross-Decimal Handling: Automatic scaling between different token decimals (e.g., 18 decimal ETH to 6 decimal USDC)
- 🛡️ Precision Guards: Prevents common decimal errors in DeFi calculations
- 🔄 Atomic/Decimal States: Seamless handling of both contract values (wei) and human-readable amounts (ether)
- 🎭 Auto State Detection: Maintains awareness of shifted/unshifted states during operations
- 🔐 Immutable Operations: Guarantees value consistency in complex DeFi calculations
- ✅ Validation: Ensures correct decimal state for contract interactions
- ⚗️ Contract Ready: Direct conversion to formats needed for smart contract calls
- 🔄 Universal Parsing: Handles all common blockchain number formats (BigNumber, Ethers BigNumber, bigint, hex)
- 📏 Token Units: Built-in support for wei, gwei, ether, and custom decimal places
- 🎨 Display Formatting: Locale-aware formatting for UI display with decimal place control
- 🏗️ Static Factories: Intuitive creation methods for different number states
- 📝 TypeScript Ready: Comprehensive type definitions for safe DApp development
- 🔍 Debugging Support: Clear state tracking and value inspection
- 📚 Predictable API: Consistent behavior across all mathematical operations
- 🧮 BigNumber Foundation: Built on proven BigNumber.js library for reliable calculations
- ⚡ Performance: Optimized for high-volume DeFi calculations
- 🧪 Testing Friendly: Deterministic results for reliable test assertions
- 🔌 Ethers.js Integration: Seamless interaction with the most popular Ethereum library
pnpm add @dex-toolkit/number
# or
npm install @dex-toolkit/number
# or
yarn add @dex-toolkit/number
# or
bun add @dex-toolkit/numberOne of the most important rules in DexNumber is decimal inheritance. The decimals of the result will always match the decimals of the first operand in any operation. The decimals are for shift management and display/formatting. All operations work with and store 60 decimal places.
const num18 = DexNumber.fromUnshifted('1.5', 18); // 18 decimals
const num6 = DexNumber.fromUnshifted('2.5', 6); // 6 decimals
// Result will have 18 decimals
const result1 = num18.multipliedBy(num6);
console.log(result1.decimals); // 18
// Result will have 6 decimals
const result2 = num6.multipliedBy(num18);
console.log(result2.decimals); // 6
// Chaining operations
const result3 = num18.multipliedBy(num6).dividedBy(num6);
console.log(result3.decimals); // 18DexNumber maintains three possible states:
shifted: Values in their smallest unit (e.g., wei)unshifted: Human-readable values (e.g., ETH)neutral: Initial state before shifting operations
const amount = DexNumber.fromUnshifted('1.5', 18);
console.log(amount.shiftedState); // 'unshifted'
const shifted = amount.shift();
console.log(shifted.shiftedState); // 'shifted'
console.log(shifted.toString()); // '1500000000000000000'Just like decimal inheritance, the results shifted state will be the same as the first operand in any operation.
const num18 = DexNumber.fromUnshifted('1.5', 18); // 18 decimals
const num6 = DexNumber.fromShifted('2500000', 6); // 6 decimals
// Result will be unshifted
const result1 = num18.multipliedBy(num6);
console.log(result1.decimals); // 18
console.log(result1.shiftedState); // 'unshifted'
// Result will be shifted
const result2 = num6.multipliedBy(num18);
console.log(result2.decimals); // 6
console.log(result2.shiftedState); // 'shifted'DexNumber uses BigNumber.js with extended precision settings:
- 60 decimal places for internal calculations
- ROUND_DOWN as default rounding mode
- Automatic scaling for cross-decimal operations
// From human-readable values
const amount1 = DexNumber.fromUnshifted('1.5', 18);
const amount2 = DexNumber.fromUnshifted('100', 'gwei'); // 9 decimals
// From atomic values (e.g., wei)
const amount3 = DexNumber.fromShifted('1500000000000000000', 18);
const amount4 = DexNumber.fromShifted('100000000000', 'gwei');
// From other formats
const amount5 = DexNumber.fromBigInt(BigInt('1500000000000000000'), 18);
const amount6 = DexNumber.fromString('1.5', 18, 'unshifted');// USDC Example (6 decimals)
const usdc = DexNumber.fromUnshifted('100.50', 6);
// Get atomic units for contract calls
const atomic = usdc.shift();
console.log(atomic.toString()); // '100500000'
// Convert back to human-readable
const readable = atomic.unshift();
console.log(readable.toString()); // '100.5'
// Format with specific decimals
console.log(readable.toReadableString(2)); // '100.50'const token = DexNumber.fromUnshifted('1234.5678', 18);
// Check states
console.log(token.isUnshifted()); // true
console.log(token.isShifted()); // false
console.log(token.isNeutral()); // false
// Convert between states
const atomic = token.shift();
console.log(atomic.toWeiString()); // '1234567800000000000000'
const decimal = atomic.unshift();
console.log(decimal.toDecimalString()); // '1234.5678'// Different token decimals
const dai = DexNumber.fromUnshifted('100', 18); // 18 decimals
const usdc = DexNumber.fromUnshifted('100', 6); // 6 decimals
// Result inherits decimals from first operand
const ratio1 = dai.dividedBy(usdc); // 18 decimals result
const ratio2 = usdc.dividedBy(dai); // 6 decimals result
// Mixed states work automatically
const shifted = DexNumber.fromShifted('1000000', 6);
const unshifted = DexNumber.fromUnshifted('1.5', 18);
const result = shifted.multipliedBy(unshifted); // Handles conversion internally// Calculate price impact across multiple hops
const reserve0 = DexNumber.fromUnshifted('1000000', 18);
const reserve1 = DexNumber.fromUnshifted('1000', 18);
const amount = DexNumber.fromUnshifted('1000', 18);
const fee = DexNumber.fromUnshifted('0.003', 18); // 0.3%
const amountWithFee = amount.multipliedBy(
DexNumber.fromUnshifted('1', amount.decimals).minus(fee)
);
const numerator = amountWithFee.multipliedBy(reserve1);
const denominator = reserve0.plus(amountWithFee);
const outputAmount = numerator.dividedBy(denominator);// Calculate precise pool share percentage
const userLiquidity = DexNumber.fromUnshifted('15', 18);
const totalLiquidity = DexNumber.fromUnshifted('105', 18);
const sharePercentage = userLiquidity
.dividedBy(totalLiquidity)
.multipliedBy(DexNumber.fromUnshifted('100', 18));
console.log(sharePercentage.toDecimalString()); // '14.285714285714285714'const number = DexNumber.fromUnshifted('1234.5678', 18);
const formats = {
readable: number.toTradeFormat({
type: 'readable',
options: {
decimalPlaces: 2,
locales: 'de-DE'
}
}), // '1.234,57'
decimal: number.toTradeFormat({
type: 'decimal',
options: { decimalPlaces: 4 }
}), // '1234.5678'
wei: number.toTradeFormat({ type: 'wei' }),
hex: number.toTradeFormat({ type: 'hex' }),
bigint: number.toTradeFormat({ type: 'bigint' }),
ethers: number.toTradeFormat({ type: 'ethers' }),
bignumber: number.toTradeFormat({ type: 'bignumber' })
};const amount = DexNumber.fromUnshifted('1234.5678', 18);
// Different format options
console.log(amount.toReadableString()); // '1,234.567800000000000000'
console.log(amount.toReadableString(2)); // '1,234.57'
console.log(amount.toDecimalString(4)); // '1234.5678'
console.log(amount.toWeiString()); // '1234567800000000000000'
console.log(amount.toHexString()); // '0x422ca8b0a00a425000000'
// Locale-specific formatting
console.log(amount.toReadableString(2)); // '1,234.57'
console.log(amount.toReadableString(2, 'de-DE')); // '1.234,57'
console.log(amount.toReadableString(2, 'fr-FR')); // '1 234,57'const amount = DexNumber.fromUnshifted('1.5', 18);
// Convert for contract interactions
const ethersBN = amount.toEthersBigNumber();
const bigInt = amount.toBigInt();
const hexString = amount.toHexString();
// For storage/serialization
const serialized = amount.toSerialized();
const restored = DexNumber.fromSerialized(serialized);You can easily convert between BN.js and DexNumber using the existing static methods:
import BN from 'bn.js';
// Converting from BN to DexNumber
const bnValue = new BN('1000000000000000000'); // 1 ETH in wei
const dexNumber = DexNumber.fromShifted(bnValue.toString(), 18);
// Converting from DexNumber back to BN
const dexValue = DexNumber.fromUnshifted('1.5', 18);
const backToBN = new BN(dexValue.shift().toFixed());
// Working with Web3.js values
const web3Amount = new BN('500000000'); // value from Web3.js
const dexAmount = DexNumber.fromShifted(web3Amount.toString(), 6); // e.g., USDC
console.log(dexAmount.unshift().toString()); // '500'
// Converting back for Web3.js contract calls
const dexResult = DexNumber.fromUnshifted('750.5', 6);
const web3Ready = new BN(dexResult.shift().toFixed());Check out the tests for more examples.
const a = DexNumber.fromUnshifted('10.5', 18);
const b = DexNumber.fromUnshifted('3.2', 18);
console.log(a.plus(b).toString()); // '13.7'
console.log(a.minus(b).toString()); // '7.3'
console.log(a.multipliedBy(b).toString()); // '33.6'
console.log(a.dividedBy(b).toString()); // '3.28125'const num = DexNumber.fromUnshifted('10.5', 18);
console.log(num.pow(2).toString()); // '110.25'
console.log(num.sqrt().toString()); // '3.24037...'
console.log(num.absoluteValue().toString()); // '10.5'
// Precision control
console.log(num.decimalPlaces(2).toString()); // '10.50'
console.log(num.integerValue().toString()); // '10'const decimals = 18;
const numbers = ['10', '20', '30'].map(n =>
DexNumber.fromUnshifted(n, decimals)
);
console.log(DexNumber.max(decimals, ...numbers).toString()); // '30'
console.log(DexNumber.min(decimals, ...numbers).toString()); // '10'
console.log(DexNumber.sum(decimals, ...numbers).toString()); // '60'const x = DexNumber.fromUnshifted('100', 18);
const y = DexNumber.fromUnshifted('150', 18);
console.log(x.isLessThan(y)); // true
console.log(x.isGreaterThanOrEqualTo(y)); // false
console.log(x.isEqualTo(y)); // false
// Works across different decimals
const z = DexNumber.fromUnshifted('100', 6);
console.log(x.isEqualTo(z)); // true-
Always Use Appropriate Initialization:
// For human input/display const userAmount = DexNumber.fromUnshifted('1.5', 18); // For contract data const contractAmount = DexNumber.fromShifted('1500000000000000000', 18);
-
Match Token Decimals:
const usdc = DexNumber.fromUnshifted('100', 6); // USDC has 6 decimals const dai = DexNumber.fromUnshifted('100', 18); // DAI has 18 decimals
-
Use Appropriate Conversion Methods:
// For contract calls amount.toWeiString(); // Safe for any size amount.toEthersBigNumber(); // For ethers.js v4 and v5 integration amount.toBigInt(); // For bigint operations amount.toHexString(); // For hex input
-
Handle Cross-Decimal Operations Carefully:
// Remember: result inherits decimals from first operand const ratio = usdc.dividedBy(dai); // Result will have 6 decimals
import {
DexNumber,
DexNumberUnit,
DexNumberState,
SerializedDexNumber,
TradeFormat,
TradeFormatValue
} from '@dex-toolkit/number';
// Type-safe initialization
const amount: DexNumber = DexNumber.fromUnshifted('1.5', 18);
const unit: DexNumberUnit = 'ether';
const state: DexNumberState = amount.shiftedState;
// Type-safe format conversion
const formatted: TradeFormatValue<'readable'> = amount.toTradeFormat({
type: 'readable',
options: { decimalPlaces: 2 }
});Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the ISC License - see the LICENSE file for details.