From 4be0de4f3a6d804b7b969ec0ee22250425c74602 Mon Sep 17 00:00:00 2001 From: Justin Date: Mon, 6 Jan 2025 12:36:06 +0100 Subject: [PATCH] PortalNetwork: Implement UltralightProvider Enhancements for EIP-1193 JSON-RPC Support (#699) * chore: fix initial lint errors * convert UltraLightProvider to pure jsonrpc provider * Config updates and formatting fixes * undo edit in modules/discv5.ts * format response to match jsonrpc spec and update test files * clean up parameter typing and config * fix: update failing test * Fix initialization bug * Fix response formatting and tests --------- Co-authored-by: cjustinobi Co-authored-by: acolytec3 <17355484+acolytec3@users.noreply.github.com> --- packages/cli/scripts/testProvider.ts | 151 ++++++++++ packages/portalnetwork/src/client/eth.ts | 7 +- packages/portalnetwork/src/client/provider.ts | 268 ++++++++++++------ packages/portalnetwork/src/util/discv5.ts | 2 +- packages/portalnetwork/src/util/helpers.ts | 88 +++++- .../test/client/provider.spec.ts | 112 ++++++-- .../test/integration/integration.spec.ts | 1 - 7 files changed, 524 insertions(+), 105 deletions(-) create mode 100644 packages/cli/scripts/testProvider.ts diff --git a/packages/cli/scripts/testProvider.ts b/packages/cli/scripts/testProvider.ts new file mode 100644 index 000000000..9ec60ac9c --- /dev/null +++ b/packages/cli/scripts/testProvider.ts @@ -0,0 +1,151 @@ +import { UltralightProvider } from '../../portalnetwork/src/client/provider' +import { NetworkId } from '../../portalnetwork/src/networks/types' + +const testBlockHash = '0x95b0950557cbc3e6647766adb719f80f7c7d192f4429b6026cdbd2cbe6a64294' +const testContract = '0x6b175474e89094c44da98b954eedeac495271d0f' +const testStorage = '0x0000000000000000000000000000000000000000000000000000000000000000' +const historicalBlock = 1048576 + +async function findHistoricalAccount( + provider: UltralightProvider, + blockNumber: number, +): Promise { + try { + const block: any = await provider.request({ + method: 'eth_getBlockByNumber', + params: [blockNumber, true], + }) + + if ( + block && + block.result && + block.result.transactions && + block.result.transactions.length > 0 + ) { + const contractCreation = block.result.transactions.find((tx: any) => !tx.to) + if (contractCreation) { + console.log('Found contract creation transaction') + return contractCreation.from + } + + console.log('Using first transaction sender') + return block.result.transactions[0].from + } + return null + } catch (error) { + console.error('Error finding historical account:', error) + return null + } +} + +async function main() { + const provider = await UltralightProvider.create({ + bootnodes: [ + 'enr:-Jy4QIs2pCyiKna9YWnAF0zgf7bT0GzlAGoF8MEKFJOExmtofBIqzm71zDvmzRiiLkxaEJcs_Amr7XIhLI74k1rtlXICY5Z0IDAuMS4xLWFscGhhLjEtMTEwZjUwgmlkgnY0gmlwhKEjVaWJc2VjcDI1NmsxoQLSC_nhF1iRwsCw0n3J4jRjqoaRxtKgsEe5a-Dz7y0JloN1ZHCCIyg', + 'enr:-Jy4QKSLYMpku9F0Ebk84zhIhwTkmn80UnYvE4Z4sOcLukASIcofrGdXVLAUPVHh8oPCfnEOZm1W1gcAxB9kV2FJywkCY5Z0IDAuMS4xLWFscGhhLjEtMTEwZjUwgmlkgnY0gmlwhJO2oc6Jc2VjcDI1NmsxoQLMSGVlxXL62N3sPtaV-n_TbZFCEM5AR7RDyIwOadbQK4N1ZHCCIyg', + 'enr:-Jy4QH4_H4cW--ejWDl_W7ngXw2m31MM2GT8_1ZgECnfWxMzZTiZKvHDgkmwUS_l2aqHHU54Q7hcFSPz6VGzkUjOqkcCY5Z0IDAuMS4xLWFscGhhLjEtMTEwZjUwgmlkgnY0gmlwhJ31OTWJc2VjcDI1NmsxoQPC0eRkjRajDiETr_DRa5N5VJRm-ttCWDoO1QAMMCg5pIN1ZHCCIyg', + 'enr:-Ia4QLBxlH0Y8hGPQ1IRF5EStZbZvCPHQ2OjaJkuFMz0NRoZIuO2dLP0L-W_8ZmgnVx5SwvxYCXmX7zrHYv0FeHFFR0TY2aCaWSCdjSCaXCEwiErIIlzZWNwMjU2azGhAnnTykipGqyOy-ZRB9ga9pQVPF-wQs-yj_rYUoOqXEjbg3VkcIIjjA', + 'enr:-Ia4QM4amOkJf5z84Lv5Fl0RgWeSSDUekwnOPRn6XA1eMWgrHwWmn_gJGtOeuVfuX7ywGuPMRwb0odqQ9N_w_2Qc53gTY2aCaWSCdjSCaXCEwiErIYlzZWNwMjU2azGhAzaQEdPmz9SHiCw2I5yVAO8sriQ-mhC5yB7ea1u4u5QZg3VkcIIjjA', + 'enr:-Ia4QKVuHjNafkYuvhU7yCvSarNIVXquzJ8QOp5YbWJRIJw_EDVOIMNJ_fInfYoAvlRCHEx9LUQpYpqJa04pUDU21uoTY2aCaWSCdjSCaXCEwiErQIlzZWNwMjU2azGhA47eAW5oIDJAqxxqI0sL0d8ttXMV0h6sRIWU4ZwS4pYfg3VkcIIjjA', + 'enr:-Ia4QIU9U3zrP2DM7sfpgLJbbYpg12sWeXNeYcpKN49-6fhRCng0IUoVRI2E51mN-2eKJ4tbTimxNLaAnbA7r7fxVjcTY2aCaWSCdjSCaXCEwiErQYlzZWNwMjU2azGhAxOroJ3HceYvdD2yK1q9w8c9tgrISJso8q_JXI6U0Xwng3VkcIIjjA', + 'enr:-IS4QFV_wTNknw7qiCGAbHf6LxB-xPQCktyrCEZX-b-7PikMOIKkBg-frHRBkfwhI3XaYo_T-HxBYmOOQGNwThkBBHYDgmlkgnY0gmlwhKRc9_OJc2VjcDI1NmsxoQKHPt5CQ0D66ueTtSUqwGjfhscU_LiwS28QvJ0GgJFd-YN1ZHCCE4k', + 'enr:-IS4QDpUz2hQBNt0DECFm8Zy58Hi59PF_7sw780X3qA0vzJEB2IEd5RtVdPUYZUbeg4f0LMradgwpyIhYUeSxz2Tfa8DgmlkgnY0gmlwhKRc9_OJc2VjcDI1NmsxoQJd4NAVKOXfbdxyjSOUJzmA4rjtg43EDeEJu1f8YRhb_4N1ZHCCE4o', + 'enr:-IS4QGG6moBhLW1oXz84NaKEHaRcim64qzFn1hAG80yQyVGNLoKqzJe887kEjthr7rJCNlt6vdVMKMNoUC9OCeNK-EMDgmlkgnY0gmlwhKRc9-KJc2VjcDI1NmsxoQLJhXByb3LmxHQaqgLDtIGUmpANXaBbFw3ybZWzGqb9-IN1ZHCCE4k', + 'enr:-IS4QA5hpJikeDFf1DD1_Le6_ylgrLGpdwn3SRaneGu9hY2HUI7peHep0f28UUMzbC0PvlWjN8zSfnqMG07WVcCyBhADgmlkgnY0gmlwhKRc9-KJc2VjcDI1NmsxoQJMpHmGj1xSP1O-Mffk_jYIHVcg6tY5_CjmWVg1gJEsPIN1ZHCCE4o', + ], + bindAddress: '0.0.0.0', + supportedNetworks: [ + { networkId: NetworkId.HistoryNetwork, maxStorage: 1024 }, + { networkId: NetworkId.StateNetwork, maxStorage: 1024 }, + ], + }) + console.log('Provider created:', provider.portal.discv5.enr.nodeId) + await provider.portal.start() + console.log('portal started') + + while (provider.portal.network()['0x500b']?.routingTable.values().length === 0) { + console.log('Waiting for network to start...') + await new Promise((resolve) => setTimeout(resolve, 1000)) + } + + console.log('Testing eth_getBlockByHash...') + const block: any = await provider.request({ + method: 'eth_getBlockByHash', + params: [testBlockHash, false], + }) + + console.log('Block by hash retrieved:', block) + + console.log('Testing eth_getBlockByNumber...') + const blockByNumber = await provider.request({ + method: 'eth_getBlockByNumber', + params: [historicalBlock, false], + }) + + console.log('Block by number retrieved:', blockByNumber) + + console.log(`Looking for accounts in block ${historicalBlock}...`) + + // const testAddress = await findHistoricalAccount(provider, historicalBlock) + const testAddress = '0x3DC00AaD844393c110b61aED5849b7c82104e748' + console.log('test address ', testAddress) + if (!testAddress) { + console.error('Could not find a historical account to test with') + } + + console.log(`Found historical address: ${testAddress}`) + + console.log('Testing eth_getTransactionCount...') + const transactionCount = await provider.request({ + method: 'eth_getTransactionCount', + params: [testAddress, historicalBlock], + }) + + console.log('Transaction count:', transactionCount) + + console.log('Testing eth_getCode...') + const code = await provider.request({ + method: 'eth_getCode', + params: [testContract, '100'], + }) + console.log('Contract code retrieved:', code) + + console.log('Testing eth_getBalance...') + const balance = await provider.request({ + method: 'eth_getBalance', + params: [testAddress, historicalBlock], + }) + console.log('Account balance:', balance) + + console.log('Testing eth_getStorageAt...') + const storage = await provider.request({ + method: 'eth_getStorageAt', + params: [testContract, testStorage, historicalBlock], + }) + console.log('Storage value:', storage) + + console.log('Testing eth_call...') + const callData = { + to: testAddress, + data: '0x06fdde03', + from: testContract, + } + const callResult = await provider.request({ + method: 'eth_call', + params: [callData, historicalBlock], + }) + console.log('Contract call result:', callResult) + + try { + await provider.request({ + method: 'eth_unsupportedMethod', + params: [], + }) + } catch (error) { + console.log('Expected error for unsupported method:', error.message) + } + + process.exit(0) +} + +main().catch((err) => console.error(err)) diff --git a/packages/portalnetwork/src/client/eth.ts b/packages/portalnetwork/src/client/eth.ts index 9aa5a5acc..43e7b2a15 100644 --- a/packages/portalnetwork/src/client/eth.ts +++ b/packages/portalnetwork/src/client/eth.ts @@ -33,7 +33,7 @@ export class ETH { activeNetworks: NetworkId[] logger: Debugger constructor(portal: PortalNetwork) { - this.activeNetworks = Object.keys(portal.network()) as NetworkId[] + this.activeNetworks = Array.from(portal.networks.keys()) as NetworkId[] this.history = portal.network()['0x500b'] this.state = portal.network()['0x500a'] this.beacon = portal.network()['0x500c'] @@ -221,9 +221,8 @@ export class ETH { for (const network of networks) { if (this.activeNetworks.findIndex((el) => el === network) === -1) throw new Error( - `${ - Object.entries(NetworkId).find((el) => el[1] === network)?.[0] ?? - 'Unsupported network ' + network + `${Object.entries(NetworkId).find((el) => el[1] === network)?.[0] ?? + 'Unsupported network ' + network } required for this call`, ) } diff --git a/packages/portalnetwork/src/client/provider.ts b/packages/portalnetwork/src/client/provider.ts index 3f02af716..5bc8fbd29 100644 --- a/packages/portalnetwork/src/client/provider.ts +++ b/packages/portalnetwork/src/client/provider.ts @@ -1,102 +1,210 @@ -import { bytesToHex, hexToBytes } from '@ethereumjs/util' -import { ethers } from 'ethers' +import { PortalNetwork } from './client.js' -import { addRLPSerializedBlock } from '../networks/index.js' -import { NetworkId } from '../networks/types.js' -import { - blockFromRpc, - ethJsBlockToEthersBlock, - ethJsBlockToEthersBlockWithTxs, -} from '../util/helpers.js' +import type { PortalNetworkOpts } from './types' +import { hexToBytes } from '@ethereumjs/util' -import { PortalNetwork } from './client.js' +import { formatBlockResponse, formatResponse } from '../util/helpers.js' + +const ERROR_CODES = { + UNSUPPORTED_METHOD: 4200, + INVALID_PARAMS: -32602, + INTERNAL_ERROR: -32603, +} + +const SUPPORTED_METHODS = new Set([ + 'eth_getBlockByHash', + 'eth_getBlockByNumber', + 'eth_getTransactionCount', + 'eth_getCode', + 'eth_getBalance', + 'eth_getStorageAt', + 'eth_call', +]) -import type { HistoryNetwork } from '../networks/index.js' -import type { PortalNetworkOpts } from './types.js' +interface RequestArguments { + method: string + params?: unknown[] +} -export class UltralightProvider extends ethers.JsonRpcProvider { - private fallbackProvider: ethers.JsonRpcProvider +export class UltralightProvider { public portal: PortalNetwork - public historyNetwork: HistoryNetwork - public static create = async ( - fallbackProviderUrl: string | ethers.JsonRpcProvider, - opts: Partial, - ) => { - const portal = await PortalNetwork.create(opts) - return new UltralightProvider(fallbackProviderUrl, portal) - } - constructor(fallbackProvider: string | ethers.JsonRpcProvider, portal: PortalNetwork) { - const staticNetwork = ethers.Network.from('mainnet') - super( - typeof fallbackProvider === 'string' ? fallbackProvider : fallbackProvider._getConnection(), - staticNetwork, - { staticNetwork }, - ) - this.fallbackProvider = - typeof fallbackProvider === 'string' - ? new ethers.JsonRpcProvider(fallbackProvider, staticNetwork, { staticNetwork }) - : fallbackProvider + + constructor(portal: PortalNetwork) { this.portal = portal - this.historyNetwork = portal.networks.get(NetworkId.HistoryNetwork) as HistoryNetwork } - getBlock = async (blockTag: ethers.BlockTag): Promise => { - let block - if (typeof blockTag === 'string' && blockTag.length === 66) { - const blockHash = hexToBytes(blockTag) - block = await this.portal.ETH.getBlockByHash(blockHash, false) - if (block !== undefined) { - return ethJsBlockToEthersBlock(block, this.provider) + static async create(opts: Partial): Promise { + const portal = await PortalNetwork.create(opts) + return new UltralightProvider(portal) + } + + async request({ method, params = [] }: RequestArguments): Promise { + if (!SUPPORTED_METHODS.has(method)) { + throw this.createError( + ERROR_CODES.UNSUPPORTED_METHOD, + `The provider does not support the requested method`, + ) + } + + try { + switch (method) { + case 'eth_getBlockByHash': { + if (params.length !== 2) + throw this.createError( + ERROR_CODES.INVALID_PARAMS, + 'Invalid params for eth_getBlockByHash', + ) + const [blockHash, fullTx] = params + return await this.getBlockByHash(hexToBytes(blockHash as string), fullTx as boolean) + } + + case 'eth_getBlockByNumber': { + if (params.length !== 2) + throw this.createError( + ERROR_CODES.INVALID_PARAMS, + 'Invalid params for eth_getBlockByNumber', + ) + + const [blockNumber, includeTx] = params + if ( + typeof blockNumber !== 'number' && + typeof blockNumber !== 'bigint' && + blockNumber !== 'latest' && + blockNumber !== 'finalized' + ) { + throw this.createError( + ERROR_CODES.INVALID_PARAMS, + `Invalid block number: ${blockNumber}`, + ) + } + return await this.getBlockByNumber(blockNumber, includeTx as boolean) + } + + case 'eth_getTransactionCount': { + if (params.length !== 2) + throw this.createError( + ERROR_CODES.INVALID_PARAMS, + 'Invalid params for eth_getTransactionCount', + ) + const [address, block] = params + return await this.getTransactionCount(address as string, block as string) + } + + case 'eth_getCode': { + if (params.length !== 2) + throw this.createError(ERROR_CODES.INVALID_PARAMS, 'Invalid params for eth_getCode') + const [codeAddress, codeBlock] = params + return await this.getCode(codeAddress as string, codeBlock as string) + } + + case 'eth_getBalance': { + if (params.length !== 2) + throw this.createError(ERROR_CODES.INVALID_PARAMS, 'Invalid params for eth_getBalance') + const [balanceAddress, balanceBlock] = params + return await this.getBalance(balanceAddress as string, balanceBlock as bigint) + } + + case 'eth_getStorageAt': { + if (params.length !== 3) + throw this.createError( + ERROR_CODES.INVALID_PARAMS, + 'Invalid params for eth_getStorageAt', + ) + const [storageAddress, position, storageBlock] = params + return await this.getStorageAt( + storageAddress as string, + position as string, + storageBlock as string, + ) + } + + case 'eth_call': { + if (params.length !== 2) + throw this.createError(ERROR_CODES.INVALID_PARAMS, 'Invalid params for eth_call') + const [callObject, callBlock] = params + return await this.call(callObject as any, callBlock as bigint) + } + + default: + throw this.createError( + ERROR_CODES.UNSUPPORTED_METHOD, + `The Provider does not support the requested method`, + ) } - } else if (blockTag !== 'latest') { - const blockNum = typeof blockTag === 'number' ? blockTag : Number(BigInt(blockTag)) - block = await this.portal.ETH.getBlockByNumber(blockNum, false) - if (block !== undefined) { - return ethJsBlockToEthersBlock(block, this.provider) + } catch (error: any) { + return { + jsonrpc: '2.0', + id: null, + error: { + code: error.code ?? ERROR_CODES.INTERNAL_ERROR, + message: error.message ?? 'Internal error', + }, } } - // TODO: Add block to history network if retrieved from provider - return this.fallbackProvider.getBlock(blockTag) } - getBlockWithTransactions = async (blockTag: ethers.BlockTag) => { + private async getBlockByHash(blockHash: Uint8Array, fullTx: boolean) { + const response = await this.portal.ETH.getBlockByHash(blockHash, fullTx) + if (!response) { + throw this.createError(ERROR_CODES.INTERNAL_ERROR, 'Block not found') + } + + return formatBlockResponse(response, fullTx) + } + + private async getBlockByNumber(blockNumber: string | number | bigint, includeTx: boolean) { let block - const isBlockHash = - ethers.isHexString(blockTag) && typeof blockTag === 'string' && blockTag.length === 66 - if (isBlockHash) { - const blockHash = hexToBytes(blockTag) - block = await this.portal.ETH.getBlockByHash(blockHash, true) - if (block !== undefined) { - return ethJsBlockToEthersBlockWithTxs(block, this.provider) - } - } else if (blockTag !== 'latest') { - const blockNum = typeof blockTag === 'number' ? blockTag : Number(BigInt(blockTag)) - block = await this.portal.ETH.getBlockByNumber(blockNum, true) - if (block !== undefined) { - return ethJsBlockToEthersBlockWithTxs(block, this.provider) + + if (typeof blockNumber === 'string') { + if (blockNumber === 'latest' || blockNumber === 'finalized') { + block = await this.portal.ETH.getBlockByNumber(blockNumber, includeTx) + } else { + block = await this.portal.ETH.getBlockByNumber(BigInt(blockNumber), includeTx) } + } else { + block = await this.portal.ETH.getBlockByNumber(blockNumber, includeTx) } - if (isBlockHash) { - block = await this.fallbackProvider.send('eth_getBlockByHash', [blockTag, true]) - } else { - const blockNum = - typeof blockTag === 'number' - ? blockTag - : blockTag !== 'latest' - ? Number(BigInt(blockTag)) - : blockTag - block = await this.fallbackProvider.send('eth_getBlockByNumber', [blockNum, true]) + if (!block) { + throw this.createError(ERROR_CODES.INTERNAL_ERROR, 'Block not found') } - const ethJSBlock = blockFromRpc(block) - await addRLPSerializedBlock( - bytesToHex(ethJSBlock.serialize()), - block.hash, - this.historyNetwork, - [], // I'm too lazy to fix this right now + return formatBlockResponse(block, includeTx) + } + + private async getTransactionCount(address: string, block: string) { + const txCount = await this.portal.ETH.getTransactionCount(hexToBytes(address), block) + return formatResponse('0x' + (txCount !== undefined ? txCount.toString(16) : '')) + } + + private async getCode(codeAddress: string, codeBlock: string) { + const code = await this.portal.ETH.getCode(hexToBytes(codeAddress), codeBlock) + return formatResponse('0x' + (code !== undefined ? code.toString() : '')) + } + + private async getBalance(balanceAddress: string, balanceBlock: bigint) { + + const balance = await this.portal.ETH.getBalance(hexToBytes(balanceAddress), balanceBlock) + return formatResponse('0x' + (balance !== undefined ? balance.toString(16) : '')) + } + + private async getStorageAt(storageAddress: string, position: string, storageBlock: string) { + const storage = await this.portal.ETH.getStorageAt( + hexToBytes(storageAddress), + hexToBytes(position), + storageBlock, ) - const ethersBlock = await ethJsBlockToEthersBlockWithTxs(ethJSBlock, this.provider) - return ethersBlock + return formatResponse('0x' + (storage !== undefined ? storage.toString() : '')) + } + + private async call(callObject: any, callBlock: bigint) { + const result = await this.portal.ETH.call(callObject, callBlock) + return formatResponse('0x' + (result !== undefined ? result.toString() : '')) + } + + private createError(code: number, message: string) { + const error = new Error(message) + ; (error as any).code = code + return error } } diff --git a/packages/portalnetwork/src/util/discv5.ts b/packages/portalnetwork/src/util/discv5.ts index 7401beade..57f27b173 100644 --- a/packages/portalnetwork/src/util/discv5.ts +++ b/packages/portalnetwork/src/util/discv5.ts @@ -1 +1 @@ -export { distance, IDiscv5CreateOptions, log2Distance } from '@chainsafe/discv5' +export { distance, log2Distance } from '@chainsafe/discv5' diff --git a/packages/portalnetwork/src/util/helpers.ts b/packages/portalnetwork/src/util/helpers.ts index ab17bb5e0..e7f77158e 100644 --- a/packages/portalnetwork/src/util/helpers.ts +++ b/packages/portalnetwork/src/util/helpers.ts @@ -1,6 +1,6 @@ import { Block, BlockHeader } from '@ethereumjs/block' import { TransactionFactory } from '@ethereumjs/tx' -import { TypeOutput, bytesToHex, setLengthLeft, toBytes, toType } from '@ethereumjs/util' +import { TypeOutput, bigIntToHex, bytesToHex, intToHex, setLengthLeft, toBytes, toType } from '@ethereumjs/util' import { VM } from '@ethereumjs/vm' import debug from 'debug' import { ethers } from 'ethers' @@ -291,3 +291,89 @@ export function blockFromRpc( { ...options, setHardfork: true }, ) } + +export function formatBlockResponse(block: Block, includeTransactions: boolean) { + const json = JSON.stringify(block, null, 2) + const parsedBlock = JSON.parse(json) + const header = parsedBlock.header + + const withdrawalsAttr = + header.withdrawalsRoot !== undefined + ? { + withdrawalsRoot: header.withdrawalsRoot!, + withdrawals: parsedBlock.withdrawals, + } + : {} + + const transactions = block.transactions.map((tx, txIndex) => + includeTransactions ? toJSONRPCTx(tx, block, txIndex) : bytesToHex(tx.hash()), + ) + + return { + jsonrpc: '2.0', + id: 1, + result: { + number: header.number, + hash: bytesToHex(block.hash()), + parentHash: header.parentHash, + mixHash: header.mixHash, + nonce: header.nonce!, + sha3Uncles: header.uncleHash!, + logsBloom: header.logsBloom!, + transactionsRoot: header.transactionsTrie!, + stateRoot: header.stateRoot!, + receiptsRoot: header.receiptTrie!, + miner: header.coinbase!, + difficulty: header.difficulty!, + extraData: header.extraData!, + size: intToHex(block.serialize().length), + gasLimit: header.gasLimit!, + gasUsed: header.gasUsed!, + timestamp: header.timestamp!, + transactions, + uncles: block.uncleHeaders.map((uh) => bytesToHex(uh.hash())), + baseFeePerGas: header.baseFeePerGas, + ...withdrawalsAttr, + blobGasUsed: header.blobGasUsed, + excessBlobGas: header.excessBlobGas, + parentBeaconBlockRoot: header.parentBeaconBlockRoot, + requestsRoot: header.requestsRoot, + requests: block.requests?.map((req) => bytesToHex(req.serialize())), + }, + } +} + +export function toJSONRPCTx(tx: TypedTransaction, block?: Block, txIndex?: number) { + const txJSON = tx.toJSON() + return { + blockHash: block ? bytesToHex(block.hash()) : null, + blockNumber: block ? bigIntToHex(block.header.number) : null, + from: tx.getSenderAddress().toString(), + gas: txJSON.gasLimit!, + gasPrice: txJSON.gasPrice ?? txJSON.maxFeePerGas!, + maxFeePerGas: txJSON.maxFeePerGas, + maxPriorityFeePerGas: txJSON.maxPriorityFeePerGas, + type: intToHex(tx.type), + accessList: txJSON.accessList, + chainId: txJSON.chainId, + hash: bytesToHex(tx.hash()), + input: txJSON.data!, + nonce: txJSON.nonce!, + to: tx.to?.toString() ?? null, + transactionIndex: txIndex !== undefined ? intToHex(txIndex) : null, + value: txJSON.value!, + v: txJSON.v!, + r: txJSON.r!, + s: txJSON.s!, + maxFeePerBlobGas: txJSON.maxFeePerBlobGas, + blobVersionedHashes: txJSON.blobVersionedHashes + } +} + +export function formatResponse(result: string) { + return { + id: 1, + jsonrpc: '2.0', + result, + } +} \ No newline at end of file diff --git a/packages/portalnetwork/test/client/provider.spec.ts b/packages/portalnetwork/test/client/provider.spec.ts index 0d7af44db..310ab225a 100644 --- a/packages/portalnetwork/test/client/provider.spec.ts +++ b/packages/portalnetwork/test/client/provider.spec.ts @@ -2,20 +2,19 @@ import { SignableENR } from '@chainsafe/enr' import { Block, BlockHeader } from '@ethereumjs/block' import { keys } from '@libp2p/crypto' import { multiaddr } from '@multiformats/multiaddr' -import { assert, it } from 'vitest' +import { expect, it } from 'vitest' import { UltralightProvider } from '../../src/client/provider.js' import { TransportLayer } from '../../src/index.js' import { NetworkId } from '../../src/networks/types.js' -import { MockProvider } from '../testUtils/mockProvider.js' it('Test provider functionality', async () => { const ma = multiaddr('/ip4/0.0.0.0/udp/1500') const privateKey = await keys.generateKeyPair('secp256k1') const enr = SignableENR.createFromPrivateKey(privateKey) enr.setLocationMultiaddr(ma) - const provider = await UltralightProvider.create(new MockProvider(), { - bindAddress: '0.0.0.0', + const provider = await UltralightProvider.create({ + bindAddress: '0.0.0.0.0', transport: TransportLayer.NODE, config: { bindAddrs: { @@ -24,25 +23,102 @@ it('Test provider functionality', async () => { enr, privateKey, }, - supportedNetworks: [{ networkId: NetworkId.HistoryNetwork }], + supportedNetworks: [{ networkId: NetworkId.HistoryNetwork }, { networkId: NetworkId.StateNetwork }], }) - const block = await provider.getBlock(5000) - assert.ok(block!.number === 5000, 'retrieved block from fallback provider') // Stub getBlockByHash for unit testing provider.portal.ETH.getBlockByHash = async (_hash: Uint8Array) => { return Block.fromBlockData({ header: BlockHeader.fromHeaderData({ number: 2n }) }) } - const block2 = await provider.getBlock( - '0xb495a1d7e6663152ae92708da4843337b958146015a2802f4193a410044698c9', - ) - assert.equal(block2!.number, 2, 'got block 2 from portal network') - await (provider as any).portal.stop() - - assert.equal( - 1n, - (await provider._detectNetwork()).chainId, - 'parent class methods work as expected', - ) + + provider.portal.ETH.getBlockByNumber = async (blockNumber: number | bigint | "latest" | "finalized") => { + return Block.fromBlockData({ + header: BlockHeader.fromHeaderData({ + number: typeof blockNumber === 'string' ? 0n : blockNumber, + + }) + }) + } + + provider.portal.ETH.getTransactionCount = async (_address: Uint8Array) => { + return BigInt('0x5') + } + + provider.portal.ETH.getCode = async (_address: Uint8Array) => { + return new Uint8Array(Buffer.from('60806040', 'hex')) + } + + provider.portal.ETH.getBalance = async (_address: Uint8Array) => { + return 1000000000000000000n + } + + provider.portal.ETH.getStorageAt = async (_address: Uint8Array, _position: Uint8Array) => { + const result = new Uint8Array(32) + result[30] = 0x00 + result[31] = 0x01 + return Buffer.from(result).toString('hex') + } + + provider.portal.ETH.call = async (_txObject: any) => { + return Buffer.from([0x00, 0x01]).toString('hex') + } + + const blockByHash = await provider.request({ + method: 'eth_getBlockByHash', + params: ['0x123', false] + }) as { result: { number: string } } + expect(blockByHash.result.number).toBe('0x2') + + const blockByNumber = await provider.request({ + method: 'eth_getBlockByNumber', + params: [100, false] + }) as { result: { number: string } } + expect(blockByNumber.result.number).toBe('0x64') + + const balance = await provider.request({ + method: 'eth_getBalance', + params: ['0x3DC00AaD844393c110b61aED5849b7c82104e748', '0x0'] + }) as { result: string } + expect(balance.result).toBe('0xde0b6b3a7640000') + + + const storage = await provider.request({ + method: 'eth_getStorageAt', + params: [ + '0x1234567890123456789012345678901234567890', + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x64' + ] + }) as { result: string } + expect(storage.result).toBe('0x' + '00'.repeat(30) + '0001') + + const call = await provider.request({ + method: 'eth_call', + params: [{ + to: '0x1234567890123456789012345678901234567890', + data: '0x70a08231000000000000000000000000' + }, '0x64'] + }) as { result: string } + expect(call.result).toBe('0x0001') + + await expect(provider.request({ + method: 'eth_unsupportedMethod', + params: [] + })).rejects.toThrow() + + await expect(provider.request({ + method: 'eth_getBlockByHash', + params: ['0x123'] + })).resolves.toEqual({ + error: { + code: -32602, + message: 'Invalid params for eth_getBlockByHash' + }, + id: null, + jsonrpc: '2.0', + }) + + await provider.portal.stop() + }) diff --git a/packages/portalnetwork/test/integration/integration.spec.ts b/packages/portalnetwork/test/integration/integration.spec.ts index 978ab896b..cea6b7830 100644 --- a/packages/portalnetwork/test/integration/integration.spec.ts +++ b/packages/portalnetwork/test/integration/integration.spec.ts @@ -19,7 +19,6 @@ import { generateRandomNodeIdAtDistance, getContentKey, log2Distance, - log2Distance, serializedContentKeyToContentId, } from '../../src/index.js'