11'use client'
22
3- import { useState } from 'react'
43import {
54 SandpackProvider ,
6- SandpackLayout ,
75 SandpackCodeEditor ,
86 SandpackConsole ,
97 SandpackPreview ,
@@ -34,16 +32,8 @@ const NETWORKS = {
3432 rpcUrl : 'https://evmtestnet.confluxrpc.com' ,
3533 blockExplorer : 'https://evmtestnet.confluxscan.io' ,
3634 } ,
37- local : {
38- label : 'Local (localhost:8545)' ,
39- chainId : 2030 ,
40- rpcUrl : 'http://localhost:8545' ,
41- blockExplorer : '' ,
42- } ,
4335} as const
4436
45- type Network = keyof typeof NETWORKS
46-
4737// ── Run button — must be inside SandpackProvider ─────────────────────────────
4838function RunButton ( ) {
4939 const { sandpack } = useSandpack ( )
@@ -52,6 +42,7 @@ function RunButton() {
5242
5343 return (
5444 < button
45+ type = "button"
5546 onClick = { refresh }
5647 disabled = { busy }
5748 style = { {
@@ -76,14 +67,9 @@ function RunButton() {
7667 )
7768}
7869
79- // ── Network toggle ────────────────────────────────────────────────────────────
80- function NetworkToggle ( {
81- network,
82- onChange,
83- } : {
84- network : Network
85- onChange : ( n : Network ) => void
86- } ) {
70+ // ── Network label ─────────────────────────────────────────────────────────────
71+ function NetworkLabel ( ) {
72+ const cfg = NETWORKS . testnet
8773 return (
8874 < div
8975 style = { {
@@ -93,38 +79,23 @@ function NetworkToggle({
9379 fontSize : '12px' ,
9480 fontFamily : 'ui-monospace, monospace' ,
9581 color : T . muted ,
96- minWidth : 0 ,
97- overflow : 'hidden' ,
9882 } }
9983 >
10084 < span style = { { whiteSpace : 'nowrap' } } > network:</ span >
101- { ( Object . keys ( NETWORKS ) as Network [ ] ) . map ( ( key ) => (
102- < button
103- key = { key }
104- onClick = { ( ) => onChange ( key ) }
105- style = { {
106- padding : '2px 9px' ,
107- borderRadius : '4px' ,
108- border : 'none' ,
109- cursor : 'pointer' ,
110- fontFamily : 'inherit' ,
111- fontSize : '11px' ,
112- whiteSpace : 'nowrap' ,
113- background : network === key ? T . activeBg : T . pillBg ,
114- color : network === key ? T . activeText : T . text ,
115- } }
116- >
117- { NETWORKS [ key ] . label }
118- </ button >
119- ) ) }
12085 < span
12186 style = { {
122- color : T . muted ,
123- fontSize : '10px' ,
87+ padding : '2px 9px' ,
88+ borderRadius : '4px' ,
89+ background : T . activeBg ,
90+ color : T . activeText ,
91+ fontSize : '11px' ,
12492 whiteSpace : 'nowrap' ,
12593 } }
12694 >
127- · chain { NETWORKS [ network ] . chainId }
95+ { cfg . label }
96+ </ span >
97+ < span style = { { fontSize : '10px' , whiteSpace : 'nowrap' } } >
98+ · chain { cfg . chainId }
12899 </ span >
129100 </ div >
130101 )
@@ -141,9 +112,130 @@ interface PlaygroundProps {
141112
142113const DEFAULT_DEPS : Record < string , string > = {
143114 viem : '^2.0.0' ,
144- // @cfxdevkit /* packages omitted — not yet on npm; examples use viem directly
145115}
146116
117+ // ── Browser-compatible shim ────────────────────────────────────────────────
118+ // @cfxdevkit /core uses node:events (via ClientManager) which Sandpack's bundler
119+ // cannot polyfill. This shim re-implements the same public API surface using
120+ // viem directly. In a real project you import from '@cfxdevkit/core'.
121+ const CFXDEVKIT_SHIM = `// cfxdevkit.ts — browser shim for @cfxdevkit/core
122+ // Real project: replace './cfxdevkit' with '@cfxdevkit/core'
123+ import {
124+ createPublicClient, createWalletClient, http,
125+ formatEther, formatUnits, parseEther, parseUnits, defineChain,
126+ } from 'viem'
127+ import { privateKeyToAccount } from 'viem/accounts'
128+
129+ export { formatEther, formatUnits, parseEther, parseUnits } from 'viem'
130+
131+ export const ERC20_ABI = [
132+ { name: 'name', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'string' }] },
133+ { name: 'symbol', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'string' }] },
134+ { name: 'decimals', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint8' }] },
135+ { name: 'totalSupply', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
136+ { name: 'balanceOf', type: 'function', stateMutability: 'view', inputs: [{ name: 'account', type: 'address' }], outputs: [{ type: 'uint256' }] },
137+ { name: 'allowance', type: 'function', stateMutability: 'view', inputs: [{ name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }], outputs: [{ type: 'uint256' }] },
138+ { name: 'transfer', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }], outputs: [{ type: 'bool' }] },
139+ { name: 'approve', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }], outputs: [{ type: 'bool' }] },
140+ { name: 'transferFrom',type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'from', type: 'address' }, { name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }], outputs: [{ type: 'bool' }] },
141+ ] as const
142+
143+ function makeChain(chainId, rpcUrl) {
144+ const names = { 71: 'Conflux eSpace Testnet', 1030: 'Conflux eSpace' }
145+ return defineChain({
146+ id: chainId,
147+ name: names[chainId] || 'Conflux eSpace (' + chainId + ')',
148+ nativeCurrency: { name: 'Conflux', symbol: 'CFX', decimals: 18 },
149+ rpcUrls: { default: { http: [rpcUrl] } },
150+ })
151+ }
152+
153+ export class EspaceClient {
154+ public publicClient
155+ public chainId
156+ public rpcUrl
157+ public address = ''
158+
159+ constructor({ chainId, rpcUrl }) {
160+ this.chainId = chainId
161+ this.rpcUrl = rpcUrl
162+ this.publicClient = createPublicClient({
163+ chain: makeChain(chainId, rpcUrl),
164+ transport: http(rpcUrl),
165+ })
166+ }
167+
168+ async getBlockNumber() { return this.publicClient.getBlockNumber() }
169+ async getChainId() { return this.publicClient.getChainId() }
170+ async getGasPrice() { return this.publicClient.getGasPrice() }
171+ async isConnected() { try { await this.publicClient.getBlockNumber(); return true } catch { return false } }
172+
173+ async getBalance(address) {
174+ const wei = await this.publicClient.getBalance({ address })
175+ return formatEther(wei)
176+ }
177+
178+ async getTokenBalance(address, tokenAddress) {
179+ const bal = await this.publicClient.readContract({
180+ address: tokenAddress, abi: ERC20_ABI, functionName: 'balanceOf', args: [address],
181+ })
182+ return String(bal)
183+ }
184+
185+ async readContract({ address, abi, functionName, args = [] }) {
186+ return this.publicClient.readContract({ address, abi, functionName, args })
187+ }
188+
189+ async estimateGas({ to, value, data }) {
190+ return this.publicClient.estimateGas({ to, value, data })
191+ }
192+
193+ async waitForTransaction(hash) {
194+ const r = await this.publicClient.waitForTransactionReceipt({ hash })
195+ return {
196+ hash: r.transactionHash, blockNumber: r.blockNumber,
197+ gasUsed: r.gasUsed, status: r.status, contractAddress: r.contractAddress ?? undefined,
198+ }
199+ }
200+ }
201+
202+ export class EspaceWalletClient extends EspaceClient {
203+ _account
204+ _walletClient
205+
206+ constructor({ chainId, rpcUrl, privateKey }) {
207+ super({ chainId, rpcUrl })
208+ this._account = privateKeyToAccount(privateKey)
209+ this.address = this._account.address
210+ this._walletClient = createWalletClient({
211+ account: this._account, chain: makeChain(chainId, rpcUrl), transport: http(rpcUrl),
212+ })
213+ }
214+
215+ getAddress() { return this.address }
216+
217+ async sendTransaction({ to, value, data }) {
218+ return this._walletClient.sendTransaction({
219+ account: this._account, chain: makeChain(this.chainId, this.rpcUrl),
220+ to, value, data,
221+ })
222+ }
223+
224+ async signMessage(message) {
225+ return this._walletClient.signMessage({ account: this._account, message })
226+ }
227+
228+ async deployContract(abi, bytecode, args = []) {
229+ const hash = await this._walletClient.deployContract({
230+ account: this._account, chain: makeChain(this.chainId, this.rpcUrl),
231+ abi, bytecode, args,
232+ })
233+ const r = await this.waitForTransaction(hash)
234+ return r.contractAddress
235+ }
236+ }
237+ `
238+
147239// ── Main component ────────────────────────────────────────────────────────────
148240export function Playground ( {
149241 files = { } ,
@@ -152,13 +244,12 @@ export function Playground({
152244 extraDeps = { } ,
153245 file = 'index.ts' ,
154246} : PlaygroundProps ) {
155- const [ network , setNetwork ] = useState < Network > ( ' testnet' )
247+ const networkConfig = NETWORKS . testnet
156248
157- const networkConfig = NETWORKS [ network ]
158-
159- const networkFile = `// Auto-generated — changes when you toggle network\nexport const NETWORK = {\n chainId: ${ networkConfig . chainId } ,\n rpcUrl: '${ networkConfig . rpcUrl } ',\n blockExplorer: '${ networkConfig . blockExplorer } ',\n} as const\n`
249+ const networkFile = `// Auto-generated network config\nexport const NETWORK = {\n chainId: ${ networkConfig . chainId } ,\n rpcUrl: '${ networkConfig . rpcUrl } ',\n blockExplorer: '${ networkConfig . blockExplorer } ',\n} as const\n`
160250
161251 const mergedFiles = {
252+ '/cfxdevkit.ts' : { code : CFXDEVKIT_SHIM , hidden : true , readOnly : true } ,
162253 '/network-config.ts' : { code : networkFile , readOnly : true } ,
163254 ...Object . fromEntries (
164255 Object . entries ( files ) . map ( ( [ name , code ] ) => [
@@ -180,13 +271,14 @@ export function Playground({
180271 } }
181272 >
182273 < SandpackProvider
183- key = { network }
184274 template = { template }
185275 theme = "dark"
186276 files = { mergedFiles }
187277 options = { {
188278 activeFile,
189- visibleFiles : Object . keys ( mergedFiles ) ,
279+ visibleFiles : Object . entries ( mergedFiles )
280+ . filter ( ( [ , v ] ) => ! ( v as { hidden ?: boolean } ) . hidden )
281+ . map ( ( [ k ] ) => k ) ,
190282 recompileMode : 'delayed' ,
191283 recompileDelay : 600 ,
192284 autorun : true ,
@@ -209,23 +301,38 @@ export function Playground({
209301 gap : '8px' ,
210302 } }
211303 >
212- < NetworkToggle network = { network } onChange = { setNetwork } />
304+ < NetworkLabel />
213305 < RunButton />
214306 </ div >
215307
216- < SandpackLayout >
308+ < div
309+ style = { {
310+ display : 'flex' ,
311+ flexDirection : 'column' ,
312+ background : T . bg ,
313+ } }
314+ >
217315 < SandpackCodeEditor
218316 showLineNumbers
219317 showInlineErrors
220318 wrapContent
221- style = { { height : 380 } }
319+ style = { { height : 340 , minWidth : 0 } }
222320 />
321+ < div style = { { height : 1 , background : T . border } } />
223322 { showConsole ? (
224- < SandpackConsole showHeader style = { { height : 380 } } />
323+ < SandpackConsole showHeader style = { { height : 260 , minWidth : 0 } } />
225324 ) : (
226- < SandpackPreview style = { { height : 380 } } showNavigator = { false } />
325+ < SandpackPreview style = { { height : 260 , minWidth : 0 } } showNavigator = { false } />
227326 ) }
228- </ SandpackLayout >
327+ </ div >
328+
329+ { /* SandpackConsole needs a running iframe (SandpackPreview) to execute code.
330+ When only the console is shown, render a hidden preview to provide that client. */ }
331+ { showConsole && (
332+ < div aria-hidden = "true" style = { { height : 0 , overflow : 'hidden' } } >
333+ < SandpackPreview />
334+ </ div >
335+ ) }
229336 </ SandpackProvider >
230337 </ div >
231338 )
0 commit comments