Skip to content

Commit a4a4e47

Browse files
committed
Merge remote-tracking branch 'origin/master' into smalls
2 parents 9b62fca + 5d537d3 commit a4a4e47

11 files changed

Lines changed: 230 additions & 215 deletions

File tree

packages/portal-client/__tests__/unit/services/createPortalClient.test.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,41 @@ import { NetworkId } from 'portalnetwork'
55
vi.mock('portalnetwork', () => {
66
const MockNetworkId = {
77
HistoryNetwork: 'history',
8-
StateNetwork: 'state'
8+
StateNetwork: 'state',
99
}
1010

1111
const MockTransportLayer = {
12-
MOBILE: 'mobile'
12+
MOBILE: 'mobile',
1313
}
1414

1515
const mockClient = {
1616
discv5: {
1717
enr: {
1818
setLocationMultiaddr: vi.fn(),
1919
getLocationMultiaddr: () => ({
20-
toOptions: () => ({ port: 9000 })
20+
toOptions: () => ({ port: 9000 }),
2121
}),
22-
kvs: new Map()
23-
}
22+
kvs: new Map(),
23+
},
2424
},
2525
networks: new Map([
2626
[MockNetworkId.HistoryNetwork, { networkId: MockNetworkId.HistoryNetwork }],
27-
[MockNetworkId.StateNetwork, { networkId: MockNetworkId.StateNetwork }]
27+
[MockNetworkId.StateNetwork, { networkId: MockNetworkId.StateNetwork }],
2828
]),
2929
start: vi.fn().mockResolvedValue(undefined),
30+
storeNodeDetails: vi.fn().mockResolvedValue(undefined),
3031
stop: vi.fn().mockResolvedValue(undefined),
31-
bootstrap: vi.fn().mockResolvedValue(undefined)
32+
bootstrap: vi.fn().mockResolvedValue(undefined),
33+
enableLog: vi.fn().mockResolvedValue(undefined),
3234
}
3335

3436
return {
3537
NetworkId: MockNetworkId,
3638
TransportLayer: MockTransportLayer,
3739
createPortalNetwork: vi.fn().mockResolvedValue(mockClient),
3840
DEFAULT_BOOTNODES: {
39-
mainnet: ['enr:-example-bootnode']
40-
}
41+
mainnet: ['enr:-example-bootnode'],
42+
},
4143
}
4244
})
4345

@@ -54,7 +56,10 @@ describe('Portal Client Tests', () => {
5456
assert.isDefined(client, 'Client should be defined')
5557

5658
// Verify network support (using our mock values)
57-
assert.isDefined(client.networks.get(NetworkId.HistoryNetwork), 'Should support History Network')
59+
assert.isDefined(
60+
client.networks.get(NetworkId.HistoryNetwork),
61+
'Should support History Network',
62+
)
5863
assert.isDefined(client.networks.get(NetworkId.StateNetwork), 'Should support State Network')
5964

6065
// Verify ENR configuration
@@ -63,7 +68,7 @@ describe('Portal Client Tests', () => {
6368
assert.equal(
6469
enr.getLocationMultiaddr('udp')?.toOptions().port ?? 0,
6570
port,
66-
'ENR should have correct port'
71+
'ENR should have correct port',
6772
)
6873

6974
// Verify lifecycle methods were called

packages/portal-client/src/components/jsonrpc/BlockExplorer.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ import { useJsonRpc } from '@/hooks/useJsonRpc'
33
import { MethodInput } from '@/components/ui/MethodInput'
44
import { ResponseViewer } from '@/components/ui/ResponseViewer'
55
import Select from '@/components/ui/Select'
6-
import { methodRegistry, MethodType } from '@/utils/constants/methodRegistry'
76
import { usePortalNetwork } from '@/contexts/PortalNetworkContext'
87
import { useNotification } from '@/contexts/NotificationContext'
9-
10-
import { APPROVED_METHODS } from '@/services/portalNetwork/types'
8+
import { methodRegistry, MethodType } from '@/utils/rpcMethods'
9+
import { APPROVED_METHODS } from '@/utils/constants/methodRegistry'
1110

1211
const BlockExplorer: FC = () => {
1312
const [selectedMethod, setSelectedMethod] = useState<MethodType | ''>('')

packages/portal-client/src/contexts/PortalNetworkContext.tsx

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
import { createContext, useContext, useEffect, useState, ReactNode, FC } from 'react'
1+
import {
2+
createContext,
3+
useContext,
4+
useState,
5+
ReactNode,
6+
FC,
7+
} from 'react'
28
import { createPortalClient } from '@/services/portalNetwork/client'
3-
import { getConfigValue } from '@/utils/helpers'
4-
import { ConfigId } from '@/utils/types'
9+
import { usePersistedState } from '@/hooks/usePersistedState'
510

611
type PortalNetworkContextType = {
712
client: any | null
@@ -31,10 +36,10 @@ export const PortalNetworkProvider: FC<PortalNetworkProviderProps> = ({
3136
}) => {
3237
const [client, setClient] = useState<any | null>(null)
3338
const [isLoading, setIsLoading] = useState(false)
34-
const [isNetworkReady, setIsNetworkReady] = useState(false)
35-
36-
const udpPort = getConfigValue(ConfigId.UdpPort)
37-
const shouldAutoInitialize = false
39+
const [isNetworkReady, setIsNetworkReady] = usePersistedState<boolean>(
40+
'portal-network-ready',
41+
false,
42+
)
3843

3944
const initialize = async (port: number): Promise<void> => {
4045

@@ -60,21 +65,14 @@ export const PortalNetworkProvider: FC<PortalNetworkProviderProps> = ({
6065
await client.stop()
6166
setClient(null)
6267
setIsNetworkReady(false)
68+
localStorage.removeItem('portal-network-ready')
6369
} catch (err) {
6470
const message = err instanceof Error ? err.message : 'An unknown error occurred'
65-
throw new Error(message)
71+
throw new Error(message)
6672
}
6773
}
6874
}
6975

70-
useEffect(() => {
71-
if (shouldAutoInitialize) {
72-
initialize(Number(udpPort)).catch((err) => {
73-
console.error('Auto-initialization failed:', err);
74-
});
75-
}
76-
}, [shouldAutoInitialize, udpPort])
77-
7876
const contextValue: PortalNetworkContextType = {
7977
client,
8078
isLoading,
Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,41 @@
11
import { SignableENR } from '@chainsafe/enr'
22
import { keys } from '@libp2p/crypto'
33
import { multiaddr } from '@multiformats/multiaddr'
4-
import {
5-
NetworkId,
6-
TransportLayer,
7-
createPortalNetwork,
8-
DEFAULT_BOOTNODES,
9-
} from 'portalnetwork'
4+
import { NetworkId, TransportLayer, createPortalNetwork, DEFAULT_BOOTNODES } from 'portalnetwork'
105
import { createDatabase } from './db'
116
import { TauriUDPTransportService } from './transports'
7+
import { DEFAULT_DB_SIZE, STARTUP_DELAY_MS } from '@/utils/constants/config'
128

139
import type { PortalNetwork } from 'portalnetwork'
1410
import type { Multiaddr } from '@multiformats/multiaddr'
15-
// const isBrowser = () => !window.__TAURI__
1611

17-
const db = createDatabase('network_db', { prefix: 'portalclient_' })
12+
let client: PortalNetwork
1813

1914
export const createPortalClient = async (port: number): Promise<PortalNetwork> => {
20-
try {
21-
const privateKey = await keys.generateKeyPair('secp256k1')
15+
const db = createDatabase('db', { prefix: 'portalclient_' })
16+
17+
const createNetwork = async (rebuildFromMemory: boolean): Promise<PortalNetwork> => {
18+
let privateKey: any
19+
if (rebuildFromMemory) {
20+
privateKey = await db.getPrivateKey()
21+
} else {
22+
privateKey = await keys.generateKeyPair('secp256k1')
23+
await db.savePrivateKey(privateKey)
24+
}
25+
2226
const enr = SignableENR.createFromPrivateKey(privateKey)
2327
const nodeAddr = multiaddr(`/ip4/0.0.0.0/udp/${port}`)
2428
enr.setLocationMultiaddr(nodeAddr)
25-
const client = await createPortalNetwork({
29+
30+
return createPortalNetwork({
2631
transport: TransportLayer.TAURI,
2732
supportedNetworks: [
2833
{ networkId: NetworkId.HistoryNetwork },
2934
{ networkId: NetworkId.StateNetwork },
3035
],
3136
db,
32-
dbSize: async () => 1024 * 1024 * 1024,
37+
rebuildFromMemory,
38+
dbSize: async () => DEFAULT_DB_SIZE,
3339
config: {
3440
enr,
3541
bindAddrs: { ip4: nodeAddr },
@@ -42,14 +48,23 @@ export const createPortalClient = async (port: number): Promise<PortalNetwork> =
4248
},
4349
},
4450
})
51+
}
4552

46-
await client.start()
47-
await client.bootstrap()
48-
49-
await new Promise(resolve => setTimeout(resolve, 1000))
50-
51-
return client
53+
try {
54+
client = await createNetwork(true)
5255
} catch (error) {
53-
throw error
56+
console.error('Failed to create node from memory, creating from scratch:', error)
57+
client = await createNetwork(false)
5458
}
59+
60+
await client.start()
61+
await client.storeNodeDetails()
62+
63+
client.enableLog('*Portal*')
64+
65+
// @ts-expect-error Window is available in browser
66+
window.portal = client // add portal to window to allow for direct access to portalnetwork client
67+
await new Promise((resolve) => setTimeout(resolve, STARTUP_DELAY_MS))
68+
69+
return client
5570
}

packages/portal-client/src/services/portalNetwork/db.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { BrowserLevel } from 'browser-level'
2+
import { keys } from '@libp2p/crypto'
3+
import { PRIVATE_KEY_DB_KEY } from './types'
24

35
export function createDatabase(
46
name: string,
@@ -19,5 +21,27 @@ export function createDatabase(
1921
const enhancedDb = browserDb as any
2022
enhancedDb.nextTick = (fn: Function) => setTimeout(fn, 0)
2123

24+
enhancedDb.getPrivateKey = async function() {
25+
try {
26+
const storedKeyData = await this.get(PRIVATE_KEY_DB_KEY)
27+
if (storedKeyData) {
28+
const keyBytes = Uint8Array.from(Buffer.from(storedKeyData, 'hex'))
29+
return keys.privateKeyFromRaw(keyBytes)
30+
}
31+
} catch (e) {
32+
console.log('No existing private key found in DB', e)
33+
}
34+
return null
35+
}
36+
37+
enhancedDb.savePrivateKey = async function(privateKey: any) {
38+
try {
39+
const rawBytes = privateKey.raw
40+
await this.put(PRIVATE_KEY_DB_KEY, Buffer.from(rawBytes).toString('hex'))
41+
} catch (e) {
42+
console.error('Failed to save private key to DB:', e)
43+
}
44+
}
45+
2246
return enhancedDb
2347
}
Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
export const APPROVED_METHODS = [
2-
'eth_getBlockByHash',
3-
'eth_getBlockByNumber',
4-
// 'portal_findNodes',
5-
// 'eth_getBlockReceipts',
6-
// 'eth_getLogs',
7-
] as const
1+
export const PRIVATE_KEY_DB_KEY = 'portal_client_private_key'
2+
3+
export interface MethodConfig {
4+
name: string
5+
paramPlaceholder: string
6+
handler: (
7+
input: string,
8+
sendRequest: (method: string, params?: any[])
9+
=> Promise<any>) => void | Promise<any>
10+
}

packages/portal-client/src/utils/constants/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { ConfigId } from '../types'
22

3+
export const DEFAULT_DB_SIZE = 1024 * 1024 * 1024
4+
export const STARTUP_DELAY_MS = 1000
5+
36
export const CONFIG_DEFAULTS: { id: ConfigId; title: string; defaultValue: string; description: string }[] = [
47
{
58
id: ConfigId.UdpPort,
Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,5 @@
1-
import { hexToBytes } from 'viem'
2-
import { APPROVED_METHODS } from '@/services/portalNetwork/types'
3-
import { isHexString } from '@ethereumjs/util'
4-
import { InputValue } from '../types'
1+
export const APPROVED_METHODS = [
2+
'eth_getBlockByHash',
3+
'eth_getBlockByNumber',
4+
] as const
55

6-
export type MethodType = typeof APPROVED_METHODS[number]
7-
8-
interface MethodConfig {
9-
name: string
10-
paramPlaceholder: string
11-
handler: (
12-
input: string,
13-
sendRequest: (method: string, params?: any[])
14-
=> Promise<any>) => void | Promise<any>
15-
}
16-
17-
export const methodRegistry: Record<MethodType, MethodConfig> = {
18-
'eth_getBlockByHash': {
19-
name: 'Get Block By Hash',
20-
paramPlaceholder: 'Enter Block Hash',
21-
handler: (input: string, sendRequestHandle: (method: string, params?: any[]) => Promise<any>) => {
22-
const [hash, includeFullTx] = input.split(',') as [InputValue, boolean]
23-
if (!isHexString(hash as `0x${string}`, 32)) {
24-
throw new Error('Invalid block hash. It should be a valid 32-byte hex string.')
25-
}
26-
return sendRequestHandle('eth_getBlockByHash', [hexToBytes(hash as `0x${string}`), includeFullTx])
27-
},
28-
},
29-
'eth_getBlockByNumber': {
30-
name: 'Get Block By Number',
31-
paramPlaceholder: 'Enter Block Number',
32-
handler: (input: string, sendRequestHandle: (method: string, params?: any[]) => Promise<any>) => {
33-
const [blockNumber, includeFullTx] = input.split(',')
34-
if (isNaN(Number(blockNumber))) {
35-
throw new Error('Invalid block number. It should be a valid number.')
36-
}
37-
return sendRequestHandle('eth_getBlockByNumber', [blockNumber, includeFullTx])
38-
},
39-
},
40-
}

packages/portal-client/src/utils/polyfills/debug-shim.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { hexToBytes } from 'viem'
2+
import { MethodConfig } from '@/services/portalNetwork/types'
3+
import { isHexString } from '@ethereumjs/util'
4+
import { InputValue } from './types'
5+
import { APPROVED_METHODS } from './constants/methodRegistry'
6+
7+
export type MethodType = typeof APPROVED_METHODS[number]
8+
9+
export const methodRegistry: Record<MethodType, MethodConfig> = {
10+
'eth_getBlockByHash': {
11+
name: 'Get Block By Hash',
12+
paramPlaceholder: 'Enter Block Hash',
13+
handler: (input: string, sendRequestHandle: (method: string, params?: any[]) => Promise<any>) => {
14+
const [hash, includeFullTx] = input.split(',') as [InputValue, boolean]
15+
if (!isHexString(hash as `0x${string}`, 32)) {
16+
throw new Error('Invalid block hash. It should be a valid 32-byte hex string.')
17+
}
18+
return sendRequestHandle('eth_getBlockByHash', [hexToBytes(hash as `0x${string}`), includeFullTx])
19+
},
20+
},
21+
'eth_getBlockByNumber': {
22+
name: 'Get Block By Number',
23+
paramPlaceholder: 'Enter Block Number',
24+
handler: (input: string, sendRequestHandle: (method: string, params?: any[]) => Promise<any>) => {
25+
const [blockNumber, includeFullTx] = input.split(',')
26+
if (isNaN(Number(blockNumber))) {
27+
throw new Error('Invalid block number. It should be a valid number.')
28+
}
29+
return sendRequestHandle('eth_getBlockByNumber', [blockNumber, includeFullTx])
30+
},
31+
},
32+
}

0 commit comments

Comments
 (0)