Part 2 of the Journey: Discovering the Patterns - Learning Through Pain Previous: Error Handling in MCP | Next: The Cross-Chain Tool Naming Registry
BigInt Testing Hell: Mocking Blockchain Numbers in Jest
How JavaScript’s BigInt type turned our test suites into debugging nightmares
Historical Context (November 2025): Encountered August 2025 while building testing infrastructure for blockchain servers. BigInt testing patterns for MCP weren’t documented—we solved this through experimentation.
Date: August 28, 2025 Author: Myron Koch & Claude Code Category: Testing War Stories
The Innocent Beginning
// Simple test, right?
describe('getBalance', () => {
it('should return wallet balance', async () => {
const mockClient = {
getBalance: jest.fn().mockResolvedValue(1000000000000000000)
};
const result = await handleGetBalance({ address: '0x123' }, mockClient);
expect(result.content[0].text).toContain('1000000000000000000');
});
});
Test result:
FAIL tests/core.test.ts
● getBalance › should return wallet balance
TypeError: Cannot mix BigInt and other types, use explicit conversions
Welcome to BigInt hell.
The Problem
Blockchain numbers are HUGE:
- Wei: 1 ETH = 1,000,000,000,000,000,000 wei
- Satoshi: 1 BTC = 100,000,000 satoshi
- Lamports: 1 SOL = 1,000,000,000 lamports
JavaScript’s Number type: Max safe integer = 9,007,199,254,740,991
One ETH in wei: 1,000,000,000,000,000,000
JavaScript Number literally cannot hold blockchain values.
Enter BigInt
JavaScript’s BigInt to the rescue! But with a catch:
// BigInt rules that will ruin your day:
const a = BigInt(10);
const b = 20;
a + b; // 💥 TypeError: Cannot mix BigInt and other types
a + BigInt(b); // ✅ 30n
JSON.stringify({ value: a }); // 💥 TypeError: Do not know how to serialize a BigInt
console.log(a); // ✅ Works
`Value: ${a}`; // ✅ Works (coercion)
a.toString(); // ✅ Works
The Jest Mocking Nightmare
Attempt 1: Mock with Numbers
mockClient.getBalance.mockResolvedValue(1000000000000000000);
// 💥 Runtime: Cannot convert 1000000000000000000 to BigInt
Attempt 2: Mock with BigInt
mockClient.getBalance.mockResolvedValue(BigInt('1000000000000000000'));
// 💥 Jest: Cannot serialize BigInt in snapshot
Attempt 3: Mock with String
mockClient.getBalance.mockResolvedValue('1000000000000000000');
// 💥 Application: Expected BigInt, got string
Attempt 4: The “n” Notation
mockClient.getBalance.mockResolvedValue(1000000000000000000n);
// 💥 Jest: Unexpected token 'n'
The Type System Chaos
TypeScript made it worse:
interface Balance {
value: bigint; // TypeScript type
}
const mockBalance: Balance = {
value: BigInt(1000) // Runtime value
};
// In tests:
expect(result.value).toBe(BigInt(1000));
// 💥 Received: serializes to the same string
expect(result.value).toBe(1000n);
// 💥 Parsing error: Unexpected token
expect(result.value.toString()).toBe('1000');
// ✅ Finally works, but loses type safety
The JSON.stringify Apocalypse
Our MCP servers return JSON responses:
return {
content: [{
type: 'text',
text: JSON.stringify({ balance: BigInt(1000) })
// 💥 TypeError: Do not know how to serialize a BigInt
}]
};
The “fix”:
// The BigInt JSON hack everyone uses
JSON.stringify(obj, (_, v) => typeof v === 'bigint' ? v.toString() : v);
But this breaks EVERYWHERE:
// Have to add replacer to every JSON.stringify
JSON.stringify(data, bigIntReplacer);
JSON.stringify(response, bigIntReplacer);
JSON.stringify(config, bigIntReplacer);
// Miss one? 💥 Runtime explosion
The Testing Solutions That Actually Work
Solution 1: The BigInt Test Helper
// tests/helpers/bigint.ts
export function mockBigInt(value: string | number): any {
const bigIntValue = BigInt(value);
// Return object that behaves like BigInt in tests
return {
toString: () => bigIntValue.toString(),
valueOf: () => bigIntValue,
[Symbol.toStringTag]: 'BigInt',
// Add math operations as needed
add: (other: any) => mockBigInt((bigIntValue + BigInt(other)).toString()),
sub: (other: any) => mockBigInt((bigIntValue - BigInt(other)).toString()),
mul: (other: any) => mockBigInt((bigIntValue * BigInt(other)).toString()),
};
}
// Usage in tests:
mockClient.getBalance.mockResolvedValue(mockBigInt('1000000000000000000'));
Solution 2: The String Protocol
// Everywhere in the app: Convert BigInt to string immediately
async function getBalance(address: string): Promise<string> {
const balance = await web3.eth.getBalance(address); // Returns BigInt
return balance.toString(); // Always stringify immediately
}
// In tests: Just use strings
mockClient.getBalance.mockResolvedValue('1000000000000000000');
Solution 3: The Wrapper Pattern
// Create a Balance class that handles serialization
class Balance {
private value: bigint;
constructor(value: string | bigint) {
this.value = BigInt(value);
}
toString(): string {
return this.value.toString();
}
toJSON(): string {
return this.toString();
}
add(other: Balance): Balance {
return new Balance(this.value + other.value);
}
}
// Tests become manageable
const mockBalance = new Balance('1000000000000000000');
mockClient.getBalance.mockResolvedValue(mockBalance);
The Jest Configuration Battle
Getting Jest to handle BigInt:
// jest.config.cjs
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
globals: {
'ts-jest': {
tsconfig: {
target: 'ES2020', // Supports BigInt
},
},
},
// Custom serializer for BigInt
snapshotSerializers: ['./tests/bigint-serializer.js'],
};
// tests/bigint-serializer.js
module.exports = {
serialize(val) {
return val.toString() + 'n';
},
test(val) {
return typeof val === 'bigint';
},
};
Real-World Test Failures We Hit
The Comparison Trap
// Test
expect(result.balance).toBeGreaterThan(0);
// 💥 Cannot compare BigInt with number
// Fix
expect(result.balance).toBeGreaterThan(BigInt(0));
The Math Operation Disaster
// Test calculating fees
const fee = mockBalance * 0.01;
// 💥 Cannot mix BigInt and other types
// Fix
const fee = (mockBalance * BigInt(1)) / BigInt(100);
The Array Sum Nightmare
// Summing balances
const total = balances.reduce((sum, bal) => sum + bal, 0);
// 💥 Cannot mix BigInt and number
// Fix
const total = balances.reduce((sum, bal) => sum + bal, BigInt(0));
The Patterns We Developed
1. The BigInt Factory
// utils/bigint.ts
export function toBigInt(value: string | number | bigint): bigint {
if (typeof value === 'bigint') return value;
if (typeof value === 'string') return BigInt(value);
if (Number.isSafeInteger(value)) return BigInt(value);
throw new Error(`Cannot convert ${value} to BigInt safely`);
}
// Always use the factory
const balance = toBigInt(mockValue);
2. The Safe Math Module
// utils/safeMath.ts
export const SafeMath = {
add: (a: string, b: string): string => {
return (BigInt(a) + BigInt(b)).toString();
},
sub: (a: string, b: string): string => {
return (BigInt(a) - BigInt(b)).toString();
},
mul: (a: string, b: string): string => {
return (BigInt(a) * BigInt(b)).toString();
},
div: (a: string, b: string): string => {
return (BigInt(a) / BigInt(b)).toString();
},
};
3. The Test Data Builder
// tests/builders/blockchain.ts
export class BlockchainDataBuilder {
static balance(value: string = '1000000000000000000'): any {
return {
raw: BigInt(value),
toString: () => value,
toJSON: () => value,
formatted: () => `${BigInt(value) / BigInt(1e18)} ETH`, // BigInt-safe division
};
}
static transaction(overrides = {}): any {
return {
hash: '0x123...',
value: this.balance(),
gasPrice: BigInt('20000000000'),
gasLimit: BigInt('21000'),
...overrides,
};
}
}
The Current State of Our Tests
After months of battle:
// A working test that handles BigInt properly
describe('Ethereum Balance Operations', () => {
let mockClient: any;
beforeEach(() => {
mockClient = {
getBalance: jest.fn(),
// Use string protocol for consistency
formatBalance: jest.fn((wei: string) => {
const eth = Number(wei) / 1e18;
return `${eth.toFixed(4)} ETH`;
}),
};
});
it('should handle large balances correctly', async () => {
const hugeBalance = '999999999999999999999999'; // Whale alert!
mockClient.getBalance.mockResolvedValue(hugeBalance);
const result = await handleGetBalance(
{ address: '0xwhale' },
mockClient
);
const parsed = JSON.parse(result.content[0].text);
expect(parsed.balance).toBe(hugeBalance);
expect(parsed.formatted).toContain('ETH');
// Validate it's actually a huge number
expect(BigInt(parsed.balance)).toBeGreaterThan(
BigInt('999999999999999999999998')
);
});
});
The Checklist for BigInt Testing
When testing blockchain code:
- Never use JavaScript numbers for wei/satoshi/lamports
- Always convert BigInt to string before JSON.stringify
- Mock with strings, convert to BigInt in implementation
- Use explicit BigInt() conversions, never assume
- Create test helpers for BigInt operations
- Configure Jest for ES2020 or later
- Add custom serializers for snapshots
- Test with maximum values (2^256 - 1)
- Test math operations separately
- Document BigInt handling for team
The Philosophical Question
Should blockchain values be BigInt at all?
The String Camp says: “Just use strings everywhere. No type confusion.”
The BigInt Camp says: “We need math operations. BigInt is correct.”
The Reality: We use BigInt internally, convert to strings at boundaries.
The Future
When will this get better?
- JSON.stringify BigInt support: Proposed, not implemented
- Jest native BigInt support: Getting better each version
- TypeScript inference: Still can’t infer
123nsyntax
Until then, we live in BigInt testing hell.
References
- BigInt helpers:
/tests/helpers/bigint.ts - Safe math module:
/src/utils/safeMath.ts - Test builders:
/tests/builders/ - Jest config:
/jest.config.cjs
This is part of our ongoing series documenting architectural patterns and insights from building the Blockchain MCP Server Ecosystem. Some battles you don’t win, you just survive.
Related Reading
Prerequisites
- Error Handling in MCP: Where Do Errors Actually Go? - Understanding how errors are structured is key to debugging them.
Next Steps
- The Cross-Chain Tool Naming Registry - After solving type issues, we tackled the next big problem: inconsistent naming.
Deep Dives
- The TypeScript Migration Journey - This post details the broader context where the BigInt problem first emerged.
- The AI Dream Team - See how a robust testing strategy for issues like BigInt enables AI agents to effectively fix bugs.