2
2
3
3
import { useEffect , useState } from "react" ;
4
4
import { Button } from "../../components/Button" ;
5
- import { createPublicClient , http } from 'viem' ;
5
+ import { createPublicClient , http , formatUnits } from 'viem' ;
6
6
import { useErrorBoundary } from "react-error-boundary" ;
7
7
import { pvm } from '@avalabs/avalanchejs' ;
8
8
import { RPCURLInput } from "../../components/RPCURLInput" ;
9
9
import { useWalletStore } from "../../stores/walletStore" ;
10
+ import { ChevronDown , ChevronUp } from "lucide-react" ;
10
11
11
12
type TestResult = Record < string , { passed : boolean , message : string } > ;
13
+
14
+ // Add utility functions at the top level
15
+ const formatPChainBalance = ( balance : bigint ) : string => {
16
+ return `${ ( Number ( balance ) / 1e9 ) . toLocaleString ( undefined , {
17
+ minimumFractionDigits : 0 ,
18
+ maximumFractionDigits : 9
19
+ } ) } AVAX`;
20
+ } ;
21
+
22
+ const formatEVMBalance = ( balance : bigint ) : string => {
23
+ const formattedBalance = formatUnits ( balance , 18 ) ;
24
+ return Number ( formattedBalance ) . toLocaleString ( undefined , {
25
+ minimumFractionDigits : 0 ,
26
+ maximumFractionDigits : 18
27
+ } ) ;
28
+ } ;
29
+
12
30
async function runPChainTests ( payload : { evmChainRpcUrl : string , baseURL : string , pChainAddress : string , ethAddress : string } ) : Promise < TestResult > {
13
31
const pvmApi = new pvm . PVMApi ( payload . baseURL ) ;
14
-
15
32
const result : TestResult = { } ;
16
33
17
34
try {
@@ -21,7 +38,7 @@ async function runPChainTests(payload: { evmChainRpcUrl: string, baseURL: string
21
38
}
22
39
result [ "Get Balance" ] = {
23
40
passed : true ,
24
- message : `Balance: ${ balanceResponse . balance } `
41
+ message : formatPChainBalance ( balanceResponse . balance )
25
42
} ;
26
43
} catch ( error ) {
27
44
console . log ( 'error' , error ) ;
@@ -42,9 +59,14 @@ async function runEVMTests(payload: { evmChainRpcUrl: string, baseURL: string, p
42
59
} ) ;
43
60
44
61
try {
45
- const balance = await publicClient . getBalance ( { address : payload . ethAddress as `0x${string } ` } ) ;
62
+ const balance = await publicClient . getBalance ( {
63
+ address : payload . ethAddress as `0x${string } `
64
+ } ) ;
46
65
47
- result [ "Get Balance" ] = { passed : true , message : `Balance: ${ balance } ` } ;
66
+ result [ "Get Balance" ] = {
67
+ passed : true ,
68
+ message : formatEVMBalance ( balance )
69
+ } ;
48
70
} catch ( error ) {
49
71
console . log ( 'error' , error ) ;
50
72
result [ "Get Balance" ] = {
@@ -239,6 +261,7 @@ const isInExtBcFormat = (rpcUrl: string) => {
239
261
export default function RPCMethodsCheck ( ) {
240
262
const [ evmChainRpcUrl , setEvmChainRpcUrl ] = useState < string > ( "" ) ;
241
263
const { pChainAddress, walletEVMAddress } = useWalletStore ( ) ;
264
+ const [ baseURL , setBaseURL ] = useState < string > ( "https://api.avax-test.network" ) ;
242
265
243
266
const { showBoundary } = useErrorBoundary ( ) ;
244
267
const [ isChecking , setIsChecking ] = useState ( false ) ;
@@ -248,8 +271,6 @@ export default function RPCMethodsCheck() {
248
271
admin : TestResult | null ,
249
272
metrics : TestResult | null
250
273
} > ( { pChain : null , evm : null , admin : null , metrics : null } ) ;
251
- const [ baseURL , setBaseURL ] = useState < string > ( "" ) ;
252
-
253
274
254
275
useEffect ( ( ) => {
255
276
if ( ! baseURL && isInExtBcFormat ( evmChainRpcUrl ) ) {
@@ -287,60 +308,176 @@ export default function RPCMethodsCheck() {
287
308
}
288
309
}
289
310
290
- const TestGroup = ( { title, results } : { title : string , results : TestResult | null } ) => (
291
- < div className = "border rounded-lg p-4" >
292
- < h3 className = "font-semibold mb-3" > { title } </ h3 >
293
- { results ? (
294
- < div className = "space-y-2" >
295
- { Object . entries ( results ) . map ( ( [ testName , result ] ) => (
296
- < div key = { testName } className = "flex items-center space-x-2" >
297
- < div className = { `w-2 h-2 rounded-full ${ result . passed ? 'bg-green-500' : 'bg-red-500' } ` } />
298
- < div >
299
- < span > { testName } </ span >
300
- { result . message && (
301
- < span className = "text-sm " > | { result . message . substring ( 0 , 100 ) } { result . message . length > 100 && '...' } </ span >
302
- ) }
303
- </ div >
304
- </ div >
305
- ) ) }
311
+ const TestGroup = ( { title, results, description } : {
312
+ title : string ,
313
+ results : TestResult | null ,
314
+ description : string
315
+ } ) => {
316
+ const [ expandedTests , setExpandedTests ] = useState < Record < string , boolean > > ( { } ) ;
317
+
318
+ const toggleExpand = ( testName : string ) => {
319
+ setExpandedTests ( prev => ( {
320
+ ...prev ,
321
+ [ testName ] : ! prev [ testName ]
322
+ } ) ) ;
323
+ } ;
324
+
325
+ const shouldShowDropdown = ( testName : string ) => {
326
+ // Only show direct values for Balance and "Get latest block"
327
+ return ! testName . includes ( 'Balance' ) && testName !== 'Get latest block' ;
328
+ } ;
329
+
330
+ return (
331
+ < div className = "border rounded-lg p-4" >
332
+ < div className = "mb-2" >
333
+ < h3 className = "font-semibold" > { title } </ h3 >
334
+ < p className = "text-sm text-zinc-600 dark:text-zinc-400" > { description } </ p >
306
335
</ div >
307
- ) : (
308
- < p className = "" > No results yet</ p >
309
- ) }
310
- </ div >
311
- ) ;
336
+ { results ? (
337
+ < div className = "space-y-1" >
338
+ { Object . entries ( results ) . map ( ( [ testName , result ] , index , array ) => {
339
+ const isDirectDisplay = ! shouldShowDropdown ( testName ) ;
340
+ const nextIsDirectDisplay = index < array . length - 1 && ! shouldShowDropdown ( array [ index + 1 ] [ 0 ] ) ;
341
+
342
+ return (
343
+ < div key = { testName } className = { `flex flex-col ${ ( isDirectDisplay || nextIsDirectDisplay ) ? 'mb-4' : '' } ` } >
344
+ { shouldShowDropdown ( testName ) ? (
345
+ // Dropdown layout (original horizontal layout)
346
+ < div className = "flex items-center justify-between py-1" >
347
+ < div className = "flex items-center space-x-2 min-w-[120px]" >
348
+ { result . passed ? (
349
+ < div className = "flex items-center text-green-600 dark:text-green-500" >
350
+ < svg className = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
351
+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = "2" d = "M5 13l4 4L19 7" />
352
+ </ svg >
353
+ </ div >
354
+ ) : (
355
+ < div className = "flex items-center text-red-600 dark:text-red-500" >
356
+ < svg className = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
357
+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = "2" d = "M6 18L18 6M6 6l12 12" />
358
+ </ svg >
359
+ </ div >
360
+ ) }
361
+ < span className = "font-medium whitespace-nowrap" > { testName } </ span >
362
+ </ div >
363
+ { result . message && (
364
+ < button
365
+ onClick = { ( ) => toggleExpand ( testName ) }
366
+ className = "flex items-center text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200"
367
+ >
368
+ { expandedTests [ testName ] ? (
369
+ < ChevronUp className = "w-4 h-4" />
370
+ ) : (
371
+ < ChevronDown className = "w-4 h-4" />
372
+ ) }
373
+ </ button >
374
+ ) }
375
+ </ div >
376
+ ) : (
377
+ // Direct display layout (new vertical layout)
378
+ < div className = "flex flex-col space-y-2" >
379
+ < div className = "flex items-center space-x-2" >
380
+ { result . passed ? (
381
+ < div className = "flex items-center text-green-600 dark:text-green-500" >
382
+ < svg className = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
383
+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = "2" d = "M5 13l4 4L19 7" />
384
+ </ svg >
385
+ </ div >
386
+ ) : (
387
+ < div className = "flex items-center text-red-600 dark:text-red-500" >
388
+ < svg className = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
389
+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = "2" d = "M6 18L18 6M6 6l12 12" />
390
+ </ svg >
391
+ </ div >
392
+ ) }
393
+ < span className = "font-medium whitespace-nowrap" > { testName } </ span >
394
+ </ div >
395
+ { result . message && (
396
+ < div className = "text-sm text-zinc-600 dark:text-zinc-400 break-all pl-6" >
397
+ { result . message }
398
+ </ div >
399
+ ) }
400
+ </ div >
401
+ ) }
402
+ { shouldShowDropdown ( testName ) && expandedTests [ testName ] && result . message && (
403
+ < div className = "text-sm text-zinc-600 dark:text-zinc-400 mt-2 pl-6 break-all" >
404
+ { result . message }
405
+ </ div >
406
+ ) }
407
+ </ div >
408
+ ) ;
409
+ } ) }
410
+ </ div >
411
+ ) : (
412
+ < p className = "text-zinc-500 dark:text-zinc-400" > No results yet</ p >
413
+ ) }
414
+ </ div >
415
+ ) ;
416
+ } ;
312
417
313
418
return (
314
419
< div className = "space-y-4" >
315
- < h2 className = "text-lg font-semibold" > RPC Methods Check</ h2 >
316
- < div className = "space-y-4" >
317
- < RPCURLInput
318
- label = "RPC URL"
319
- value = { evmChainRpcUrl }
320
- onChange = { setEvmChainRpcUrl }
321
- placeholder = "Enter RPC URL"
322
- />
323
- < RPCURLInput
324
- label = "Base URL to query P Chain, optional"
325
- value = { baseURL }
326
- onChange = { setBaseURL }
327
- />
328
- < Button
329
- variant = "primary"
330
- onClick = { checkRpc }
331
- loading = { isChecking }
332
- disabled = { ! evmChainRpcUrl }
333
- >
334
- Run Tests
335
- </ Button >
420
+ < div className = "border rounded-lg p-4 bg-white dark:bg-zinc-900" >
421
+ < h2 className = "text-lg font-semibold mb-4" > RPC Methods Security Check</ h2 >
422
+ < p className = "text-zinc-600 dark:text-zinc-400 mb-4" >
423
+ This tool helps verify the security configuration of your node's RPC endpoints.
424
+ </ p >
336
425
337
426
< div className = "space-y-4" >
338
- < TestGroup title = "P-Chain" results = { testResults . pChain } />
339
- < TestGroup title = "EVM" results = { testResults . evm } />
340
- < TestGroup title = "Admin API" results = { testResults . admin } />
341
- < TestGroup title = "Metrics API" results = { testResults . metrics } />
427
+ < RPCURLInput
428
+ label = "EVM Chain RPC URL"
429
+ value = { evmChainRpcUrl }
430
+ onChange = { setEvmChainRpcUrl }
431
+ placeholder = "e.g., http://localhost:9650/ext/bc/C/rpc"
432
+ />
433
+ < RPCURLInput
434
+ label = "P-Chain URL"
435
+ value = { baseURL }
436
+ onChange = { setBaseURL }
437
+ placeholder = "e.g., http://localhost:9650"
438
+ />
439
+ < Button
440
+ variant = "primary"
441
+ onClick = { checkRpc }
442
+ loading = { isChecking }
443
+ disabled = { ! evmChainRpcUrl }
444
+ >
445
+ { isChecking ? 'Running Security Checks...' : 'Run Security Check' }
446
+ </ Button >
342
447
</ div >
343
448
</ div >
449
+
450
+ < div className = "bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3 text-sm" >
451
+ < h3 className = "font-semibold text-blue-800 dark:text-blue-400" > Understanding Results</ h3 >
452
+ < ul className = "mt-1 space-y-1 text-blue-700 dark:text-blue-300 ml-4 list-disc" >
453
+ < li > Green checkmark means the endpoint is properly configured</ li >
454
+ < li > Red X indicates a potential security concern</ li >
455
+ < li > For Admin and Debug endpoints, errors indicate proper security, the appropriate errors will return green checkmarks</ li >
456
+ </ ul >
457
+ </ div >
458
+
459
+ < div className = "space-y-2" >
460
+ < TestGroup
461
+ title = "P-Chain API Tests"
462
+ results = { testResults . pChain }
463
+ description = "Verifies basic P-Chain operations like balance queries."
464
+ />
465
+ < TestGroup
466
+ title = "EVM API Tests"
467
+ results = { testResults . evm }
468
+ description = "Checks EVM endpoints and debug/trace method restrictions."
469
+ />
470
+ < TestGroup
471
+ title = "Admin API Security"
472
+ results = { testResults . admin }
473
+ description = "Verifies administrative endpoints are properly secured."
474
+ />
475
+ < TestGroup
476
+ title = "Metrics API Security"
477
+ results = { testResults . metrics }
478
+ description = "Ensures metrics endpoint is properly restricted."
479
+ />
480
+ </ div >
344
481
</ div >
345
482
) ;
346
483
} ;
0 commit comments