Skip to content

Commit 1518735

Browse files
committed
fix(playground): pin viem to 2.46.2 to avoid Math.pow BigInt error
esm.sh was resolving viem@^2.0.0 to 2.47.x which updated ox to 0.14.1. That release is served by esm.sh transpiled for older browser targets, turning BigInt exponentiation (**) into Math.pow() — which throws 'TypeError: Cannot convert a BigInt value to a number'. Pin to 2.46.2 (the version locked in the monorepo lockfile). Verified: all 6 playground smoke tests pass locally at 2.46.2 with zero Math.pow errors (formatUnits, readContract, getBalance, HD derivation). test(core): add playground-examples.test.ts smoke tests Run the exact code from each docs Playground to catch regressions.
1 parent 66afab5 commit 1518735

File tree

2 files changed

+203
-1
lines changed

2 files changed

+203
-1
lines changed

docs-site/components/Playground.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,11 @@ interface PlaygroundProps {
101101
}
102102

103103
const DEFAULT_DEPS: Record<string, string> = {
104-
viem: '^2.0.0',
104+
// Pin viem to the exact version the monorepo uses.
105+
// Loose ranges resolve via esm.sh to the latest release, which may be
106+
// compiled for older browsers (transpiling BigInt ** to Math.pow) and will
107+
// throw "Cannot convert a BigInt value to a number".
108+
viem: '2.46.2',
105109
'@cfxdevkit/core': '^1.0.16',
106110
}
107111

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/*
2+
* Copyright 2025 Conflux DevKit Team
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Smoke tests that run the exact code shown in the docs Playground examples.
19+
* These run against the LOCAL build (not esm.sh) so we can determine:
20+
* - PASS → the code is correct; the bug is in Sandpack/esm.sh bundling
21+
* - FAIL → the bug is in the code itself or a transitive dep
22+
*
23+
* Tests are marked with the `network` pool configuration so vitest knows
24+
* they require real network access (Conflux eSpace testnet).
25+
*/
26+
27+
import { describe, it, expect } from 'vitest';
28+
import {
29+
EspaceClient,
30+
EVM_TESTNET,
31+
ERC20_ABI,
32+
formatUnits,
33+
generateMnemonic,
34+
validateMnemonic,
35+
deriveAccounts,
36+
deriveAccount,
37+
} from '../index.js';
38+
39+
// ── Shared client ─────────────────────────────────────────────────────────────
40+
const client = new EspaceClient({
41+
chainId: EVM_TESTNET.id,
42+
rpcUrl: EVM_TESTNET.rpcUrls.default.http[0],
43+
});
44+
45+
// WCFX on testnet — always live
46+
const WCFX = '0x2ED3dddae5B2F321AF0806181FBFA6D049Be47d8';
47+
const HOLDER = '0x85d80245dc02f5A89589e1f19c5c718E405B56AA';
48+
49+
// ── Example: espace-block-number ─────────────────────────────────────────────
50+
describe('playground: espace-block-number', () => {
51+
it('reads block number, chain id, gas price, and latest block header', async () => {
52+
const [connected, blockNumber, onChainId, gasPrice] = await Promise.all([
53+
client.isConnected(),
54+
client.getBlockNumber(),
55+
client.getChainId(),
56+
client.getGasPrice(),
57+
]);
58+
59+
console.log('Connected :', connected);
60+
console.log('Block # :', blockNumber.toString());
61+
console.log('Chain ID :', onChainId);
62+
console.log('Gas Price :', formatUnits(gasPrice, 9), 'Gwei');
63+
64+
expect(connected).toBe(true);
65+
expect(typeof blockNumber).toBe('bigint');
66+
expect(blockNumber).toBeGreaterThan(0n);
67+
expect(onChainId).toBe(EVM_TESTNET.id);
68+
expect(typeof gasPrice).toBe('bigint');
69+
70+
const block = await client.publicClient.getBlock({ blockTag: 'latest' });
71+
console.log('Hash :', (block.hash?.slice(0, 20) ?? 'n/a') + '...');
72+
console.log('Gas Used :', block.gasUsed.toString());
73+
console.log('Timestamp :', new Date(Number(block.timestamp) * 1000).toUTCString());
74+
75+
expect(block.gasUsed).toBeGreaterThanOrEqual(0n);
76+
expect(block.timestamp).toBeGreaterThan(0n);
77+
}, 30_000);
78+
});
79+
80+
// ── Example: read-balance ─────────────────────────────────────────────────────
81+
describe('playground: read-balance', () => {
82+
it('reads native CFX balance', async () => {
83+
const cfx = await client.getBalance(HOLDER);
84+
console.log('Balance:', Number(cfx).toFixed(6), 'CFX');
85+
expect(typeof cfx).toBe('string');
86+
}, 20_000);
87+
88+
it('reads ERC-20 token balance via readContract', async () => {
89+
const [name, symbol, decimals, raw] = await Promise.all([
90+
client.publicClient.readContract({ address: WCFX, abi: ERC20_ABI, functionName: 'name' }),
91+
client.publicClient.readContract({ address: WCFX, abi: ERC20_ABI, functionName: 'symbol' }),
92+
client.publicClient.readContract({ address: WCFX, abi: ERC20_ABI, functionName: 'decimals' }),
93+
client.publicClient.readContract({
94+
address: WCFX,
95+
abi: ERC20_ABI,
96+
functionName: 'balanceOf',
97+
args: [HOLDER],
98+
}),
99+
]);
100+
101+
console.log('Token :', name, `(${symbol})`);
102+
console.log('Decimals:', decimals);
103+
console.log('Balance :', formatUnits(raw, decimals), symbol);
104+
105+
expect(typeof name).toBe('string');
106+
expect(typeof symbol).toBe('string');
107+
expect(typeof decimals).toBe('number');
108+
expect(typeof raw).toBe('bigint');
109+
// formatUnits must not throw — this is where Math.pow BigInt error would surface
110+
expect(typeof formatUnits(raw, decimals)).toBe('string');
111+
}, 20_000);
112+
});
113+
114+
// ── Example: erc20-info ───────────────────────────────────────────────────────
115+
describe('playground: erc20-info', () => {
116+
it('reads all ERC-20 metadata and allowance without throwing', async () => {
117+
const SPENDER = '0x33e5E5B262e5d8eBC443E1c6c9F14215b020554d';
118+
119+
const [name, symbol, decimals, totalSupply] = await Promise.all([
120+
client.publicClient.readContract({ address: WCFX, abi: ERC20_ABI, functionName: 'name' }),
121+
client.publicClient.readContract({ address: WCFX, abi: ERC20_ABI, functionName: 'symbol' }),
122+
client.publicClient.readContract({ address: WCFX, abi: ERC20_ABI, functionName: 'decimals' }),
123+
client.publicClient.readContract({ address: WCFX, abi: ERC20_ABI, functionName: 'totalSupply' }),
124+
]);
125+
126+
console.log('Name :', name);
127+
console.log('Symbol :', symbol);
128+
console.log('Decimals :', decimals);
129+
console.log('Total Supply :', formatUnits(totalSupply, decimals), symbol);
130+
131+
const [balance, allowance] = await Promise.all([
132+
client.publicClient.readContract({
133+
address: WCFX,
134+
abi: ERC20_ABI,
135+
functionName: 'balanceOf',
136+
args: [HOLDER],
137+
}),
138+
client.publicClient.readContract({
139+
address: WCFX,
140+
abi: ERC20_ABI,
141+
functionName: 'allowance',
142+
args: [HOLDER, SPENDER],
143+
}),
144+
]);
145+
146+
console.log('Balance :', formatUnits(balance, decimals), symbol);
147+
console.log('Allowance:', formatUnits(allowance, decimals), symbol);
148+
149+
expect(typeof totalSupply).toBe('bigint');
150+
expect(typeof balance).toBe('bigint');
151+
expect(typeof allowance).toBe('bigint');
152+
// formatUnits must not throw
153+
expect(formatUnits(totalSupply, decimals)).toMatch(/^\d/);
154+
}, 30_000);
155+
});
156+
157+
// ── Example: send-cfx (HD wallet — no network needed) ────────────────────────
158+
describe('playground: send-cfx (HD wallet)', () => {
159+
it('generates and validates mnemonics without throwing', () => {
160+
const mnemonic = generateMnemonic();
161+
const mnemonic24 = generateMnemonic(256);
162+
163+
console.log('12-word:', mnemonic);
164+
console.log('24-word:', mnemonic24);
165+
166+
const v = validateMnemonic(mnemonic);
167+
console.log('valid:', v.valid, 'words:', v.wordCount);
168+
169+
const bad = validateMnemonic('this is not a valid bip39 phrase at all');
170+
console.log('bad valid:', bad.valid, 'words:', bad.wordCount);
171+
172+
expect(v.valid).toBe(true);
173+
expect(v.wordCount).toBe(12);
174+
expect(bad.valid).toBe(false);
175+
});
176+
177+
it('derives HD accounts for both eSpace and Core Space', () => {
178+
const mnemonic = generateMnemonic();
179+
const accounts = deriveAccounts(mnemonic, { count: 3 });
180+
181+
console.log('-- Derived Accounts (first 3) --');
182+
for (const a of accounts) {
183+
console.log(`[${a.index}] eSpace : ${a.evmAddress}`);
184+
console.log(` Core : ${a.coreAddress}`);
185+
}
186+
187+
expect(accounts).toHaveLength(3);
188+
expect(accounts[0].evmAddress).toMatch(/^0x[0-9a-fA-F]{40}$/);
189+
// Core Space addresses use base32 encoding; prefix varies by chain ID
190+
// (cfx: mainnet, cfxtest: testnet, net<id>: other)
191+
expect(accounts[0].coreAddress).toMatch(/^(cfx:|cfxtest:|net\d+:)/);
192+
193+
const acc5 = deriveAccount(mnemonic, 5);
194+
console.log('Account #5 eSpace:', acc5.evmAddress);
195+
console.log('Account #5 Core :', acc5.coreAddress);
196+
expect(acc5.index).toBe(5);
197+
});
198+
});

0 commit comments

Comments
 (0)