From ab134d9045ff38dbe1af1d8af901370c2d47b4b6 Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Tue, 9 Jun 2026 14:55:22 +0800 Subject: [PATCH 01/14] test(eth_rpc_schema): add JSON-Schema RPC conformance suite --- .../eth_rpc_schema/eth_rpc_schema_test.go | 644 ++++++++++++++++++ tests/eth_rpc/eth_rpc_schema/main_test.go | 15 + tests/eth_rpc/eth_rpc_schema/rpc_test.go | 144 ++++ .../eth_rpc/eth_rpc_schema/schemas/_defs.json | 171 +++++ .../schemas/eth_blobBaseFee.json | 6 + .../schemas/eth_blockNumber.json | 6 + .../eth_rpc_schema/schemas/eth_call.json | 6 + .../eth_rpc_schema/schemas/eth_chainId.json | 6 + .../schemas/eth_estimateGas.json | 6 + .../schemas/eth_feeHistory.json | 25 + .../eth_rpc_schema/schemas/eth_gasPrice.json | 6 + .../schemas/eth_getBalance.json | 6 + .../schemas/eth_getBlockByHash.json | 6 + .../schemas/eth_getBlockByNumber.json | 29 + .../schemas/eth_getBlockReceipts.json | 12 + .../eth_getBlockTransactionCountByHash.json | 6 + .../eth_getBlockTransactionCountByNumber.json | 6 + .../eth_rpc_schema/schemas/eth_getCode.json | 6 + .../eth_rpc_schema/schemas/eth_getLogs.json | 7 + .../schemas/eth_getStorageAt.json | 7 + ...eth_getTransactionByBlockHashAndIndex.json | 9 + ...h_getTransactionByBlockNumberAndIndex.json | 9 + .../schemas/eth_getTransactionByHash.json | 9 + .../schemas/eth_getTransactionCount.json | 6 + .../schemas/eth_getTransactionReceipt.json | 9 + .../schemas/eth_maxPriorityFeePerGas.json | 6 + .../schemas/eth_sendRawTransaction.json | 7 + .../eth_rpc_schema/schemas/eth_syncing.json | 18 + .../eth_rpc_schema/schemas/net_listening.json | 6 + .../eth_rpc_schema/schemas/net_peerCount.json | 6 + .../eth_rpc_schema/schemas/net_version.json | 8 + .../schemas/web3_clientVersion.json | 8 + tests/go.mod | 1 + tests/go.sum | 2 + 34 files changed, 1224 insertions(+) create mode 100644 tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_test.go create mode 100644 tests/eth_rpc/eth_rpc_schema/main_test.go create mode 100644 tests/eth_rpc/eth_rpc_schema/rpc_test.go create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/_defs.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_blobBaseFee.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_blockNumber.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_call.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_chainId.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_estimateGas.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_feeHistory.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_gasPrice.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getBalance.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockByHash.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockByNumber.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockReceipts.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockTransactionCountByHash.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockTransactionCountByNumber.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getCode.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getLogs.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getStorageAt.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionByBlockHashAndIndex.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionByBlockNumberAndIndex.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionByHash.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionCount.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionReceipt.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_maxPriorityFeePerGas.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_sendRawTransaction.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_syncing.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/net_listening.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/net_peerCount.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/net_version.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/web3_clientVersion.json diff --git a/tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_test.go b/tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_test.go new file mode 100644 index 0000000..4727f72 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_test.go @@ -0,0 +1,644 @@ +// Schema-driven JSON-RPC conformance tests for an Ethereum-compatible node. +// +// Each test: +// 1. Builds a JSON-RPC 2.0 request from scratch. +// 2. Posts it to /rpc over plain HTTP. +// 3. Decodes the response envelope. +// 4. Validates the `result` payload against schemas/.json — a +// hand-written JSON Schema (draft 2020-12) mirroring go-ethereum's +// RPC method contract. +// +// The schemas are embedded via //go:embed in rpc.go. + +package ethrpcschema + +import ( + "crypto/ecdsa" + "encoding/hex" + "encoding/json" + "errors" + "math/big" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + gethrlp "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/vechain/interstellar-e2e/tests/helper" + "github.com/vechain/thor/v2/thor" +) + +// ----------------------------------------------------------------------------- +// Network / web3 family +// ----------------------------------------------------------------------------- + +// TestNetVersion checks net_version returns a decimal-digit string. +func TestNetVersion(t *testing.T) { + rpcCallAndValidate(t, "net_version") +} + +// TestNetPeerCount checks net_peerCount returns a QUANTITY. +func TestNetPeerCount(t *testing.T) { + rpcCallAndValidate(t, "net_peerCount") +} + +// TestNetListening checks net_listening returns a boolean. +// Skipped if the node doesn't implement the method. +func TestNetListening(t *testing.T) { + result, err := rpcCall(t, "net_listening") + require.NoError(t, err, "net_listening rpc call") + validateResult(t, "net_listening", result) +} + +// TestWeb3ClientVersion checks web3_clientVersion returns a non-empty string. +// Skipped if the node doesn't implement the method. +func TestWeb3ClientVersion(t *testing.T) { + result, err := rpcCall(t, "web3_clientVersion") + require.NoError(t, err, "web3_clientVersion rpc call") + validateResult(t, "web3_clientVersion", result) +} + +// ----------------------------------------------------------------------------- +// Chain state — chain ID, gas price, block number, syncing +// ----------------------------------------------------------------------------- + +// TestEthChainID asserts eth_chainId returns a positive QUANTITY. +func TestEthChainID(t *testing.T) { + result := rpcCallAndValidate(t, "eth_chainId") + assert.Positive(t, hexQuantityToInt(t, result).Sign(), "chainId must be > 0") +} + +// TestEthBlockNumber asserts eth_blockNumber returns a QUANTITY (>= 0). +func TestEthBlockNumber(t *testing.T) { + rpcCallAndValidate(t, "eth_blockNumber") +} + +// TestEthGasPrice asserts eth_gasPrice returns a QUANTITY. +func TestEthGasPrice(t *testing.T) { + rpcCallAndValidate(t, "eth_gasPrice") +} + +// TestEthSyncing asserts eth_syncing returns either false or a SyncProgress object. +func TestEthSyncing(t *testing.T) { + rpcCallAndValidate(t, "eth_syncing") +} + +// TestEthMaxPriorityFeePerGas asserts eth_maxPriorityFeePerGas returns a QUANTITY. +// Skipped if the node doesn't implement the method. +func TestEthMaxPriorityFeePerGas(t *testing.T) { + result, err := rpcCall(t, "eth_maxPriorityFeePerGas") + require.NoError(t, err, "eth_maxPriorityFeePerGas rpc call") + validateResult(t, "eth_maxPriorityFeePerGas", result) +} + +// TestEthFeeHistory asserts eth_feeHistory returns a fee-history object. +// Skipped if the node doesn't implement the method. +func TestEthFeeHistory(t *testing.T) { + result, err := rpcCall(t, "eth_feeHistory", "0x1", "latest", []float64{}) + require.NoError(t, err, "eth_feeHistory rpc call") + validateResult(t, "eth_feeHistory", result) +} + +// TestEthBlobBaseFee asserts eth_blobBaseFee returns a QUANTITY. +// Skipped if the node doesn't implement the method. +func TestEthBlobBaseFee(t *testing.T) { + result, err := rpcCall(t, "eth_blobBaseFee") + if isMethodNotFound(err) { + t.Skipf("eth_blobBaseFee not supported: %v", err) + } + require.NoError(t, err, "eth_blobBaseFee rpc call") + validateResult(t, "eth_blobBaseFee", result) +} + +// ----------------------------------------------------------------------------- +// Account state — balance, code, storage, transaction count +// ----------------------------------------------------------------------------- + +// TestEthGetBalance exercises eth_getBalance across the JSON forms of +// go-ethereum's rpc.BlockNumberOrHash: +// +// - Plain string tags: "latest", "earliest", "pending", "safe", "finalized". +// - Plain hex block number: "0x0". +// - Object {"blockNumber":"0x0"} — the explicit-object number form. +// - Object {"blockHash":"0x..."} — the explicit-object hash form. +// - Object {"blockHash":"0x...", "requireCanonical":true} — with canonical flag. +// +// All sub-cases hit the funded node1 address so the response is non-zero where +// applicable. Optional tags / hash-form lookups that Thor doesn't support are +// auto-skipped on the standard error surfaces (method not found, "not yet +// supported", "invalid block tag"). +func TestEthGetBalance(t *testing.T) { + node1Addr := "0x61fF580B63D3845934610222245C116E013717ec" + + // Each sub-case represents one valid encoding of go-ethereum's + // rpc.BlockNumberOrHash. allowSkip == true means the sub-case may be + // skipped if the node reports the encoding as unsupported. + cases := []struct { + name string + blockArg any + }{ + {name: "string tag latest", blockArg: "latest"}, + {name: "string tag earliest", blockArg: "earliest"}, + {name: "string tag pending", blockArg: "pending"}, + {name: "string tag safe", blockArg: "safe"}, + {name: "string tag finalized", blockArg: "finalized"}, + {name: "hex block number 0x0", blockArg: "0x0"}, + {name: "object blockNumber 0x0", blockArg: map[string]any{"blockNumber": "0x0"}}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + result, err := rpcCall(t, "eth_getBalance", node1Addr, tc.blockArg) + require.NoError(t, err, "eth_getBalance(%v)", tc.blockArg) + validateResult(t, "eth_getBalance", result) + }) + } + + // Hash-form lookups need a real block hash, so fetch it once here. + t.Run("object blockHash (latest)", func(t *testing.T) { + hash := fetchLatestBlockHash(t) + result, err := rpcCall(t, "eth_getBalance", node1Addr, map[string]any{"blockHash": hash}) + require.NoError(t, err, "eth_getBalance({blockHash})") + validateResult(t, "eth_getBalance", result) + }) + + t.Run("object blockHash with requireCanonical", func(t *testing.T) { + hash := fetchLatestBlockHash(t) + result, err := rpcCall(t, "eth_getBalance", node1Addr, map[string]any{ + "blockHash": hash, + "requireCanonical": true, + }) + require.NoError(t, err, "eth_getBalance({blockHash,requireCanonical})") + validateResult(t, "eth_getBalance", result) + }) +} + +// TestEthGetCode reads code at the VTHO contract address across every +// JSON encoding of go-ethereum's rpc.BlockNumberOrHash. See +// runBlockNumberOrHashCases for the full sub-case list. +func TestEthGetCode(t *testing.T) { + vthoAddr := "0x0000000000000000000000000000456e65726779" + runBlockNumberOrHashCases(t, func(t *testing.T, blockArg any) { + result, err := rpcCall(t, "eth_getCode", vthoAddr, blockArg) + require.NoError(t, err, "eth_getCode(%v)", blockArg) + validateResult(t, "eth_getCode", result) + }) +} + +// TestEthGetStorageAt reads slot 0 of the zero address across every JSON +// encoding of go-ethereum's rpc.BlockNumberOrHash. +func TestEthGetStorageAt(t *testing.T) { + zeroAddr := common.Address{}.Hex() + runBlockNumberOrHashCases(t, func(t *testing.T, blockArg any) { + result, err := rpcCall(t, "eth_getStorageAt", zeroAddr, "0x0", blockArg) + require.NoError(t, err, "eth_getStorageAt(%v)", blockArg) + validateResult(t, "eth_getStorageAt", result) + }) +} + +// TestEthGetTransactionCount reads the nonce of the zero address at latest. +func TestEthGetTransactionCount(t *testing.T) { + t.Helper() + + t.Run("get transaction count of zero address", func(t *testing.T) { + rpcCallAndValidate(t, "eth_getTransactionCount", common.Address{}.Hex(), "latest") + }) + + t.Run("get transaction count of address", func(t *testing.T) { + chainIDRaw := rpcCallAndValidate(t, "eth_chainId") + chainID := hexQuantityToInt(t, chainIDRaw) + + from := crypto.PubkeyToAddress(helper.TestSenderKey.PublicKey) + nonceRaw, err := rpcCall(t, "eth_getTransactionCount", from.Hex(), "pending") + require.NoError(t, err, "eth_getTransactionCount(pending)") + nonce := hexQuantityToInt(t, nonceRaw).Uint64() + + baseFee := fetchLatestBaseFee(t) + gasTipCap := big.NewInt(1) + gasFeeCap := new(big.Int).Add(new(big.Int).Mul(baseFee, big.NewInt(2)), gasTipCap) + + to := common.HexToAddress("0x327931085B4cCbCE0baABb5a5E1C678707C51d90") // node2 + raw := signDynamicFeeTx(t, helper.TestSenderKey, chainID, nonce, gasTipCap, gasFeeCap, 21_000, &to, big.NewInt(1), nil) + + hashRaw := rpcCallAndValidate(t, "eth_sendRawTransaction", "0x"+hex.EncodeToString(raw)) + var hash string + require.NoError(t, json.Unmarshal(hashRaw, &hash), "unmarshal tx hash") + require.Regexp(t, "^0x[0-9a-fA-F]{64}$", hash) + + // Wait for the receipt on the Thor side so subsequent lookups succeed. + thorClient := helper.NewClient(nodeURL) + b32 := thor.Bytes32(common.HexToHash(hash)) + helper.WaitForReceipt(t, thorClient, &b32, 30*time.Second) + + rpcCallAndValidate(t, "eth_getTransactionCount", from.Hex(), "latest") + }) + + t.Run("block number or hash forms", func(t *testing.T) { + from := crypto.PubkeyToAddress(helper.TestSenderKey.PublicKey).Hex() + runBlockNumberOrHashCases(t, func(t *testing.T, blockArg any) { + result, err := rpcCall(t, "eth_getTransactionCount", from, blockArg) + require.NoError(t, err, "eth_getTransactionCount(%v)", blockArg) + validateResult(t, "eth_getTransactionCount", result) + }) + }) +} + +// ----------------------------------------------------------------------------- +// Block queries +// ----------------------------------------------------------------------------- + +// TestEthGetBlockByNumber_LatestHashesOnly fetches latest with includeTxs=false. +func TestEthGetBlockByNumber_LatestHashesOnly(t *testing.T) { + rpcCallAndValidate(t, "eth_getBlockByNumber", "latest", false) +} + +// TestEthGetBlockByNumber_LatestFullTxs fetches latest with includeTxs=true. +func TestEthGetBlockByNumber_LatestFullTxs(t *testing.T) { + t.Helper() + + chainIDRaw := rpcCallAndValidate(t, "eth_chainId") + chainID := hexQuantityToInt(t, chainIDRaw) + + from := crypto.PubkeyToAddress(helper.TestSenderKey.PublicKey) + nonceRaw, err := rpcCall(t, "eth_getTransactionCount", from.Hex(), "pending") + require.NoError(t, err, "eth_getTransactionCount(pending)") + nonce := hexQuantityToInt(t, nonceRaw).Uint64() + + baseFee := fetchLatestBaseFee(t) + gasTipCap := big.NewInt(1) + gasFeeCap := new(big.Int).Add(new(big.Int).Mul(baseFee, big.NewInt(2)), gasTipCap) + + to := common.HexToAddress("0x327931085B4cCbCE0baABb5a5E1C678707C51d90") // node2 + raw := signDynamicFeeTx(t, helper.TestSenderKey, chainID, nonce, gasTipCap, gasFeeCap, 21_000, &to, big.NewInt(1), nil) + + hashRaw := rpcCallAndValidate(t, "eth_sendRawTransaction", "0x"+hex.EncodeToString(raw)) + var hash string + require.NoError(t, json.Unmarshal(hashRaw, &hash), "unmarshal tx hash") + require.Regexp(t, "^0x[0-9a-fA-F]{64}$", hash) + + // Wait for the receipt on the Thor side so subsequent lookups succeed. + thorClient := helper.NewClient(nodeURL) + b32 := thor.Bytes32(common.HexToHash(hash)) + helper.WaitForReceipt(t, thorClient, &b32, 30*time.Second) + + rpcCallAndValidate(t, "eth_getBlockByNumber", "latest", true) +} + +// TestEthGetBlockByNumber_Future asserts the result is null for a future block. +func TestEthGetBlockByNumber_Future(t *testing.T) { + result, err := rpcCall(t, "eth_getBlockByNumber", "0x4000000000000000", false) + require.NoError(t, err, "eth_getBlockByNumber(future) call") + assert.JSONEq(t, "null", string(result), "future block must surface as null") + validateResult(t, "eth_getBlockByNumber", result) +} + +// TestEthGetBlockByHash_LatestHashesOnly fetches the latest block by hash. +func TestEthGetBlockByHash_LatestHashesOnly(t *testing.T) { + hash := fetchLatestBlockHash(t) + rpcCallAndValidate(t, "eth_getBlockByHash", hash, false) +} + +// TestEthGetBlockByHash_NonExistent asserts a made-up hash yields null. +func TestEthGetBlockByHash_NonExistent(t *testing.T) { + nonExistent := "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + result, err := rpcCall(t, "eth_getBlockByHash", nonExistent, false) + require.NoError(t, err, "eth_getBlockByHash(non-existent) call") + assert.JSONEq(t, "null", string(result), "non-existent hash must surface as null") + validateResult(t, "eth_getBlockByHash", result) +} + +// TestEthGetBlockTransactionCountByNumber checks count at latest. +func TestEthGetBlockTransactionCountByNumber(t *testing.T) { + rpcCallAndValidate(t, "eth_getBlockTransactionCountByNumber", "latest") +} + +// TestEthGetBlockTransactionCountByHash checks count for the latest-block hash. +func TestEthGetBlockTransactionCountByHash(t *testing.T) { + hash := fetchLatestBlockHash(t) + rpcCallAndValidate(t, "eth_getBlockTransactionCountByHash", hash) +} + +// ----------------------------------------------------------------------------- +// Transaction round-trip — sendRawTransaction → receipts/lookups +// ----------------------------------------------------------------------------- + +// TestEthSendRawTransaction signs an EIP-1559 transfer and submits it. +// Asserts the returned tx hash matches the schema. +func TestEthSendRawTransaction(t *testing.T) { + hash := sendTransfer(t) + require.NotEmpty(t, hash) +} + +// TestEthGetTransactionByHash fetches a freshly-submitted tx by its hash. +func TestEthGetTransactionByHash(t *testing.T) { + hash := sendTransfer(t) + require.NotEmpty(t, hash) + rpcCallAndValidate(t, "eth_getTransactionByHash", hash) +} + +// TestEthGetTransactionReceipt fetches the receipt of a freshly-submitted tx. +func TestEthGetTransactionReceipt(t *testing.T) { + hash := sendTransfer(t) + require.NotEmpty(t, hash) + rpcCallAndValidate(t, "eth_getTransactionReceipt", hash) +} + +// TestEthGetTransactionReceipt_NotFound asserts a made-up hash yields null. +func TestEthGetTransactionReceipt_NotFound(t *testing.T) { + result, err := rpcCall(t, "eth_getTransactionReceipt", "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + require.NoError(t, err, "eth_getTransactionReceipt(non-existent) call") + assert.JSONEq(t, "null", string(result)) + validateResult(t, "eth_getTransactionReceipt", result) +} + +// TestEthGetTransactionByBlockHashAndIndex picks index 0 of the block containing +// a freshly-submitted tx. +func TestEthGetTransactionByBlockHashAndIndex(t *testing.T) { + hash := sendTransfer(t) + receipt := waitReceipt(t, hash) + blockHash := receipt["blockHash"].(string) + rpcCallAndValidate(t, "eth_getTransactionByBlockHashAndIndex", blockHash, "0x0") +} + +// TestEthGetTransactionByBlockNumberAndIndex picks index 0 of the block containing +// a freshly-submitted tx. +func TestEthGetTransactionByBlockNumberAndIndex(t *testing.T) { + hash := sendTransfer(t) + receipt := waitReceipt(t, hash) + blockNumber := receipt["blockNumber"].(string) + rpcCallAndValidate(t, "eth_getTransactionByBlockNumberAndIndex", blockNumber, "0x0") +} + +// TestEthGetBlockReceipts fetches receipts across every JSON encoding of +// go-ethereum's rpc.BlockNumberOrHash. (eth_getBlockReceipts takes a +// BlockNumberOrHash as its sole parameter.) +func TestEthGetBlockReceipts(t *testing.T) { + runBlockNumberOrHashCases(t, func(t *testing.T, blockArg any) { + result, err := rpcCall(t, "eth_getBlockReceipts", blockArg) + require.NoError(t, err, "eth_getBlockReceipts(%v)", blockArg) + validateResult(t, "eth_getBlockReceipts", result) + }) +} + +// ----------------------------------------------------------------------------- +// Execution — eth_call, eth_estimateGas +// ----------------------------------------------------------------------------- + +// TestEthCall executes a no-op call from the zero address to itself across +// every JSON encoding of go-ethereum's rpc.BlockNumberOrHash. +func TestEthCall(t *testing.T) { + msg := map[string]any{ + "from": common.Address{}.Hex(), + "to": common.Address{}.Hex(), + } + runBlockNumberOrHashCases(t, func(t *testing.T, blockArg any) { + result, err := rpcCall(t, "eth_call", msg, blockArg) + require.NoError(t, err, "eth_call(%v)", blockArg) + validateResult(t, "eth_call", result) + }) +} + +// TestEthEstimateGas estimates gas for a no-op call. The block-tag parameter +// is optional in the JSON-RPC spec, so we exercise the no-arg form as well as +// every JSON encoding of go-ethereum's rpc.BlockNumberOrHash. +func TestEthEstimateGas(t *testing.T) { + msg := map[string]any{ + "from": common.Address{}.Hex(), + "to": common.Address{}.Hex(), + } + + t.Run("no block param", func(t *testing.T) { + rpcCallAndValidate(t, "eth_estimateGas", msg) + }) + + runBlockNumberOrHashCases(t, func(t *testing.T, blockArg any) { + result, err := rpcCall(t, "eth_estimateGas", msg, blockArg) + require.NoError(t, err, "eth_estimateGas(%v)", blockArg) + validateResult(t, "eth_estimateGas", result) + }) +} + +// ----------------------------------------------------------------------------- +// Logs +// ----------------------------------------------------------------------------- + +// TestEthGetLogs queries logs over the full chain so far. The result may be +// empty but must still validate as an array of log objects. +func TestEthGetLogs(t *testing.T) { + q := map[string]any{ + "fromBlock": "0x0", + "toBlock": "latest", + } + rpcCallAndValidate(t, "eth_getLogs", q) +} + +// ----------------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------------- + +// runBlockNumberOrHashCases enumerates the JSON encodings of go-ethereum's +// rpc.BlockNumberOrHash and runs `do` once per encoding under its own +// t.Run sub-test: +// +// - "string tag latest" / "earliest" / "pending" / "safe" / "finalized" +// - "hex block number 0x0" +// - "object blockNumber 0x0" → {"blockNumber":"0x0"} +// - "object blockHash (latest)" → {"blockHash":"0x..."} +// - "object blockHash with requireCanonical" → {"blockHash":"0x...","requireCanonical":true} +// +// `do` receives the block-arg payload to splice into the call. Sub-cases for +// the two hash-form variants fetch the latest block hash on demand. +func runBlockNumberOrHashCases(t *testing.T, do func(t *testing.T, blockArg any)) { + t.Helper() + cases := []struct { + name string + blockArg any + }{ + {name: "string tag latest", blockArg: "latest"}, + {name: "string tag earliest", blockArg: "earliest"}, + {name: "string tag pending", blockArg: "pending"}, + {name: "string tag safe", blockArg: "safe"}, + {name: "string tag finalized", blockArg: "finalized"}, + {name: "hex block number 0x0", blockArg: "0x0"}, + {name: "object blockNumber 0x0", blockArg: map[string]any{"blockNumber": "0x0"}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + do(t, tc.blockArg) + }) + } + t.Run("object blockHash (latest)", func(t *testing.T) { + hash := fetchLatestBlockHash(t) + do(t, map[string]any{"blockHash": hash}) + }) + t.Run("object blockHash with requireCanonical", func(t *testing.T) { + hash := fetchLatestBlockHash(t) + do(t, map[string]any{"blockHash": hash, "requireCanonical": true}) + }) +} + +// isMethodNotFound returns true for a JSON-RPC -32601 "method not found" +// error, which we treat as "skip" for optional methods. +func isMethodNotFound(err error) bool { + if err == nil { + return false + } + var rpcErr *jsonRPCError + if errors.As(err, &rpcErr) { + return rpcErr.Code == -32601 || strings.Contains(strings.ToLower(rpcErr.Message), "not found") || + strings.Contains(strings.ToLower(rpcErr.Message), "not supported") + } + return false +} + +// isUnsupported is the broader version of isMethodNotFound: it also matches +// non-standard "this shape isn't implemented" surfaces such as Thor's +// "invalid block tag" (returned for hash-form block tags) and "not yet +// supported" (returned for parameters the node hasn't wired up yet). +func isUnsupported(err error) bool { + if err == nil { + return false + } + if isMethodNotFound(err) { + return true + } + var rpcErr *jsonRPCError + if errors.As(err, &rpcErr) { + msg := strings.ToLower(rpcErr.Message) + for _, marker := range []string{"invalid block tag", "not yet supported"} { + if strings.Contains(msg, marker) { + return true + } + } + } + return false +} + +// hexQuantityToInt parses a JSON-encoded QUANTITY ("0x..."). Fails the test on +// any parse error. +func hexQuantityToInt(t *testing.T, raw json.RawMessage) *big.Int { + t.Helper() + var s string + require.NoError(t, json.Unmarshal(raw, &s), "unmarshal quantity string") + n, ok := new(big.Int).SetString(strings.TrimPrefix(s, "0x"), 16) + require.True(t, ok, "parse quantity %q", s) + return n +} + +// fetchLatestBlockHash returns the hash of the latest block via +// eth_getBlockByNumber("latest", false). +func fetchLatestBlockHash(t *testing.T) string { + t.Helper() + raw, err := rpcCall(t, "eth_getBlockByNumber", "latest", false) + require.NoError(t, err, "fetch latest block for hash") + var block map[string]any + require.NoError(t, json.Unmarshal(raw, &block), "unmarshal latest block") + hash, ok := block["hash"].(string) + require.True(t, ok, "latest block has no string hash field") + return hash +} + +// fetchLatestBaseFee returns the baseFeePerGas of the latest block as *big.Int. +func fetchLatestBaseFee(t *testing.T) *big.Int { + t.Helper() + raw, err := rpcCall(t, "eth_getBlockByNumber", "latest", false) + require.NoError(t, err, "fetch latest block for baseFee") + var block map[string]any + require.NoError(t, json.Unmarshal(raw, &block), "unmarshal latest block") + bf, ok := block["baseFeePerGas"].(string) + require.True(t, ok, "latest block missing baseFeePerGas — chain must be post-1559") + n, ok := new(big.Int).SetString(strings.TrimPrefix(bf, "0x"), 16) + require.True(t, ok, "parse baseFeePerGas %q", bf) + return n +} + +// sendTransfer signs an EIP-1559 1-wei transfer (helper.TestSenderKey → node2) +// and submits it via eth_sendRawTransaction. Returns the submitted tx hash and +// blocks until the receipt is available on the Thor side. +func sendTransfer(t *testing.T) string { + t.Helper() + + chainIDRaw := rpcCallAndValidate(t, "eth_chainId") + chainID := hexQuantityToInt(t, chainIDRaw) + + from := crypto.PubkeyToAddress(helper.TestSenderKey.PublicKey) + nonceRaw, err := rpcCall(t, "eth_getTransactionCount", from.Hex(), "pending") + require.NoError(t, err, "eth_getTransactionCount(pending)") + nonce := hexQuantityToInt(t, nonceRaw).Uint64() + + baseFee := fetchLatestBaseFee(t) + gasTipCap := big.NewInt(1) + gasFeeCap := new(big.Int).Add(new(big.Int).Mul(baseFee, big.NewInt(2)), gasTipCap) + + to := common.HexToAddress("0x327931085B4cCbCE0baABb5a5E1C678707C51d90") // node2 + raw := signDynamicFeeTx(t, helper.TestSenderKey, chainID, nonce, gasTipCap, gasFeeCap, 21_000, &to, big.NewInt(1), nil) + + hashRaw := rpcCallAndValidate(t, "eth_sendRawTransaction", "0x"+hex.EncodeToString(raw)) + var hash string + require.NoError(t, json.Unmarshal(hashRaw, &hash), "unmarshal tx hash") + require.Regexp(t, "^0x[0-9a-fA-F]{64}$", hash) + + // Wait for the receipt on the Thor side so subsequent lookups succeed. + thorClient := helper.NewClient(nodeURL) + b32 := thor.Bytes32(common.HexToHash(hash)) + helper.WaitForReceipt(t, thorClient, &b32, 30*time.Second) + + return hash +} + +// waitReceipt fetches eth_getTransactionReceipt for hash. Returns the receipt +// as a map for caller convenience (already schema-validated). +func waitReceipt(t *testing.T, hash string) map[string]any { + t.Helper() + raw := rpcCallAndValidate(t, "eth_getTransactionReceipt", hash) + require.NotEqual(t, "null", string(raw), "receipt must not be null for a confirmed tx") + var receipt map[string]any + require.NoError(t, json.Unmarshal(raw, &receipt), "unmarshal receipt") + return receipt +} + +// signDynamicFeeTx hand-builds and signs an EIP-1559 (type-2) transaction +// envelope without depending on the local eth_client package. The result is +// the raw bytes ready for eth_sendRawTransaction. +func signDynamicFeeTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, tipCap, feeCap *big.Int, gas uint64, to *common.Address, value *big.Int, data []byte) []byte { + t.Helper() + payload := []any{ + chainID, + nonce, + tipCap, + feeCap, + gas, + to, + value, + data, + []any{}, // empty access list + } + // signing hash: keccak256(0x02 || rlp(payload without sig fields)) + prefixed := append([]byte{0x02}, mustRLP(t, payload)...) + sigHash := crypto.Keccak256Hash(prefixed) + sig, err := crypto.Sign(sigHash.Bytes(), key) + require.NoError(t, err, "sign tx") + if len(sig) != 65 { + t.Fatalf("unexpected signature length %d", len(sig)) + } + v := uint64(sig[64]) + r := new(big.Int).SetBytes(sig[:32]) + s := new(big.Int).SetBytes(sig[32:64]) + + signed := []any{ + chainID, nonce, tipCap, feeCap, gas, to, value, data, []any{}, + v, r, s, + } + return append([]byte{0x02}, mustRLP(t, signed)...) +} + +func mustRLP(t *testing.T, v any) []byte { + t.Helper() + b, err := gethrlp.EncodeToBytes(v) + require.NoError(t, err, "rlp encode") + return b +} diff --git a/tests/eth_rpc/eth_rpc_schema/main_test.go b/tests/eth_rpc/eth_rpc_schema/main_test.go new file mode 100644 index 0000000..6a4c8ce --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/main_test.go @@ -0,0 +1,15 @@ +package ethrpcschema + +import ( + "os" + "testing" + + "github.com/vechain/interstellar-e2e/tests/helper" +) + +var nodeURL string + +func TestMain(m *testing.M) { + os.Setenv("THOR_BRANCH", "pedro/eth_eq_json_rpc") + os.Exit(helper.RunTestMain(m, &nodeURL, nil)) +} diff --git a/tests/eth_rpc/eth_rpc_schema/rpc_test.go b/tests/eth_rpc/eth_rpc_schema/rpc_test.go new file mode 100644 index 0000000..6960e73 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/rpc_test.go @@ -0,0 +1,144 @@ +// JSON-RPC over HTTP test harness for Ethereum-compatible endpoints. +// +// The harness sends standard JSON-RPC 2.0 envelopes to /rpc and +// validates the returned `result` against a JSON Schema embedded under +// schemas/.json. Schemas reference shared definitions in _defs.json. + +package ethrpcschema + +import ( + "bytes" + "context" + "embed" + "encoding/json" + "fmt" + "io" + "net/http" + "testing" + + "github.com/santhosh-tekuri/jsonschema/v5" + "github.com/stretchr/testify/require" +) + +//go:embed schemas/*.json +var schemaFS embed.FS + +// jsonRPCRequest is the on-wire JSON-RPC 2.0 request envelope. +type jsonRPCRequest struct { + JSONRPC string `json:"jsonrpc"` + ID int `json:"id"` + Method string `json:"method"` + Params []any `json:"params"` +} + +// jsonRPCError is the JSON-RPC 2.0 error object. +type jsonRPCError struct { + Code int `json:"code"` + Message string `json:"message"` + Data json.RawMessage `json:"data,omitempty"` +} + +func (e *jsonRPCError) Error() string { + return fmt.Sprintf("jsonrpc error %d: %s", e.Code, e.Message) +} + +// jsonRPCResponse is the JSON-RPC 2.0 response envelope. +type jsonRPCResponse struct { + JSONRPC string `json:"jsonrpc"` + ID int `json:"id"` + Result json.RawMessage `json:"result"` + Error *jsonRPCError `json:"error,omitempty"` +} + +// rpcCall sends a JSON-RPC 2.0 request to nodeURL+"/rpc" and returns the raw +// `result` field. Non-nil error means either transport-level failure or a +// JSON-RPC error response. +func rpcCall(t *testing.T, method string, params ...any) (json.RawMessage, error) { + t.Helper() + if params == nil { + params = []any{} + } + body, err := json.Marshal(jsonRPCRequest{ + JSONRPC: "2.0", + ID: 1, + Method: method, + Params: params, + }) + require.NoError(t, err, "marshal request") + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, nodeURL+"/rpc", bytes.NewReader(body)) + require.NoError(t, err, "build http request") + httpReq.Header.Set("Content-Type", "application/json") + + httpResp, err := http.DefaultClient.Do(httpReq) + require.NoError(t, err, "http POST to rpc endpoint") + defer httpResp.Body.Close() + + raw, err := io.ReadAll(httpResp.Body) + require.NoError(t, err, "read http response") + + if httpResp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("http %d: %s", httpResp.StatusCode, string(raw)) + } + + var resp jsonRPCResponse + if err := json.Unmarshal(raw, &resp); err != nil { + return nil, fmt.Errorf("decode response: %w (body=%s)", err, string(raw)) + } + if resp.Error != nil { + return nil, resp.Error + } + return resp.Result, nil +} + +// compiledSchemas caches one compiler per process; each call to validateResult +// compiles a single root schema but reuses the underlying $ref resolution. +var compiledSchemas = map[string]*jsonschema.Schema{} + +// loadSchema reads schemas/.json from the embedded FS and compiles it +// alongside every other schema (so $ref to "_defs.json" or sibling files +// resolves). Compilation is cached per process. +func loadSchema(t *testing.T, name string) *jsonschema.Schema { + t.Helper() + if sch, ok := compiledSchemas[name]; ok { + return sch + } + compiler := jsonschema.NewCompiler() + entries, err := schemaFS.ReadDir("schemas") + require.NoError(t, err, "read embedded schemas dir") + for _, e := range entries { + data, err := schemaFS.ReadFile("schemas/" + e.Name()) + require.NoError(t, err, "read schema %s", e.Name()) + require.NoError(t, compiler.AddResource(e.Name(), bytes.NewReader(data)), "register schema %s", e.Name()) + } + sch, err := compiler.Compile(name + ".json") + require.NoError(t, err, "compile schema %s", name) + compiledSchemas[name] = sch + return sch +} + +// validateResult validates a raw JSON-RPC `result` value against the named +// schema in schemas/.json. Failures include both the validator +// error and the original payload to make root-causing fast. +func validateResult(t *testing.T, schemaName string, result json.RawMessage) { + t.Helper() + var v any + require.NoError(t, json.Unmarshal(result, &v), "unmarshal result for schema validation") + sch := loadSchema(t, schemaName) + if err := sch.Validate(v); err != nil { + t.Fatalf("schema validation failed for %s:\n error: %v\n result: %s", schemaName, err, string(result)) + } +} + +// rpcCallAndValidate is a convenience: send the request, fail on jsonrpc +// error, then validate the result against schemas/.json (which uses +// the same name as the RPC method). +func rpcCallAndValidate(t *testing.T, method string, params ...any) json.RawMessage { + t.Helper() + result, err := rpcCall(t, method, params...) + require.NoError(t, err, "%s rpc call", method) + validateResult(t, method, result) + return result +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/_defs.json b/tests/eth_rpc/eth_rpc_schema/schemas/_defs.json new file mode 100644 index 0000000..a87e82f --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/_defs.json @@ -0,0 +1,171 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "_defs.json", + "$defs": { + "quantity": { + "description": "Ethereum QUANTITY: hex-encoded non-negative integer, no leading zeros except for 0x0.", + "type": "string", + "pattern": "^0x([1-9a-fA-F][0-9a-fA-F]*|0)$" + }, + "quantityOrNull": { + "anyOf": [ + { "$ref": "_defs.json#/$defs/quantity" }, + { "type": "null" } + ] + }, + "hex": { + "description": "Arbitrary-length 0x-prefixed hex string (DATA).", + "type": "string", + "pattern": "^0x[0-9a-fA-F]*$" + }, + "address": { + "description": "20-byte Ethereum address.", + "type": "string", + "pattern": "^0x[0-9a-fA-F]{40}$" + }, + "addressOrNull": { + "anyOf": [ + { "$ref": "_defs.json#/$defs/address" }, + { "type": "null" } + ] + }, + "hash32": { + "description": "32-byte hash (block hash, tx hash, storage slot, etc.).", + "type": "string", + "pattern": "^0x[0-9a-fA-F]{64}$" + }, + "hash32OrNull": { + "anyOf": [ + { "$ref": "_defs.json#/$defs/hash32" }, + { "type": "null" } + ] + }, + "bloom": { + "description": "256-byte (512 hex chars) logs bloom filter.", + "type": "string", + "pattern": "^0x[0-9a-fA-F]{512}$" + }, + "nonce8": { + "description": "8-byte block nonce.", + "type": "string", + "pattern": "^0x[0-9a-fA-F]{16}$" + }, + "accessListEntry": { + "type": "object", + "properties": { + "address": { "$ref": "_defs.json#/$defs/address" }, + "storageKeys": { + "type": "array", + "items": { "$ref": "_defs.json#/$defs/hash32" } + } + }, + "required": ["address", "storageKeys"] + }, + "transaction": { + "description": "JSON-RPC transaction object (legacy, EIP-2930, EIP-1559, EIP-4844).", + "type": "object", + "properties": { + "blockHash": { "$ref": "_defs.json#/$defs/hash32OrNull" }, + "blockNumber": { "$ref": "_defs.json#/$defs/quantityOrNull" }, + "from": { "$ref": "_defs.json#/$defs/address" }, + "gas": { "$ref": "_defs.json#/$defs/quantity" }, + "gasPrice": { "$ref": "_defs.json#/$defs/quantity" }, + "maxFeePerGas": { "$ref": "_defs.json#/$defs/quantity" }, + "maxPriorityFeePerGas": { "$ref": "_defs.json#/$defs/quantity" }, + "hash": { "$ref": "_defs.json#/$defs/hash32" }, + "input": { "$ref": "_defs.json#/$defs/hex" }, + "nonce": { "$ref": "_defs.json#/$defs/quantity" }, + "to": { "$ref": "_defs.json#/$defs/addressOrNull" }, + "transactionIndex": { "$ref": "_defs.json#/$defs/quantityOrNull" }, + "value": { "$ref": "_defs.json#/$defs/quantity" }, + "type": { "$ref": "_defs.json#/$defs/quantity" }, + "chainId": { "$ref": "_defs.json#/$defs/quantity" }, + "accessList": { + "type": "array", + "items": { "$ref": "_defs.json#/$defs/accessListEntry" } + }, + "v": { "$ref": "_defs.json#/$defs/quantity" }, + "r": { "$ref": "_defs.json#/$defs/quantity" }, + "s": { "$ref": "_defs.json#/$defs/quantity" }, + "yParity": { "$ref": "_defs.json#/$defs/quantity" } + }, + "required": ["hash", "nonce", "from", "value", "gas", "input", "to"] + }, + "log": { + "description": "Event log record returned from eth_getLogs / receipt.", + "type": "object", + "properties": { + "address": { "$ref": "_defs.json#/$defs/address" }, + "topics": { + "type": "array", + "items": { "$ref": "_defs.json#/$defs/hash32" }, + "minItems": 0, + "maxItems": 4 + }, + "data": { "$ref": "_defs.json#/$defs/hex" }, + "blockNumber": { "$ref": "_defs.json#/$defs/quantity" }, + "transactionHash": { "$ref": "_defs.json#/$defs/hash32" }, + "transactionIndex": { "$ref": "_defs.json#/$defs/quantity" }, + "blockHash": { "$ref": "_defs.json#/$defs/hash32" }, + "logIndex": { "$ref": "_defs.json#/$defs/quantity" }, + "removed": { "type": "boolean" } + }, + "required": ["address", "topics", "data"] + }, + "receipt": { + "description": "Transaction receipt object.", + "type": "object", + "properties": { + "transactionHash": { "$ref": "_defs.json#/$defs/hash32" }, + "transactionIndex": { "$ref": "_defs.json#/$defs/quantity" }, + "blockHash": { "$ref": "_defs.json#/$defs/hash32" }, + "blockNumber": { "$ref": "_defs.json#/$defs/quantity" }, + "from": { "$ref": "_defs.json#/$defs/address" }, + "to": { "$ref": "_defs.json#/$defs/addressOrNull" }, + "cumulativeGasUsed": { "$ref": "_defs.json#/$defs/quantity" }, + "gasUsed": { "$ref": "_defs.json#/$defs/quantity" }, + "effectiveGasPrice": { "$ref": "_defs.json#/$defs/quantity" }, + "contractAddress": { "$ref": "_defs.json#/$defs/addressOrNull" }, + "logs": { + "type": "array", + "items": { "$ref": "_defs.json#/$defs/log" } + }, + "logsBloom": { "$ref": "_defs.json#/$defs/bloom" }, + "type": { "$ref": "_defs.json#/$defs/quantity" }, + "status": { "$ref": "_defs.json#/$defs/quantity" }, + "root": { "$ref": "_defs.json#/$defs/hex" } + }, + "required": ["transactionHash", "blockHash", "blockNumber", "cumulativeGasUsed", "gasUsed", "logs", "logsBloom"] + }, + "blockBase": { + "description": "Common block fields shared between header-only and full forms.", + "type": "object", + "properties": { + "number": { "$ref": "_defs.json#/$defs/quantity" }, + "hash": { "$ref": "_defs.json#/$defs/hash32" }, + "parentHash": { "$ref": "_defs.json#/$defs/hash32" }, + "nonce": { "$ref": "_defs.json#/$defs/nonce8" }, + "sha3Uncles": { "$ref": "_defs.json#/$defs/hash32" }, + "logsBloom": { "$ref": "_defs.json#/$defs/bloom" }, + "transactionsRoot": { "$ref": "_defs.json#/$defs/hash32" }, + "stateRoot": { "$ref": "_defs.json#/$defs/hash32" }, + "receiptsRoot": { "$ref": "_defs.json#/$defs/hash32" }, + "miner": { "$ref": "_defs.json#/$defs/address" }, + "difficulty": { "$ref": "_defs.json#/$defs/quantity" }, + "totalDifficulty": { "$ref": "_defs.json#/$defs/quantity" }, + "extraData": { "$ref": "_defs.json#/$defs/hex" }, + "size": { "$ref": "_defs.json#/$defs/quantity" }, + "gasLimit": { "$ref": "_defs.json#/$defs/quantity" }, + "gasUsed": { "$ref": "_defs.json#/$defs/quantity" }, + "timestamp": { "$ref": "_defs.json#/$defs/quantity" }, + "baseFeePerGas": { "$ref": "_defs.json#/$defs/quantity" }, + "mixHash": { "$ref": "_defs.json#/$defs/hash32" }, + "uncles": { + "type": "array", + "items": { "$ref": "_defs.json#/$defs/hash32" } + } + }, + "required": ["number", "hash", "parentHash", "transactionsRoot", "stateRoot", "receiptsRoot", "gasLimit", "gasUsed", "timestamp"] + } + } +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_blobBaseFee.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_blobBaseFee.json new file mode 100644 index 0000000..d6d49b2 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_blobBaseFee.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_blobBaseFee.json", + "title": "eth_blobBaseFee result", + "$ref": "_defs.json#/$defs/quantity" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_blockNumber.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_blockNumber.json new file mode 100644 index 0000000..a0ea6a1 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_blockNumber.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_blockNumber.json", + "title": "eth_blockNumber result", + "$ref": "_defs.json#/$defs/quantity" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_call.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_call.json new file mode 100644 index 0000000..64260fc --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_call.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_call.json", + "title": "eth_call result", + "$ref": "_defs.json#/$defs/hex" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_chainId.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_chainId.json new file mode 100644 index 0000000..a6fc69e --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_chainId.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_chainId.json", + "title": "eth_chainId result", + "$ref": "_defs.json#/$defs/quantity" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_estimateGas.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_estimateGas.json new file mode 100644 index 0000000..ed5797d --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_estimateGas.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_estimateGas.json", + "title": "eth_estimateGas result", + "$ref": "_defs.json#/$defs/quantity" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_feeHistory.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_feeHistory.json new file mode 100644 index 0000000..dee4ff7 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_feeHistory.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_feeHistory.json", + "title": "eth_feeHistory result", + "type": "object", + "properties": { + "oldestBlock": { "$ref": "_defs.json#/$defs/quantity" }, + "baseFeePerGas": { + "type": "array", + "items": { "$ref": "_defs.json#/$defs/quantity" } + }, + "gasUsedRatio": { + "type": "array", + "items": { "type": "number" } + }, + "reward": { + "type": "array", + "items": { + "type": "array", + "items": { "$ref": "_defs.json#/$defs/quantity" } + } + } + }, + "required": ["oldestBlock", "baseFeePerGas", "gasUsedRatio"] +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_gasPrice.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_gasPrice.json new file mode 100644 index 0000000..b445cd4 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_gasPrice.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_gasPrice.json", + "title": "eth_gasPrice result", + "$ref": "_defs.json#/$defs/quantity" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getBalance.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getBalance.json new file mode 100644 index 0000000..5946fe9 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getBalance.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getBalance.json", + "title": "eth_getBalance result", + "$ref": "_defs.json#/$defs/quantity" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockByHash.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockByHash.json new file mode 100644 index 0000000..9407c5d --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockByHash.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getBlockByHash.json", + "title": "eth_getBlockByHash result", + "$ref": "eth_getBlockByNumber.json" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockByNumber.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockByNumber.json new file mode 100644 index 0000000..7c36d1e --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockByNumber.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getBlockByNumber.json", + "title": "eth_getBlockByNumber result", + "description": "Either null (block not found) or a full block, with transactions as either hashes (includeTxs=false) or expanded objects (includeTxs=true).", + "oneOf": [ + { "type": "null" }, + { + "allOf": [ + { "$ref": "_defs.json#/$defs/blockBase" }, + { + "type": "object", + "properties": { + "transactions": { + "type": "array", + "items": { + "anyOf": [ + { "$ref": "_defs.json#/$defs/hash32" }, + { "$ref": "_defs.json#/$defs/transaction" } + ] + } + } + }, + "required": ["transactions"] + } + ] + } + ] +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockReceipts.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockReceipts.json new file mode 100644 index 0000000..d86e9ed --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockReceipts.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getBlockReceipts.json", + "title": "eth_getBlockReceipts result", + "oneOf": [ + { "type": "null" }, + { + "type": "array", + "items": { "$ref": "_defs.json#/$defs/receipt" } + } + ] +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockTransactionCountByHash.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockTransactionCountByHash.json new file mode 100644 index 0000000..0df487b --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockTransactionCountByHash.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getBlockTransactionCountByHash.json", + "title": "eth_getBlockTransactionCountByHash result", + "$ref": "_defs.json#/$defs/quantity" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockTransactionCountByNumber.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockTransactionCountByNumber.json new file mode 100644 index 0000000..0dd54bb --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getBlockTransactionCountByNumber.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getBlockTransactionCountByNumber.json", + "title": "eth_getBlockTransactionCountByNumber result", + "$ref": "_defs.json#/$defs/quantity" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getCode.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getCode.json new file mode 100644 index 0000000..6953fc9 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getCode.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getCode.json", + "title": "eth_getCode result", + "$ref": "_defs.json#/$defs/hex" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getLogs.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getLogs.json new file mode 100644 index 0000000..a80d56a --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getLogs.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getLogs.json", + "title": "eth_getLogs result", + "type": "array", + "items": { "$ref": "_defs.json#/$defs/log" } +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getStorageAt.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getStorageAt.json new file mode 100644 index 0000000..3036e1e --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getStorageAt.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getStorageAt.json", + "title": "eth_getStorageAt result", + "description": "Single 32-byte storage word, hex-encoded.", + "$ref": "_defs.json#/$defs/hash32" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionByBlockHashAndIndex.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionByBlockHashAndIndex.json new file mode 100644 index 0000000..b6470f1 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionByBlockHashAndIndex.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getTransactionByBlockHashAndIndex.json", + "title": "eth_getTransactionByBlockHashAndIndex result", + "oneOf": [ + { "type": "null" }, + { "$ref": "_defs.json#/$defs/transaction" } + ] +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionByBlockNumberAndIndex.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionByBlockNumberAndIndex.json new file mode 100644 index 0000000..3add65e --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionByBlockNumberAndIndex.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getTransactionByBlockNumberAndIndex.json", + "title": "eth_getTransactionByBlockNumberAndIndex result", + "oneOf": [ + { "type": "null" }, + { "$ref": "_defs.json#/$defs/transaction" } + ] +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionByHash.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionByHash.json new file mode 100644 index 0000000..8434c90 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionByHash.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getTransactionByHash.json", + "title": "eth_getTransactionByHash result", + "oneOf": [ + { "type": "null" }, + { "$ref": "_defs.json#/$defs/transaction" } + ] +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionCount.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionCount.json new file mode 100644 index 0000000..0c7a9dc --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionCount.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getTransactionCount.json", + "title": "eth_getTransactionCount result", + "$ref": "_defs.json#/$defs/quantity" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionReceipt.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionReceipt.json new file mode 100644 index 0000000..a5f33f7 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getTransactionReceipt.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getTransactionReceipt.json", + "title": "eth_getTransactionReceipt result", + "oneOf": [ + { "type": "null" }, + { "$ref": "_defs.json#/$defs/receipt" } + ] +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_maxPriorityFeePerGas.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_maxPriorityFeePerGas.json new file mode 100644 index 0000000..81e956f --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_maxPriorityFeePerGas.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_maxPriorityFeePerGas.json", + "title": "eth_maxPriorityFeePerGas result", + "$ref": "_defs.json#/$defs/quantity" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_sendRawTransaction.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_sendRawTransaction.json new file mode 100644 index 0000000..4f343e8 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_sendRawTransaction.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_sendRawTransaction.json", + "title": "eth_sendRawTransaction result", + "description": "32-byte transaction hash returned by the node after accepting the raw tx.", + "$ref": "_defs.json#/$defs/hash32" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_syncing.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_syncing.json new file mode 100644 index 0000000..d716f3d --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_syncing.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_syncing.json", + "title": "eth_syncing result", + "description": "Either the literal false (when not syncing), or a SyncProgress object.", + "oneOf": [ + { "type": "boolean", "const": false }, + { + "type": "object", + "properties": { + "startingBlock": { "$ref": "_defs.json#/$defs/quantity" }, + "currentBlock": { "$ref": "_defs.json#/$defs/quantity" }, + "highestBlock": { "$ref": "_defs.json#/$defs/quantity" } + }, + "required": ["startingBlock", "currentBlock", "highestBlock"] + } + ] +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/net_listening.json b/tests/eth_rpc/eth_rpc_schema/schemas/net_listening.json new file mode 100644 index 0000000..0938b95 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/net_listening.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "net_listening.json", + "title": "net_listening result", + "type": "boolean" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/net_peerCount.json b/tests/eth_rpc/eth_rpc_schema/schemas/net_peerCount.json new file mode 100644 index 0000000..014d3d4 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/net_peerCount.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "net_peerCount.json", + "title": "net_peerCount result", + "$ref": "_defs.json#/$defs/quantity" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/net_version.json b/tests/eth_rpc/eth_rpc_schema/schemas/net_version.json new file mode 100644 index 0000000..426b51c --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/net_version.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "net_version.json", + "title": "net_version result", + "description": "Network ID as a decimal-digit string.", + "type": "string", + "pattern": "^[0-9]+$" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/web3_clientVersion.json b/tests/eth_rpc/eth_rpc_schema/schemas/web3_clientVersion.json new file mode 100644 index 0000000..72f3a4a --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/web3_clientVersion.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "web3_clientVersion.json", + "title": "web3_clientVersion result", + "description": "Free-form client identifier string (e.g. \"Thor/v2.x.x\").", + "type": "string", + "minLength": 1 +} diff --git a/tests/go.mod b/tests/go.mod index 3eb38f8..dd211bb 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -36,6 +36,7 @@ require ( github.com/prometheus/procfs v0.12.0 // indirect github.com/qianbin/directcache v0.9.7 // indirect github.com/qianbin/drlp v0.0.0-20240102101024-e0e02518b5f9 // indirect + github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect github.com/vechain/go-ecvrf v0.0.0-20251211112124-5d5a3ef70fc9 // indirect golang.org/x/crypto v0.49.0 // indirect diff --git a/tests/go.sum b/tests/go.sum index d4ca4e3..1b3960e 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -117,6 +117,8 @@ github.com/qianbin/drlp v0.0.0-20240102101024-e0e02518b5f9 h1:phutO88A0XihNL/23g github.com/qianbin/drlp v0.0.0-20240102101024-e0e02518b5f9/go.mod h1:OnClEjurpFUtR3RUCauP9HxNNl8xjfGAOv0kWYTznOc= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= From 416eed1e00edc982834bf7b4010e28565b1eb41b Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Tue, 9 Jun 2026 15:42:33 +0800 Subject: [PATCH 02/14] Remove env branch set --- tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_test.go | 6 +++--- tests/eth_rpc/eth_rpc_schema/main_test.go | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_test.go b/tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_test.go index 4727f72..640c57e 100644 --- a/tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_test.go +++ b/tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_test.go @@ -232,7 +232,7 @@ func TestEthGetTransactionCount(t *testing.T) { // Wait for the receipt on the Thor side so subsequent lookups succeed. thorClient := helper.NewClient(nodeURL) b32 := thor.Bytes32(common.HexToHash(hash)) - helper.WaitForReceipt(t, thorClient, &b32, 30*time.Second) + helper.WaitForReceipt(t, thorClient, &b32, 20*time.Second) rpcCallAndValidate(t, "eth_getTransactionCount", from.Hex(), "latest") }) @@ -283,7 +283,7 @@ func TestEthGetBlockByNumber_LatestFullTxs(t *testing.T) { // Wait for the receipt on the Thor side so subsequent lookups succeed. thorClient := helper.NewClient(nodeURL) b32 := thor.Bytes32(common.HexToHash(hash)) - helper.WaitForReceipt(t, thorClient, &b32, 30*time.Second) + helper.WaitForReceipt(t, thorClient, &b32, 20*time.Second) rpcCallAndValidate(t, "eth_getBlockByNumber", "latest", true) } @@ -585,7 +585,7 @@ func sendTransfer(t *testing.T) string { // Wait for the receipt on the Thor side so subsequent lookups succeed. thorClient := helper.NewClient(nodeURL) b32 := thor.Bytes32(common.HexToHash(hash)) - helper.WaitForReceipt(t, thorClient, &b32, 30*time.Second) + helper.WaitForReceipt(t, thorClient, &b32, 20*time.Second) return hash } diff --git a/tests/eth_rpc/eth_rpc_schema/main_test.go b/tests/eth_rpc/eth_rpc_schema/main_test.go index 6a4c8ce..a2aabcf 100644 --- a/tests/eth_rpc/eth_rpc_schema/main_test.go +++ b/tests/eth_rpc/eth_rpc_schema/main_test.go @@ -10,6 +10,5 @@ import ( var nodeURL string func TestMain(m *testing.M) { - os.Setenv("THOR_BRANCH", "pedro/eth_eq_json_rpc") os.Exit(helper.RunTestMain(m, &nodeURL, nil)) } From 1874880f3ac9452e1931e037f1686115e8f60ef0 Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Tue, 9 Jun 2026 16:30:48 +0800 Subject: [PATCH 03/14] Update go.mod --- network/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/go.mod b/network/go.mod index 9e24ce4..9f70e31 100644 --- a/network/go.mod +++ b/network/go.mod @@ -3,7 +3,7 @@ module github.com/vechain/interstellar-e2e/network go 1.26.1 require ( - github.com/vechain/networkhub v0.0.8-0.20260331132751-a070cb8f5bd2 + github.com/vechain/networkhub v0.0.8 github.com/vechain/thor/v2 v2.4.3 ) From c1aca302eaf1d06248f09e7834871f399f811a2b Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Tue, 9 Jun 2026 16:33:59 +0800 Subject: [PATCH 04/14] fix lint --- .../eth_rpc_schema/eth_rpc_schema_test.go | 41 +++++++------------ 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_test.go b/tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_test.go index 640c57e..cb90949 100644 --- a/tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_test.go +++ b/tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_test.go @@ -28,8 +28,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/vechain/interstellar-e2e/tests/helper" "github.com/vechain/thor/v2/thor" + + "github.com/vechain/interstellar-e2e/tests/helper" ) // ----------------------------------------------------------------------------- @@ -495,29 +496,6 @@ func isMethodNotFound(err error) bool { return false } -// isUnsupported is the broader version of isMethodNotFound: it also matches -// non-standard "this shape isn't implemented" surfaces such as Thor's -// "invalid block tag" (returned for hash-form block tags) and "not yet -// supported" (returned for parameters the node hasn't wired up yet). -func isUnsupported(err error) bool { - if err == nil { - return false - } - if isMethodNotFound(err) { - return true - } - var rpcErr *jsonRPCError - if errors.As(err, &rpcErr) { - msg := strings.ToLower(rpcErr.Message) - for _, marker := range []string{"invalid block tag", "not yet supported"} { - if strings.Contains(msg, marker) { - return true - } - } - } - return false -} - // hexQuantityToInt parses a JSON-encoded QUANTITY ("0x..."). Fails the test on // any parse error. func hexQuantityToInt(t *testing.T, raw json.RawMessage) *big.Int { @@ -604,7 +582,17 @@ func waitReceipt(t *testing.T, hash string) map[string]any { // signDynamicFeeTx hand-builds and signs an EIP-1559 (type-2) transaction // envelope without depending on the local eth_client package. The result is // the raw bytes ready for eth_sendRawTransaction. -func signDynamicFeeTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, nonce uint64, tipCap, feeCap *big.Int, gas uint64, to *common.Address, value *big.Int, data []byte) []byte { +func signDynamicFeeTx( + t *testing.T, + key *ecdsa.PrivateKey, + chainID *big.Int, + nonce uint64, + tipCap, feeCap *big.Int, + gas uint64, + to *common.Address, + value *big.Int, + data []byte, +) []byte { t.Helper() payload := []any{ chainID, @@ -630,7 +618,8 @@ func signDynamicFeeTx(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, non s := new(big.Int).SetBytes(sig[32:64]) signed := []any{ - chainID, nonce, tipCap, feeCap, gas, to, value, data, []any{}, + chainID, nonce, tipCap, feeCap, gas, to, value, data, + []any{}, v, r, s, } return append([]byte{0x02}, mustRLP(t, signed)...) From d88cf83cc82a6f3a39bf28c3c7b4eaf9ae91059e Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Tue, 9 Jun 2026 16:36:13 +0800 Subject: [PATCH 05/14] update go.sum --- network/go.sum | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network/go.sum b/network/go.sum index 4998d03..1bbbb6b 100644 --- a/network/go.sum +++ b/network/go.sum @@ -172,8 +172,8 @@ github.com/vechain/go-ethereum v1.8.15-0.20260324060835-4fc778eca93e h1:0/g3bVEx github.com/vechain/go-ethereum v1.8.15-0.20260324060835-4fc778eca93e/go.mod h1:LVuf3xPnVtHmoIP5+mN7aPnIeRBgo0xVq/wVELtSeIA= github.com/vechain/goleveldb v1.0.1-0.20220809091043-51eb019c8655 h1:CbHcWpCi7wOYfpoErRABh3Slyq9vO0Ay/EHN5GuJSXQ= github.com/vechain/goleveldb v1.0.1-0.20220809091043-51eb019c8655/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= -github.com/vechain/networkhub v0.0.8-0.20260331132751-a070cb8f5bd2 h1:dblLJGXsIvllG5P9kMsHiGnoNhy4X0DbXyzIneatous= -github.com/vechain/networkhub v0.0.8-0.20260331132751-a070cb8f5bd2/go.mod h1:lExTZ9CKxmH4pOK6z1TSa76aj8tlOieenHXxK2E8hpM= +github.com/vechain/networkhub v0.0.8 h1:HKdKvtEkNxomgVmofmrW+3IRuI+yf6Hfyeu0lLykbpw= +github.com/vechain/networkhub v0.0.8/go.mod h1:lExTZ9CKxmH4pOK6z1TSa76aj8tlOieenHXxK2E8hpM= github.com/vechain/thor/v2 v2.4.3 h1:bz0WjvnbhOe6x99ngA8hdwF7v8WroR+cZqa9k4F+mBc= github.com/vechain/thor/v2 v2.4.3/go.mod h1:AjHV4eiral0P8LdS/dI16Og2APp/tT+IAO2fMok+ufQ= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= From a3131d7ffa698993482d49d6afaf7373bccef65d Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Mon, 15 Jun 2026 22:52:38 +0800 Subject: [PATCH 06/14] test(eth_rpc): add ethersjs go wrapper, ws schema tests, and broad RPC coverage - Wire ethersjs (mocha + ethers v6) into `go test ./...` via a thin RunTestMain-driven wrapper; Makefile guards `npm ci`, workflow adds setup-node@22 (global WebSocket). - Add ws subscription schema tests for newHeads / logs / newPendingTransactions (raw gorilla/websocket) plus a syncing-rejected pin; ship matching schemas under schemas/. - Extend ethersjs suite to 68 tests covering getBlock variants and block tags, JsonRpcSigner gaps, multi-shape eth_getLogs, EIP-1898 block refs, EIP-2930 / eth_blobBaseFee rejections, batched JsonRpcProvider, CREATE2 parity (new Create2Factory.sol + multi-file compile script), WebSocketProvider end-to-end (block / pending / logs / destroy), and method-level Contract estimateGas / populateTransaction. - main_test.go now defaults THOR_BRANCH=pedro/eth_eq_json_rpc so the schema suite runs standalone, mirroring the ethersjs wrapper. --- .github/workflows/test.yml | 9 + Makefile | 7 +- .../eth_rpc_schema/eth_rpc_ws_schema_test.go | 197 +++ tests/eth_rpc/eth_rpc_schema/main_test.go | 7 + tests/eth_rpc/eth_rpc_schema/rpc_ws_test.go | 210 +++ .../eth_rpc_schema/schemas/eth_subscribe.json | 7 + .../schemas/eth_subscription_logs.json | 23 + .../schemas/eth_subscription_newHeads.json | 19 + ...h_subscription_newPendingTransactions.json | 7 + .../schemas/eth_unsubscribe.json | 7 + tests/eth_rpc/ethersjs/.gitignore | 3 + tests/eth_rpc/ethersjs/.mocharc.cjs | 8 + tests/eth_rpc/ethersjs/README.md | 101 ++ .../ethersjs/contracts/Create2Factory.json | 43 + .../ethersjs/contracts/Create2Factory.sol | 16 + tests/eth_rpc/ethersjs/contracts/Storage.json | 140 ++ tests/eth_rpc/ethersjs/contracts/Storage.sol | 41 + tests/eth_rpc/ethersjs/ethersjs_test.go | 40 + tests/eth_rpc/ethersjs/package-lock.json | 1474 +++++++++++++++++ tests/eth_rpc/ethersjs/package.json | 23 + tests/eth_rpc/ethersjs/scripts/compile.cjs | 49 + tests/eth_rpc/ethersjs/src/fixtures.ts | 65 + tests/eth_rpc/ethersjs/src/globalSetup.ts | 119 ++ tests/eth_rpc/ethersjs/test/contract.test.ts | 229 +++ tests/eth_rpc/ethersjs/test/events.test.ts | 213 +++ tests/eth_rpc/ethersjs/test/provider.test.ts | 440 +++++ tests/eth_rpc/ethersjs/test/wallet.test.ts | 250 +++ tests/eth_rpc/ethersjs/test/websocket.test.ts | 138 ++ tests/eth_rpc/ethersjs/tsconfig.json | 18 + tests/go.mod | 4 +- 30 files changed, 3903 insertions(+), 4 deletions(-) create mode 100644 tests/eth_rpc/eth_rpc_schema/eth_rpc_ws_schema_test.go create mode 100644 tests/eth_rpc/eth_rpc_schema/rpc_ws_test.go create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_subscribe.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_subscription_logs.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_subscription_newHeads.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_subscription_newPendingTransactions.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_unsubscribe.json create mode 100644 tests/eth_rpc/ethersjs/.gitignore create mode 100644 tests/eth_rpc/ethersjs/.mocharc.cjs create mode 100644 tests/eth_rpc/ethersjs/README.md create mode 100644 tests/eth_rpc/ethersjs/contracts/Create2Factory.json create mode 100644 tests/eth_rpc/ethersjs/contracts/Create2Factory.sol create mode 100644 tests/eth_rpc/ethersjs/contracts/Storage.json create mode 100644 tests/eth_rpc/ethersjs/contracts/Storage.sol create mode 100644 tests/eth_rpc/ethersjs/ethersjs_test.go create mode 100644 tests/eth_rpc/ethersjs/package-lock.json create mode 100644 tests/eth_rpc/ethersjs/package.json create mode 100644 tests/eth_rpc/ethersjs/scripts/compile.cjs create mode 100644 tests/eth_rpc/ethersjs/src/fixtures.ts create mode 100644 tests/eth_rpc/ethersjs/src/globalSetup.ts create mode 100644 tests/eth_rpc/ethersjs/test/contract.test.ts create mode 100644 tests/eth_rpc/ethersjs/test/events.test.ts create mode 100644 tests/eth_rpc/ethersjs/test/provider.test.ts create mode 100644 tests/eth_rpc/ethersjs/test/wallet.test.ts create mode 100644 tests/eth_rpc/ethersjs/test/websocket.test.ts create mode 100644 tests/eth_rpc/ethersjs/tsconfig.json diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f989b53..8c7a452 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,6 +41,15 @@ jobs: network/go.sum tests/go.sum + - uses: actions/setup-node@v5 + with: + # Node 22 makes WebSocket available as a stable global — required by + # tests/eth_rpc/ethersjs/test/websocket.test.ts (ethers v6 WebSocketProvider + # picks up globalThis.WebSocket when given a plain URL string). + node-version: '22' + cache: 'npm' + cache-dependency-path: tests/eth_rpc/ethersjs/package-lock.json + - name: Get thor commit SHA id: thor-sha run: | diff --git a/Makefile b/Makefile index caf8c1a..837e1d4 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,12 @@ -.PHONY: build-network test clean stop status lint +.PHONY: build-network ethersjs-deps test clean stop status lint build-network: cd network && go build -o /tmp/interstellar-network github.com/vechain/interstellar-e2e/network && cd .. -test: build-network +ethersjs-deps: + @[ -d tests/eth_rpc/ethersjs/node_modules ] || (cd tests/eth_rpc/ethersjs && npm ci) + +test: build-network ethersjs-deps @/tmp/interstellar-network start & \ NODE_URL=$$(/tmp/interstellar-network node-url) && \ NODE_P2P_PORT=$$(/tmp/interstellar-network node-p2p-port) && \ diff --git a/tests/eth_rpc/eth_rpc_schema/eth_rpc_ws_schema_test.go b/tests/eth_rpc/eth_rpc_schema/eth_rpc_ws_schema_test.go new file mode 100644 index 0000000..dd9d8d2 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/eth_rpc_ws_schema_test.go @@ -0,0 +1,197 @@ +// Schema-driven WebSocket subscription tests for Thor's +// eth_subscribe / eth_unsubscribe (rpc/ws/conn.go). +// +// Each test: +// 1. Dials ws://nodeURL/rpc (same path as HTTP /rpc, upgrade-on-demand). +// 2. Issues eth_subscribe and validates the subID against +// schemas/eth_subscribe.json. +// 3. Triggers an event the subscription should fire on — a new block +// for newHeads, a deployment that emits LOG1 for logs, an EIP-1559 +// transfer for newPendingTransactions. +// 4. Reads the next notification frame and validates its inner result +// against schemas/eth_subscription_.json. +// 5. Issues eth_unsubscribe and validates the boolean against +// schemas/eth_unsubscribe.json. +// +// The syncing subscription is documented as a rejection — Thor's switch in +// rpc/ws/conn.go:182-207 only implements newHeads/logs/newPendingTransactions; +// any other subtype returns InvalidParams (-32602). If Thor ever ships +// 'syncing' that test flips. + +package ethrpcschema + +import ( + "context" + "encoding/hex" + "encoding/json" + "math/big" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/vechain/thor/v2/thor" + + "github.com/vechain/interstellar-e2e/tests/helper" +) + +// jsonRPCInvalidParams is the JSON-RPC 2.0 code reserved for parameter errors. +// Thor's rpc/ws/conn.go:206 returns this for unsupported subscription subtypes. +const jsonRPCInvalidParams = -32602 + +// TestWsSubscribeNewHeads validates that an eth_subscribe('newHeads') call +// returns a hex subID, pushes a block-shaped notification on the next packed +// block, and a subsequent eth_unsubscribe returns true. +func TestWsSubscribeNewHeads(t *testing.T) { + wc := wsDial(t) + + subIDRaw := wsCallAndValidate(t, wc, 1, "eth_subscribe", "eth_subscribe", "newHeads") + var subID string + require.NoError(t, json.Unmarshal(subIDRaw, &subID), "unmarshal subID") + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + notif, err := wsReadNotification(t, wc, ctx, subID) + require.NoError(t, err, "wait for newHeads notification") + validateResult(t, "eth_subscription_newHeads", notif) + + unsubRaw, err := wsCall(t, wc, 2, "eth_unsubscribe", subID) + require.NoError(t, err, "eth_unsubscribe") + validateResult(t, "eth_unsubscribe", unsubRaw) + var ok bool + require.NoError(t, json.Unmarshal(unsubRaw, &ok)) + assert.True(t, ok, "eth_unsubscribe must return true for an active subID") +} + +// TestWsSubscribeLogs validates that an eth_subscribe('logs', {topics:[t]}) +// receives a LOG1 event emitted by a contract deployed in the same test. +// The contract is a 43-byte init blob that LOG1's a fixed topic and returns +// empty runtime code. +func TestWsSubscribeLogs(t *testing.T) { + wc := wsDial(t) + + // Fixed topic so we can filter the subscription to just our emission. + const topicHex = "0x1212121212121212121212121212121212121212121212121212121212121212" + + subIDRaw := wsCallAndValidate(t, wc, 1, "eth_subscribe", "eth_subscribe", "logs", map[string]any{ + "topics": []any{topicHex}, + }) + var subID string + require.NoError(t, json.Unmarshal(subIDRaw, &subID), "unmarshal subID") + + // LogOnDeploy init bytecode: + // PUSH32 topic; PUSH1 0; PUSH1 0; LOG1 ; PUSH1 0; PUSH1 0; RETURN + // Emits a single LOG1 with topic == topicHex and empty data, then returns + // zero-byte runtime code. + const initCode = "7f1212121212121212121212121212121212121212121212121212121212121212" + + "60006000a160006000f3" + + deployHash := broadcastEthTx(t, common.Hex2Bytes(initCode), nil, 200_000) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + notif, err := wsReadNotification(t, wc, ctx, subID) + require.NoError(t, err, "wait for logs notification") + validateResult(t, "eth_subscription_logs", notif) + + // Beyond schema shape, sanity-check that the emitted log carries our topic + // and ties back to the deployment tx hash. + var log map[string]any + require.NoError(t, json.Unmarshal(notif, &log), "unmarshal log payload") + topics, ok := log["topics"].([]any) + require.True(t, ok, "log.topics must be array, got %T", log["topics"]) + require.Len(t, topics, 1, "LOG1 must produce exactly one topic") + assert.Equal(t, topicHex, topics[0], "log.topics[0]") + assert.Equal(t, deployHash, log["transactionHash"], "log.transactionHash must match deploy tx") + assert.Equal(t, false, log["removed"], "removed must be false on canonical chain") + + unsubRaw, err := wsCall(t, wc, 2, "eth_unsubscribe", subID) + require.NoError(t, err, "eth_unsubscribe") + validateResult(t, "eth_unsubscribe", unsubRaw) +} + +// TestWsSubscribeNewPendingTransactions validates that an +// eth_subscribe('newPendingTransactions') pushes a tx-hash notification when +// an EIP-1559 transfer enters the pool, and that the hash matches what +// eth_sendRawTransaction returned. +func TestWsSubscribeNewPendingTransactions(t *testing.T) { + wc := wsDial(t) + + subIDRaw := wsCallAndValidate(t, wc, 1, "eth_subscribe", "eth_subscribe", "newPendingTransactions") + var subID string + require.NoError(t, json.Unmarshal(subIDRaw, &subID), "unmarshal subID") + + // Broadcast a 1-wei transfer; runNewPendingTransactions fires only for + // executable TypeEthDynamicFee txs (rpc/ws/subscriptions.go:113-117), which + // matches what signDynamicFeeTx produces. + to := common.HexToAddress("0x327931085B4cCbCE0baABb5a5E1C678707C51d90") // node2 + txHash := broadcastEthTx(t, nil, &to, 21_000) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + notif, err := wsReadNotification(t, wc, ctx, subID) + require.NoError(t, err, "wait for newPendingTransactions notification") + validateResult(t, "eth_subscription_newPendingTransactions", notif) + + var pendingHash string + require.NoError(t, json.Unmarshal(notif, &pendingHash), "unmarshal hash payload") + assert.Equal(t, strings.ToLower(txHash), strings.ToLower(pendingHash), "pending hash must match broadcast") + + unsubRaw, err := wsCall(t, wc, 2, "eth_unsubscribe", subID) + require.NoError(t, err, "eth_unsubscribe") + validateResult(t, "eth_unsubscribe", unsubRaw) +} + +// TestWsSubscribeSyncingRejected pins down that Thor's eth_subscribe rejects +// the 'syncing' subtype with InvalidParams (-32602). Standard go-ethereum +// nodes accept 'syncing'; if Thor catches up, flip this test to a success path. +// Reference: rpc/ws/conn.go:206 ('unsupported subscription type ...'). +func TestWsSubscribeSyncingRejected(t *testing.T) { + wc := wsDial(t) + + _, err := wsCall(t, wc, 1, "eth_subscribe", "syncing") + require.Error(t, err, "expected eth_subscribe('syncing') to be rejected") + + var rpcErr *jsonRPCError + require.ErrorAs(t, err, &rpcErr, "error must be a jsonRPCError") + assert.Equal(t, jsonRPCInvalidParams, rpcErr.Code, "expected InvalidParams (-32602)") + assert.Contains(t, strings.ToLower(rpcErr.Message), "unsupported subscription type") +} + +// broadcastEthTx signs and submits an EIP-1559 transaction from helper.TestSenderKey. +// Returns the tx hash from eth_sendRawTransaction. Used by the logs and pending +// subscription tests to trigger a server-side notification. +func broadcastEthTx(t *testing.T, data []byte, to *common.Address, gas uint64) string { + t.Helper() + + chainIDRaw := rpcCallAndValidate(t, "eth_chainId") + chainID := hexQuantityToInt(t, chainIDRaw) + + from := crypto.PubkeyToAddress(helper.TestSenderKey.PublicKey) + nonceRaw, err := rpcCall(t, "eth_getTransactionCount", from.Hex(), "pending") + require.NoError(t, err, "eth_getTransactionCount(pending)") + nonce := hexQuantityToInt(t, nonceRaw).Uint64() + + baseFee := fetchLatestBaseFee(t) + gasTipCap := big.NewInt(1) + gasFeeCap := new(big.Int).Add(new(big.Int).Mul(baseFee, big.NewInt(2)), gasTipCap) + + raw := signDynamicFeeTx(t, helper.TestSenderKey, chainID, nonce, gasTipCap, gasFeeCap, gas, to, big.NewInt(1), data) + + hashRaw := rpcCallAndValidate(t, "eth_sendRawTransaction", "0x"+hex.EncodeToString(raw)) + var hash string + require.NoError(t, json.Unmarshal(hashRaw, &hash), "unmarshal tx hash") + require.Regexp(t, "^0x[0-9a-fA-F]{64}$", hash) + + // Wait for the receipt on the Thor side so the test exits cleanly after + // the notification fires (some downstream tests assume past txs are mined). + thorClient := helper.NewClient(nodeURL) + b32 := thor.Bytes32(common.HexToHash(hash)) + helper.WaitForReceipt(t, thorClient, &b32, 20*time.Second) + + return hash +} diff --git a/tests/eth_rpc/eth_rpc_schema/main_test.go b/tests/eth_rpc/eth_rpc_schema/main_test.go index a2aabcf..ad596f8 100644 --- a/tests/eth_rpc/eth_rpc_schema/main_test.go +++ b/tests/eth_rpc/eth_rpc_schema/main_test.go @@ -10,5 +10,12 @@ import ( var nodeURL string func TestMain(m *testing.M) { + // The Ethereum-compatible JSON-RPC (POST/WS /rpc) lives on the thor + // pedro/eth_eq_json_rpc branch; the default evm-upgrades branch built by + // network/setup/network.go does not expose it. Set it unless the caller + // already did. Same approach as tests/eth_rpc/ethersjs/ethersjs_test.go. + if os.Getenv("THOR_BRANCH") == "" { + os.Setenv("THOR_BRANCH", "pedro/eth_eq_json_rpc") + } os.Exit(helper.RunTestMain(m, &nodeURL, nil)) } diff --git a/tests/eth_rpc/eth_rpc_schema/rpc_ws_test.go b/tests/eth_rpc/eth_rpc_schema/rpc_ws_test.go new file mode 100644 index 0000000..fe9a82b --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/rpc_ws_test.go @@ -0,0 +1,210 @@ +// WebSocket JSON-RPC harness for eth_subscribe / eth_unsubscribe. +// +// Mirrors rpc_test.go's HTTP harness — same envelope, same schema validation — +// but adds two extras only ws needs: +// +// 1. Notifications. Each `eth_subscribe` response carries a subID; the server +// then pushes `{"method":"eth_subscription","params":{"subscription":,"result":}}` +// frames asynchronously. wsReadNotification dequeues the next one, scoped +// to a subID and a deadline. +// 2. Frame demuxing. Responses (have an `id`) and notifications (don't) share +// the same wire. wsCall reads in a loop until a response with the matching +// id arrives, parking stray notifications into a per-conn queue for later +// consumption. + +package ethrpcschema + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "strings" + "sync" + "testing" + "time" + + "github.com/gorilla/websocket" + "github.com/stretchr/testify/require" +) + +// wsConn wraps a *websocket.Conn with a single-reader mutex and a buffered +// notification queue. Tests use one conn per subscription test; conn.Close() +// is wired into t.Cleanup so a panic or t.Fatal still tears it down. +type wsConn struct { + conn *websocket.Conn + readMu sync.Mutex + queueMu sync.Mutex + queued []json.RawMessage // notifications received while waiting for an RPC response +} + +// wsDial opens a WebSocket against nodeURL/rpc. Thor's eth_eq_json_rpc branch +// upgrades the same path as HTTP POST /rpc (cmd/thor/httpserver/api_server.go:161), +// so the only difference is the scheme. +func wsDial(t *testing.T) *wsConn { + t.Helper() + u, err := url.Parse(nodeURL) + require.NoError(t, err, "parse nodeURL") + switch u.Scheme { + case "http": + u.Scheme = "ws" + case "https": + u.Scheme = "wss" + } + u.Path = strings.TrimRight(u.Path, "/") + "/rpc" + + dialer := *websocket.DefaultDialer + dialer.HandshakeTimeout = 10 * time.Second + c, _, err := dialer.Dial(u.String(), nil) + require.NoError(t, err, "websocket dial %s", u.String()) + + wc := &wsConn{conn: c} + t.Cleanup(func() { + _ = c.Close() + }) + return wc +} + +// wsCall sends a JSON-RPC request and reads frames until the matching id +// arrives. Notifications received in the interim are appended to wc.queued so +// wsReadNotification can pick them up later. nil error means a JSON-RPC ok +// response; non-nil means transport failure or a jsonRPCError. +func wsCall(t *testing.T, wc *wsConn, id int, method string, params ...any) (json.RawMessage, error) { + t.Helper() + if params == nil { + params = []any{} + } + body, err := json.Marshal(jsonRPCRequest{ + JSONRPC: "2.0", + ID: id, + Method: method, + Params: params, + }) + require.NoError(t, err, "marshal ws request") + + if err := wc.conn.WriteMessage(websocket.TextMessage, body); err != nil { + return nil, fmt.Errorf("ws write: %w", err) + } + + deadline := time.Now().Add(15 * time.Second) + for { + if err := wc.conn.SetReadDeadline(deadline); err != nil { + return nil, fmt.Errorf("set read deadline: %w", err) + } + wc.readMu.Lock() + _, frame, err := wc.conn.ReadMessage() + wc.readMu.Unlock() + if err != nil { + return nil, fmt.Errorf("ws read: %w", err) + } + + // Frames without `id` are notifications. Park them for wsReadNotification. + var probe struct { + ID *int `json:"id"` + Method string `json:"method"` + Params json.RawMessage `json:"params"` + } + if jsonErr := json.Unmarshal(frame, &probe); jsonErr == nil && probe.ID == nil && probe.Method == "eth_subscription" { + wc.queueMu.Lock() + wc.queued = append(wc.queued, append(json.RawMessage(nil), frame...)) + wc.queueMu.Unlock() + continue + } + + var resp jsonRPCResponse + if err := json.Unmarshal(frame, &resp); err != nil { + return nil, fmt.Errorf("decode ws response: %w (body=%s)", err, string(frame)) + } + if resp.ID != id { + // Stray response from a different request — drop on the floor and keep reading. + continue + } + if resp.Error != nil { + return nil, resp.Error + } + return resp.Result, nil + } +} + +// wsCallAndValidate is the ws cousin of rpcCallAndValidate: call, fail on +// jsonrpc error, validate the result against schemas/.json. +func wsCallAndValidate(t *testing.T, wc *wsConn, id int, schemaName, method string, params ...any) json.RawMessage { + t.Helper() + result, err := wsCall(t, wc, id, method, params...) + require.NoError(t, err, "%s ws call", method) + validateResult(t, schemaName, result) + return result +} + +// wsSubscriptionNotification is the on-wire push envelope. We mirror Thor's +// rpc/ws/conn.go:notification — no id, method="eth_subscription", +// params={subscription, result}. +type wsSubscriptionNotification struct { + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params struct { + Subscription string `json:"subscription"` + Result json.RawMessage `json:"result"` + } `json:"params"` +} + +// wsReadNotification waits for the next notification matching subID, returning +// only the `params.result` payload. Stray notifications for other subscriptions +// are dropped. Honours ctx — pass context.WithTimeout for a real deadline. +// +// The queue is drained first (so notifications that arrived during a prior +// wsCall are consumed in order). Once the queue is empty the helper blocks on +// the socket with the ctx deadline. +func wsReadNotification(t *testing.T, wc *wsConn, ctx context.Context, subID string) (json.RawMessage, error) { + t.Helper() + for { + // Drain queued notifications first. + wc.queueMu.Lock() + for len(wc.queued) > 0 { + frame := wc.queued[0] + wc.queued = wc.queued[1:] + wc.queueMu.Unlock() + var n wsSubscriptionNotification + if err := json.Unmarshal(frame, &n); err == nil && n.Params.Subscription == subID { + return n.Params.Result, nil + } + wc.queueMu.Lock() + } + wc.queueMu.Unlock() + + // Block until ctx deadline or next frame. + dl, ok := ctx.Deadline() + if !ok { + dl = time.Now().Add(30 * time.Second) + } + if err := wc.conn.SetReadDeadline(dl); err != nil { + return nil, fmt.Errorf("set read deadline: %w", err) + } + wc.readMu.Lock() + _, frame, err := wc.conn.ReadMessage() + wc.readMu.Unlock() + if err != nil { + if ctxErr := ctx.Err(); ctxErr != nil { + return nil, ctxErr + } + return nil, fmt.Errorf("ws read: %w", err) + } + + // Filter to notifications for our subID; everything else is dropped. + var probe struct { + ID *int `json:"id"` + } + if err := json.Unmarshal(frame, &probe); err == nil && probe.ID != nil { + // Stray RPC response — drop. + continue + } + var n wsSubscriptionNotification + if err := json.Unmarshal(frame, &n); err != nil { + continue + } + if n.Method != "eth_subscription" || n.Params.Subscription != subID { + continue + } + return n.Params.Result, nil + } +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_subscribe.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_subscribe.json new file mode 100644 index 0000000..243c6a9 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_subscribe.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_subscribe.json", + "title": "eth_subscribe result", + "description": "Hex-encoded subscription ID returned by eth_subscribe. Thor encodes it as hexutil.EncodeUint64 of an incrementing counter (rpc/ws/conn.go:180).", + "$ref": "_defs.json#/$defs/quantity" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_subscription_logs.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_subscription_logs.json new file mode 100644 index 0000000..52cd0d8 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_subscription_logs.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_subscription_logs.json", + "title": "eth_subscription logs payload", + "description": "params.result for an eth_subscription notification with subscription type 'logs'. One EthLog per emission — same shape as eth_getLogs[i].", + "allOf": [ + { "$ref": "_defs.json#/$defs/log" }, + { + "type": "object", + "required": [ + "address", + "topics", + "data", + "blockNumber", + "transactionHash", + "transactionIndex", + "blockHash", + "logIndex", + "removed" + ] + } + ] +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_subscription_newHeads.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_subscription_newHeads.json new file mode 100644 index 0000000..d3151e0 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_subscription_newHeads.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_subscription_newHeads.json", + "title": "eth_subscription newHeads payload", + "description": "params.result for an eth_subscription notification with subscription type 'newHeads'. Thor delivers a full EthBlock with fullTxs=false (rpc/ws/subscriptions.go:42), so transactions is an array of hash32.", + "allOf": [ + { "$ref": "_defs.json#/$defs/blockBase" }, + { + "type": "object", + "properties": { + "transactions": { + "type": "array", + "items": { "$ref": "_defs.json#/$defs/hash32" } + } + }, + "required": ["transactions"] + } + ] +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_subscription_newPendingTransactions.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_subscription_newPendingTransactions.json new file mode 100644 index 0000000..325c593 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_subscription_newPendingTransactions.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_subscription_newPendingTransactions.json", + "title": "eth_subscription newPendingTransactions payload", + "description": "params.result for an eth_subscription notification with subscription type 'newPendingTransactions'. Thor pushes just the transaction hash for executable TypeEthDynamicFee txs (rpc/ws/subscriptions.go:118).", + "$ref": "_defs.json#/$defs/hash32" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_unsubscribe.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_unsubscribe.json new file mode 100644 index 0000000..84ace17 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_unsubscribe.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_unsubscribe.json", + "title": "eth_unsubscribe result", + "description": "Boolean — true if the subscription existed and was removed, false otherwise.", + "type": "boolean" +} diff --git a/tests/eth_rpc/ethersjs/.gitignore b/tests/eth_rpc/ethersjs/.gitignore new file mode 100644 index 0000000..3c25e1e --- /dev/null +++ b/tests/eth_rpc/ethersjs/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.log diff --git a/tests/eth_rpc/ethersjs/.mocharc.cjs b/tests/eth_rpc/ethersjs/.mocharc.cjs new file mode 100644 index 0000000..bb480f5 --- /dev/null +++ b/tests/eth_rpc/ethersjs/.mocharc.cjs @@ -0,0 +1,8 @@ +module.exports = { + require: ['ts-node/register', 'src/globalSetup.ts'], + extensions: ['ts'], + spec: ['test/**/*.test.ts'], + timeout: 120000, + reporter: 'spec', + exit: true, +}; diff --git a/tests/eth_rpc/ethersjs/README.md b/tests/eth_rpc/ethersjs/README.md new file mode 100644 index 0000000..8f04821 --- /dev/null +++ b/tests/eth_rpc/ethersjs/README.md @@ -0,0 +1,101 @@ +# ethers.js v6 RPC compatibility tests + +Mocha + TypeScript suite (41 tests) that drives Thor's Ethereum-compatible +JSON-RPC through stock `ethers@^6`. Covers provider reads (block/tx/log/fee +lookups, `eth_feeHistory`, `eth_getBlockReceipts`), offline signing +(`populateTransaction` / `signTransaction` / `broadcastTransaction`), in-line +EIP-1559 sends, EIP-191/712 signing, HD-wallet derivation, contract +deploy/view/write, payable functions with value, custom error decoding, +`Interface.parseLog`, event subscriptions (`contract.on`, `provider.on('block')`), +historical filters (`queryFilter` w/ indexed args), and HTTP long-polling +filters (`eth_newFilter` / `eth_getFilterChanges` / `eth_uninstallFilter`). + +## Known Thor ↔ Ethereum gaps + +The suite asserts these as **rejection** tests so they fail loudly when Thor +catches up — flip the assertion when that happens. + +- **Legacy (type-0) transactions are rejected.** Thor's + `eth_sendRawTransaction` only accepts EIP-1559 envelopes; legacy RLP comes + back as `rlp: expected List`. See `test/wallet.test.ts`. +- **`eth_feeHistory` rejects `rewardPercentiles`.** The method works without + the third argument (baseFee + gasUsedRatio come back fine); passing + percentiles returns `"reward percentiles are not yet supported"`. See + `test/provider.test.ts`. + +## Run + +```sh +npm install +npm test +``` + +`pretest` rebuilds `/tmp/interstellar-network` from the workspace; the global +fixture in `src/globalSetup.ts` spawns it, reads the JSON ready-line from +stdout, and SIGTERMs it on teardown. + +To target an externally-managed node, set `NODE_URL`: + +```sh +NODE_URL=http://127.0.0.1:8131 npm test +``` + +## Can Mocha call `helper.RunTestMain`? + +No — `helper.RunTestMain` is a Go function and Mocha is JavaScript. The +fixture in `src/globalSetup.ts` mirrors the same protocol the Go helper uses: + +| `helper.RunTestMain` (Go) | `mochaGlobalSetup` (TS) | +| -------------------------------------------------- | --------------------------------------------- | +| Honors `NODE_URL` env var | same | +| Spawns `/tmp/interstellar-network start` | same | +| Line-scans stdout for `{"nodes":[...],...}` JSON | same | +| 15-minute startup timeout (first-run thor compile) | same | +| `defer stop()` → SIGTERM the child | `mochaGlobalTeardown` → SIGTERM + await exit | + +So the JS fixture is a one-to-one port of the Go helper. + +## Thor branch requirement + +Thor's Ethereum-compat RPC (`POST /rpc`) ships on the `pedro/eth_eq_json_rpc` +branch. The default `evm-upgrades` branch wired into `network/setup/network.go` +does not expose it. `globalSetup.ts` sets `THOR_BRANCH=pedro/eth_eq_json_rpc` +for the spawned binary unless the caller has already exported it. + +## Regenerating the contract artifact + +`contracts/Storage.json` is checked in. To rebuild it after changing +`contracts/Storage.sol`: + +```sh +npm run compile:contracts +``` + +## Layout + +```text +src/globalSetup.ts # spawns network binary, exposes node URL via env +src/fixtures.ts # provider/wallet factories + pre-funded test keys +contracts/Storage.sol, Storage.json +scripts/compile.cjs # one-shot solc → Storage.json +test/provider.test.ts # 16 tests — block/tx/log lookups, getFeeData, + # eth_feeHistory, eth_getBlockReceipts +test/wallet.test.ts # 10 tests — EIP-1559 send, EIP-191/712 sign, HD wallet, + # populate/sign/broadcastTransaction +test/contract.test.ts # 9 tests — deploy, view/write, custom error, parseLog, + # payable tip with value +test/events.test.ts # 5 tests — contract.on, queryFilter, indexed filter, + # HTTP filter trio (newFilter/getChanges/uninstall) +``` + +The pre-funded test accounts mirror `tests/helper/client.go:16-21` — +`TEST_SENDER_ADDRESS`, `NODE2_ADDRESS`, `NODE3_ADDRESS` and their keys. + +## Coverage gaps still open + +The suite hits the main ethers v6 surface every dApp uses, but does **not** +yet cover: `WebSocketProvider` / `eth_subscribe`, `eth_blobBaseFee` (EIP-4844), +`eth_getBlockReceipts`, EIP-2930 access lists, multicall / `Multicall3`, +`getCreate2Address` deploy parity, reorg-driven `removed` event handling, +batched `JsonRpcProvider`. Add as the corresponding RPC methods land in Thor +or as a real dApp scenario calls for them. diff --git a/tests/eth_rpc/ethersjs/contracts/Create2Factory.json b/tests/eth_rpc/ethersjs/contracts/Create2Factory.json new file mode 100644 index 0000000..2c317ee --- /dev/null +++ b/tests/eth_rpc/ethersjs/contracts/Create2Factory.json @@ -0,0 +1,43 @@ +{ + "contractName": "Create2Factory", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "Deployed", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "initCode", + "type": "bytes" + } + ], + "name": "deploy", + "outputs": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x6080604052348015600f57600080fd5b506101b38061001f6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063cdcb760a14610030575b600080fd5b61004361003e3660046100ff565b61005f565b6040516001600160a01b03909116815260200160405180910390f35b6000604051828482378483826000f59150506001600160a01b0381166100bc5760405162461bcd60e51b815260206004820152600e60248201526d18dc99585d194c8819985a5b195960921b604482015260640160405180910390fd5b6040516001600160a01b03821681527ff40fcec21964ffb566044d083b4073f29f7f7929110ea19e1b3ebe375d89055e9060200160405180910390a19392505050565b60008060006040848603121561011457600080fd5b83359250602084013567ffffffffffffffff81111561013257600080fd5b8401601f8101861361014357600080fd5b803567ffffffffffffffff81111561015a57600080fd5b86602082840101111561016c57600080fd5b93966020919091019550929350505056fea264697066735822122063bb8db7af6a7c0d1a24b676438d45379658b1e0ba102228bcd89cff515b6ad964736f6c63430008230033" +} diff --git a/tests/eth_rpc/ethersjs/contracts/Create2Factory.sol b/tests/eth_rpc/ethersjs/contracts/Create2Factory.sol new file mode 100644 index 0000000..521624f --- /dev/null +++ b/tests/eth_rpc/ethersjs/contracts/Create2Factory.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract Create2Factory { + event Deployed(address addr); + + function deploy(bytes32 salt, bytes calldata initCode) external returns (address addr) { + assembly { + let memPtr := mload(0x40) + calldatacopy(memPtr, initCode.offset, initCode.length) + addr := create2(0, memPtr, initCode.length, salt) + } + require(addr != address(0), "create2 failed"); + emit Deployed(addr); + } +} diff --git a/tests/eth_rpc/ethersjs/contracts/Storage.json b/tests/eth_rpc/ethersjs/contracts/Storage.json new file mode 100644 index 0000000..380606f --- /dev/null +++ b/tests/eth_rpc/ethersjs/contracts/Storage.json @@ -0,0 +1,140 @@ +{ + "contractName": "Storage", + "abi": [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "given", + "type": "uint256" + } + ], + "name": "MustBeNonZero", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "who", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Set", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "who", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Tipped", + "type": "event" + }, + { + "inputs": [], + "name": "get", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "v", + "type": "uint256" + } + ], + "name": "set", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "v", + "type": "uint256" + } + ], + "name": "setStrict", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "v", + "type": "uint256" + } + ], + "name": "setStrictCustomError", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "tip", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "totalTipped", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "value", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x6080604052348015600f57600080fd5b506102648061001f6000396000f3fe6080604052600436106100705760003560e01c80635e170bd51161004e5780635e170bd5146100c757806360fe47b1146100e75780636d4ce63c14610107578063b814b7ed1461011c57600080fd5b80632755cd2d146100755780633fa4f2451461007f57806358dd18d3146100a7575b600080fd5b61007d61012f565b005b34801561008b57600080fd5b5061009560005481565b60405190815260200160405180910390f35b3480156100b357600080fd5b5061007d6100c2366004610215565b610166565b3480156100d357600080fd5b5061007d6100e2366004610215565b6101f1565b3480156100f357600080fd5b5061007d610102366004610215565b6101b4565b34801561011357600080fd5b50600054610095565b34801561012857600080fd5b5047610095565b60405134815233907f905516bf815c273f240e1d48d78ea7db3f1f0d00b912fc69522caf0ea70450a29060200160405180910390a2565b806000036101b45760405162461bcd60e51b815260206004820152601660248201527576616c7565206d757374206265206e6f6e2d7a65726f60501b60448201526064015b60405180910390fd5b600081905560405181815233907ffd28ec3ec2555238d8ad6f9faf3e4cd10e574ce7e7ef28b73caa53f9512f65b99060200160405180910390a250565b806000036101b45760405163251ed31d60e11b8152600481018290526024016101ab565b60006020828403121561022757600080fd5b503591905056fea2646970667358221220947e72bf8cff231dfcab3ec92546fb82ae35ca5ba5a041822cff8a7d7793e82364736f6c63430008230033" +} diff --git a/tests/eth_rpc/ethersjs/contracts/Storage.sol b/tests/eth_rpc/ethersjs/contracts/Storage.sol new file mode 100644 index 0000000..1b00926 --- /dev/null +++ b/tests/eth_rpc/ethersjs/contracts/Storage.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract Storage { + event Set(address indexed who, uint256 value); + + uint256 public value; + + function set(uint256 v) external { + value = v; + emit Set(msg.sender, v); + } + + function get() external view returns (uint256) { + return value; + } + + function setStrict(uint256 v) external { + require(v != 0, "value must be non-zero"); + value = v; + emit Set(msg.sender, v); + } + + error MustBeNonZero(uint256 given); + + function setStrictCustomError(uint256 v) external { + if (v == 0) revert MustBeNonZero(v); + value = v; + emit Set(msg.sender, v); + } + + event Tipped(address indexed who, uint256 amount); + + function tip() external payable { + emit Tipped(msg.sender, msg.value); + } + + function totalTipped() external view returns (uint256) { + return address(this).balance; + } +} diff --git a/tests/eth_rpc/ethersjs/ethersjs_test.go b/tests/eth_rpc/ethersjs/ethersjs_test.go new file mode 100644 index 0000000..c685b05 --- /dev/null +++ b/tests/eth_rpc/ethersjs/ethersjs_test.go @@ -0,0 +1,40 @@ +// Thin Go wrapper that launches the mocha + ethers.js v6 suite as part of +// `go test ./...`. The wrapper shares the network lifecycle with every other +// Go test package via helper.RunTestMain — when NODE_URL is exported (e.g. by +// `make test`), the existing network is reused; otherwise RunTestMain starts a +// fresh one. +// +// We invoke `npx mocha` directly (not `npm test`) to skip the `pretest` hook, +// which rebuilds /tmp/interstellar-network — that work is already done by +// `make build-network` or by RunTestMain. + +package ethersjs + +import ( + "os" + "os/exec" + "testing" + + "github.com/vechain/interstellar-e2e/tests/helper" +) + +var nodeURL string + +func TestMain(m *testing.M) { + os.Setenv("THOR_BRANCH", "pedro/eth_eq_json_rpc") + os.Exit(helper.RunTestMain(m, &nodeURL, nil)) +} + +func TestEthersJS(t *testing.T) { + if _, err := os.Stat("node_modules"); os.IsNotExist(err) { + t.Fatal("node_modules missing — run `make test` (auto-installs) or `npm ci` in tests/eth_rpc/ethersjs/") + } + + cmd := exec.CommandContext(t.Context(), "npx", "mocha") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = append(os.Environ(), "NODE_URL="+nodeURL) + if err := cmd.Run(); err != nil { + t.Fatalf("ethersjs mocha suite failed: %v", err) + } +} diff --git a/tests/eth_rpc/ethersjs/package-lock.json b/tests/eth_rpc/ethersjs/package-lock.json new file mode 100644 index 0000000..37da9cf --- /dev/null +++ b/tests/eth_rpc/ethersjs/package-lock.json @@ -0,0 +1,1474 @@ +{ + "name": "interstellar-eth-rpc-ethersjs", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "interstellar-eth-rpc-ethersjs", + "version": "0.0.0", + "devDependencies": { + "@types/chai": "^4.3.16", + "@types/mocha": "^10.0.6", + "@types/node": "^20.12.7", + "chai": "^4.4.1", + "ethers": "^6.13.0", + "mocha": "^10.4.0", + "solc": "^0.8.26", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://mirrors.cloud.tencent.com/npm/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://mirrors.cloud.tencent.com/npm/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://mirrors.cloud.tencent.com/npm/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "4.3.20", + "resolved": "https://mirrors.cloud.tencent.com/npm/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://mirrors.cloud.tencent.com/npm/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://mirrors.cloud.tencent.com/npm/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/command-exists": { + "version": "1.2.9", + "resolved": "https://mirrors.cloud.tencent.com/npm/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://mirrors.cloud.tencent.com/npm/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://mirrors.cloud.tencent.com/npm/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://mirrors.cloud.tencent.com/npm/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://mirrors.cloud.tencent.com/npm/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://mirrors.cloud.tencent.com/npm/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/solc": { + "version": "0.8.35", + "resolved": "https://mirrors.cloud.tencent.com/npm/solc/-/solc-0.8.35.tgz", + "integrity": "sha512-OaP/4zyoKRo2CjqZDxbtkeRlEo6MxP4FLCxntw1Agf9OSoecmwYKoFBSB34UcSKBFBucrTh3Mb0nRoJou62ibw==", + "dev": true, + "license": "MIT", + "dependencies": { + "command-exists": "^1.2.8", + "commander": "^8.1.0", + "follow-redirects": "^1.12.1", + "js-sha3": "0.8.0", + "memorystream": "^0.3.1", + "semver": "^5.5.0", + "tmp": "0.0.33" + }, + "bin": { + "solcjs": "solc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://mirrors.cloud.tencent.com/npm/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://mirrors.cloud.tencent.com/npm/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://mirrors.cloud.tencent.com/npm/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/tests/eth_rpc/ethersjs/package.json b/tests/eth_rpc/ethersjs/package.json new file mode 100644 index 0000000..6370c32 --- /dev/null +++ b/tests/eth_rpc/ethersjs/package.json @@ -0,0 +1,23 @@ +{ + "name": "interstellar-eth-rpc-ethersjs", + "private": true, + "version": "0.0.0", + "description": "ethers.js v6 compatibility tests against Thor's Ethereum-compatible RPC.", + "scripts": { + "build:network": "cd ../../.. && go build -o /tmp/interstellar-network github.com/vechain/interstellar-e2e/network", + "compile:contracts": "node scripts/compile.cjs", + "pretest": "npm run build:network", + "test": "mocha" + }, + "devDependencies": { + "@types/chai": "^4.3.16", + "@types/mocha": "^10.0.6", + "@types/node": "^20.12.7", + "chai": "^4.4.1", + "ethers": "^6.13.0", + "mocha": "^10.4.0", + "solc": "^0.8.26", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } +} diff --git a/tests/eth_rpc/ethersjs/scripts/compile.cjs b/tests/eth_rpc/ethersjs/scripts/compile.cjs new file mode 100644 index 0000000..69b7b8f --- /dev/null +++ b/tests/eth_rpc/ethersjs/scripts/compile.cjs @@ -0,0 +1,49 @@ +/* One-shot compile for every contracts/*.sol → contracts/.json. + * Run via `npm run compile:contracts`. The resulting JSON files are checked in, + * so the test suite has no solc dependency at runtime. */ +const fs = require('node:fs'); +const path = require('node:path'); +const solc = require('solc'); + +const root = path.join(__dirname, '..'); +const contractsDir = path.join(root, 'contracts'); + +const sources = {}; +for (const entry of fs.readdirSync(contractsDir)) { + if (entry.endsWith('.sol')) { + sources[entry] = { content: fs.readFileSync(path.join(contractsDir, entry), 'utf8') }; + } +} + +const input = { + language: 'Solidity', + sources, + settings: { + optimizer: { enabled: true, runs: 200 }, + evmVersion: 'paris', + outputSelection: { + '*': { '*': ['abi', 'evm.bytecode.object'] }, + }, + }, +}; + +const output = JSON.parse(solc.compile(JSON.stringify(input))); + +if (output.errors) { + const fatal = output.errors.filter((e) => e.severity === 'error'); + for (const e of output.errors) console.error(e.formattedMessage); + if (fatal.length > 0) process.exit(1); +} + +for (const [sourceFile, byName] of Object.entries(output.contracts)) { + for (const [contractName, contract] of Object.entries(byName)) { + const artifact = { + contractName, + abi: contract.abi, + bytecode: '0x' + contract.evm.bytecode.object, + }; + const outputPath = path.join(contractsDir, `${contractName}.json`); + fs.writeFileSync(outputPath, JSON.stringify(artifact, null, 2) + '\n'); + console.log(`wrote ${outputPath} (${artifact.bytecode.length / 2 - 1} bytes from ${sourceFile})`); + } +} diff --git a/tests/eth_rpc/ethersjs/src/fixtures.ts b/tests/eth_rpc/ethersjs/src/fixtures.ts new file mode 100644 index 0000000..0113d48 --- /dev/null +++ b/tests/eth_rpc/ethersjs/src/fixtures.ts @@ -0,0 +1,65 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { ethers, JsonRpcProvider, Wallet, WebSocketProvider } from 'ethers'; +import { getNodeUrl } from './globalSetup'; + +// Pre-funded master accounts from LocalThreeNodesNetwork genesis. +// Mirrors tests/helper/client.go:16-21. +export const TEST_SENDER_KEY = + '0x01a4107bfb7d5141ec519e75788c34295741a1eefbfe460320efd2ada944071e'; +export const TEST_SENDER_ADDRESS = '0x61fF580B63D3845934610222245C116E013717ec'; + +export const NODE2_KEY = + '0x7072249b800ddac1d29a3cd06468cc1a917cbcd110dde358a905d03dad51748d'; +export const NODE2_ADDRESS = '0x327931085B4cCbCE0baABb5a5E1C678707C51d90'; + +export const NODE3_KEY = + '0xc55455943bf026dc44fcf189e8765eb0587c94e66029d580bae795386c0b737a'; +export const NODE3_ADDRESS = '0x084E48c8AE79656D7e27368AE5317b5c2D6a7497'; + +export function getHttpUrl(): string { + // Thor exposes the Ethereum-compatible JSON-RPC at /rpc — the bare + // URL returns 307. Mirrors tests/eth_rpc/eth_rpc_schema/rpc_test.go:71. + return getNodeUrl().replace(/\/$/, '') + '/rpc'; +} + +export function makeProvider(): JsonRpcProvider { + return new JsonRpcProvider(getHttpUrl()); +} + +export function getWsUrl(): string { + // Thor accepts a WebSocket upgrade on the same /rpc path as HTTP POST + // (cmd/thor/httpserver/api_server.go:161 — `router.PathPrefix("/rpc").Handler(rpcWs)`). + const base = getNodeUrl().replace(/\/$/, ''); + return base.replace(/^http/, 'ws') + '/rpc'; +} + +export function makeWsProvider(): WebSocketProvider { + // Node 22+ exposes a global WebSocket constructor; ethers picks it up when + // given a URL string. CI Node version is pinned in .github/workflows/test.yml. + return new WebSocketProvider(getWsUrl()); +} + +export function makeWallet(privateKey: string, provider: JsonRpcProvider): Wallet { + return new Wallet(privateKey, provider); +} + +export interface StorageArtifact { + contractName: string; + abi: ethers.InterfaceAbi; + bytecode: string; +} + +export function loadStorageArtifact(): StorageArtifact { + return loadArtifact('Storage'); +} + +export function loadCreate2FactoryArtifact(): StorageArtifact { + return loadArtifact('Create2Factory'); +} + +function loadArtifact(name: string): StorageArtifact { + const artifactPath = path.join(__dirname, '..', 'contracts', `${name}.json`); + const raw = fs.readFileSync(artifactPath, 'utf8'); + return JSON.parse(raw) as StorageArtifact; +} diff --git a/tests/eth_rpc/ethersjs/src/globalSetup.ts b/tests/eth_rpc/ethersjs/src/globalSetup.ts new file mode 100644 index 0000000..46b17f7 --- /dev/null +++ b/tests/eth_rpc/ethersjs/src/globalSetup.ts @@ -0,0 +1,119 @@ +import { spawn, ChildProcess } from 'node:child_process'; +import { Readable } from 'node:stream'; +import * as readline from 'node:readline'; + +const NETWORK_BINARY = '/tmp/interstellar-network'; +const STARTUP_TIMEOUT_MS = 15 * 60 * 1000; +const RUNTIME_URL_ENV = 'INTERSTELLAR_RUNTIME_NODE_URL'; +const RUNTIME_P2P_ENV = 'INTERSTELLAR_RUNTIME_P2P_PORTS'; + +let networkProc: ChildProcess | null = null; + +interface ReadyLine { + nodes: string[]; + p2pPorts: number[]; +} + +function readReadyLine(proc: ChildProcess): Promise { + return new Promise((resolve, reject) => { + const stdout = proc.stdout; + if (!stdout) { + reject(new Error('network process has no stdout pipe')); + return; + } + const rl = readline.createInterface({ input: stdout as Readable }); + let timer: NodeJS.Timeout; + + const cleanup = () => { + clearTimeout(timer); + rl.close(); + }; + + timer = setTimeout(() => { + cleanup(); + reject(new Error(`network did not emit ready line within ${STARTUP_TIMEOUT_MS}ms`)); + }, STARTUP_TIMEOUT_MS); + + rl.on('line', (line) => { + try { + const parsed = JSON.parse(line); + if (Array.isArray(parsed.nodes) && parsed.nodes.length > 0) { + cleanup(); + resolve(parsed as ReadyLine); + return; + } + } catch { + // Non-JSON stdout line (e.g. git output from ThorBuilder) — forward to stderr + // so it remains visible without blocking the ready-line scan. + process.stderr.write(`[network] ${line}\n`); + } + }); + + proc.once('exit', (code, signal) => { + cleanup(); + reject(new Error(`network exited before ready (code=${code}, signal=${signal})`)); + }); + + proc.once('error', (err) => { + cleanup(); + reject(err); + }); + }); +} + +export async function mochaGlobalSetup(): Promise { + if (process.env.NODE_URL) { + process.env[RUNTIME_URL_ENV] = process.env.NODE_URL; + console.log(`[ethersjs] using external NODE_URL: ${process.env.NODE_URL}`); + return; + } + + // The Ethereum-compatible RPC endpoint (POST /rpc) lives on the thor + // `pedro/eth_eq_json_rpc` branch. The default `evm-upgrades` branch built by + // network/setup/network.go does not expose it, so set THOR_BRANCH here unless + // the caller explicitly overrode it. Same trick the schema TestMain used + // before commit 416eed1 — kept here so ethers tests are self-contained. + const childEnv = { ...process.env }; + if (!childEnv.THOR_BRANCH) { + childEnv.THOR_BRANCH = 'pedro/eth_eq_json_rpc'; + } + + console.log( + `[ethersjs] spawning ${NETWORK_BINARY} start (THOR_BRANCH=${childEnv.THOR_BRANCH})`, + ); + networkProc = spawn(NETWORK_BINARY, ['start'], { + stdio: ['ignore', 'pipe', 'inherit'], + env: childEnv, + }); + + const ready = await readReadyLine(networkProc); + process.env[RUNTIME_URL_ENV] = ready.nodes[0]; + process.env[RUNTIME_P2P_ENV] = ready.p2pPorts.join(','); + console.log( + `[ethersjs] network ready — node: ${ready.nodes[0]}, p2p: ${ready.p2pPorts.join(',')}`, + ); +} + +export async function mochaGlobalTeardown(): Promise { + if (!networkProc) return; + console.log('[ethersjs] stopping network'); + await new Promise((resolve) => { + networkProc!.once('exit', () => resolve()); + networkProc!.kill('SIGTERM'); + }); + networkProc = null; +} + +export function getNodeUrl(): string { + const url = process.env[RUNTIME_URL_ENV]; + if (!url) { + throw new Error('node URL not initialized — globalSetup must run first'); + } + return url; +} + +export function getP2PPorts(): number[] { + const raw = process.env[RUNTIME_P2P_ENV]; + if (!raw) return []; + return raw.split(',').map((s) => Number(s)); +} diff --git a/tests/eth_rpc/ethersjs/test/contract.test.ts b/tests/eth_rpc/ethersjs/test/contract.test.ts new file mode 100644 index 0000000..9cf1ba7 --- /dev/null +++ b/tests/eth_rpc/ethersjs/test/contract.test.ts @@ -0,0 +1,229 @@ +import { expect } from 'chai'; +import { + Contract, + ContractFactory, + Interface, + JsonRpcProvider, + Wallet, + getCreate2Address, + id, + keccak256, + randomBytes, + hexlify, +} from 'ethers'; +import { + loadCreate2FactoryArtifact, + loadStorageArtifact, + makeProvider, + makeWallet, + TEST_SENDER_KEY, +} from '../src/fixtures'; + +describe('Contract — deploy & call via ethers.Contract', () => { + const artifact = loadStorageArtifact(); + let provider: JsonRpcProvider; + let wallet: Wallet; + let contract: Contract; + let address: string; + + before(async () => { + provider = makeProvider(); + wallet = makeWallet(TEST_SENDER_KEY, provider); + const factory = new ContractFactory(artifact.abi, artifact.bytecode, wallet); + const deployed = await factory.deploy(); + await deployed.waitForDeployment(); + address = await deployed.getAddress(); + contract = new Contract(address, artifact.abi, wallet); + }); + + it('deploys to a non-empty contract address', async () => { + expect(address).to.match(/^0x[0-9a-fA-F]{40}$/); + const code = await provider.getCode(address); + expect(code.length).to.be.greaterThan(2); + }); + + it('initial value() and get() both return 0n', async () => { + expect(await contract.value()).to.equal(0n); + expect(await contract.get()).to.equal(0n); + }); + + it('set(42) persists the value and the receipt status is 1', async () => { + const tx = await contract.set(42n); + const receipt = await tx.wait(); + expect(receipt.status).to.equal(1); + expect(await contract.get()).to.equal(42n); + expect(await contract.value()).to.equal(42n); + }); + + it('setStrict(0) reverts with the declared reason', async () => { + let caught: unknown; + try { + const tx = await contract.setStrict(0n); + await tx.wait(); + } catch (err) { + caught = err; + } + expect(caught, 'expected setStrict(0) to revert').to.not.be.undefined; + const msg = String((caught as { shortMessage?: string; message: string }).shortMessage + ?? (caught as Error).message); + expect(msg).to.match(/value must be non-zero|reverted/i); + }); + + it('setStrictCustomError(0) reverts with a decoded custom error', async () => { + // staticCall surfaces revert data directly from eth_call, which is the + // pathway ethers' decoder is most reliable on. + let caught: unknown; + try { + await contract.setStrictCustomError.staticCall(0n); + } catch (err) { + caught = err; + } + expect(caught, 'expected custom-error revert').to.not.be.undefined; + const expectedSelector = id('MustBeNonZero(uint256)').slice(0, 10); + + // Scan every string-typed field in the error object (and one level of + // nested object) for either the decoded name or the raw selector. + const haystack: string[] = []; + const walk = (obj: unknown, depth: number) => { + if (depth > 3 || obj == null) return; + if (typeof obj === 'string') haystack.push(obj); + else if (typeof obj === 'object') { + for (const v of Object.values(obj as Record)) walk(v, depth + 1); + } + }; + walk(caught, 0); + + const found = haystack.some( + (s) => s.includes('MustBeNonZero') || s.includes(expectedSelector.slice(2)), + ); + expect(found, `no MustBeNonZero / ${expectedSelector} in error`).to.equal(true); + }); + + it('staticCall on a write function returns the call data without sending a tx', async () => { + const nonceBefore = await provider.getTransactionCount(wallet.address); + // get() is view, but staticCall on a writer just simulates without changing state. + await contract.set.staticCall(999n); + const nonceAfter = await provider.getTransactionCount(wallet.address); + expect(nonceAfter).to.equal(nonceBefore); + // And state should not have changed: + expect(await contract.get()).to.equal(42n); + }); + + it('Interface.parseLog decodes a Set event from a raw receipt log', async () => { + const tx = await contract.set(99n); + const receipt = await tx.wait(); + expect(receipt.logs.length).to.be.greaterThan(0); + + const iface = new Interface(artifact.abi); + const parsed = iface.parseLog({ + topics: receipt.logs[0].topics as string[], + data: receipt.logs[0].data, + }); + expect(parsed, 'parseLog result').to.not.be.null; + expect(parsed!.name).to.equal('Set'); + expect(parsed!.args.value).to.equal(99n); + }); + + it('contract..estimateGas returns a positive bigint via the method-level API', async () => { + // contract.set.estimateGas(...) wraps Interface.encodeFunctionData + + // provider.estimateGas — a distinct code path from the top-level + // provider.estimateGas covered elsewhere. Asserting a positive bigint + // result confirms ethers correctly threads the contract address, sender, + // and ABI-encoded calldata through Thor's eth_estimateGas. + const gas = await contract.set.estimateGas(7n); + expect(gas).to.be.a('bigint'); + expect(gas > 0n, `gas was ${gas}`).to.equal(true); + }); + + it('contract..populateTransaction returns a fully-populated TransactionRequest', async () => { + // Method-level populateTransaction goes through the contract's runner + + // Interface, separate from wallet.populateTransaction in wallet.test.ts. + // We assert the populated tx has `to`, `data` (the ABI-encoded set(99n) + // call), and the fee fields ethers fills in for EIP-1559. + const populated = await contract.set.populateTransaction(99n); + expect(populated.to, 'populated.to').to.exist; + expect(populated.to!.toLowerCase()).to.equal(address.toLowerCase()); + expect(populated.data, 'populated.data').to.match(/^0x[0-9a-fA-F]+$/); + // ABI selector for set(uint256) — first 4 bytes of keccak256("set(uint256)") + expect(populated.data!.startsWith('0x60fe47b1'), 'selector mismatch').to.equal(true); + // The encoded uint256 argument 99 must be the last 32 bytes of calldata + // (selector 4B + 32B arg = 36B = 72 hex chars after 0x). + expect(populated.data!.length).to.equal(2 + 4 * 2 + 32 * 2); + expect(populated.data!.toLowerCase()).to.match(/0+63$/, 'uint256 99 encoding'); + }); + + it('contract.attach returns a fresh handle bound to the same address', async () => { + const reattached = contract.attach(address) as Contract; + expect(await reattached.value()).to.equal(99n); + }); + + it('CREATE2 parity — getCreate2Address(deployer, salt, hash(initCode)) matches the on-chain deployed address', async function () { + this.timeout(60_000); + // Deploy Create2Factory once, then ask it to CREATE2-deploy the Storage + // contract. The address Thor actually places the new contract at must match + // ethers' client-side `getCreate2Address` prediction, byte-for-byte, or any + // dApp relying on counterfactual addresses (Safe, account abstraction) will + // silently break against this node. + const factoryArtifact = loadCreate2FactoryArtifact(); + const factoryDeploy = await new ContractFactory( + factoryArtifact.abi, + factoryArtifact.bytecode, + wallet, + ).deploy(); + await factoryDeploy.waitForDeployment(); + const factoryAddress = await factoryDeploy.getAddress(); + const factory = new Contract(factoryAddress, factoryArtifact.abi, wallet); + + const salt = hexlify(randomBytes(32)); + const initCode = artifact.bytecode; // reuse Storage's deploy bytecode + const expected = getCreate2Address(factoryAddress, salt, keccak256(initCode)); + + // Set gasLimit explicitly: ethers' estimateGas undercounts CREATE2 + the + // inner Storage constructor in Thor's compat layer, leaving the inner + // create2 OOG and returning addr=0x0 from the opcode (which trips the + // factory's require). 3M is well above the actual ~250k consumed. + const tx = await factory.deploy(salt, initCode, { gasLimit: 3_000_000n }); + const receipt = await tx.wait(); + expect(receipt.status).to.equal(1); + + // Locate the Deployed(address) event and decode its argument. + const iface = new Interface(factoryArtifact.abi); + const parsed = receipt.logs + .map((l: { topics: readonly string[]; data: string }) => + iface.parseLog({ topics: l.topics as string[], data: l.data }), + ) + .find((p: { name: string } | null) => p?.name === 'Deployed'); + expect(parsed, 'Deployed event').to.not.be.null; + const onChainAddr = parsed!.args.addr as string; + + expect(onChainAddr.toLowerCase()).to.equal(expected.toLowerCase()); + // And the contract really lives there — getCode must be non-empty. + const code = await provider.getCode(onChainAddr); + expect(code.length).to.be.greaterThan(2); + }); + + it('payable tip() accepts value, increments contract balance, emits Tipped', async () => { + const balanceBefore = await provider.getBalance(address); + + const tx = await contract.tip({ value: 1234n }); + const receipt = await tx.wait(); + expect(receipt.status).to.equal(1); + + const balanceAfter = await provider.getBalance(address); + expect(balanceAfter - balanceBefore).to.equal(1234n); + + const totalTipped = await contract.totalTipped(); + expect(totalTipped).to.equal(balanceAfter); + + // Tipped(address indexed who, uint256 amount) — locate in receipt logs + const iface = new Interface(artifact.abi); + const parsed = receipt.logs + .map((l: { topics: readonly string[]; data: string }) => + iface.parseLog({ topics: l.topics as string[], data: l.data }), + ) + .find((p: { name: string } | null) => p?.name === 'Tipped'); + expect(parsed, 'Tipped event').to.not.be.undefined; + expect(parsed!.args.who.toLowerCase()).to.equal(wallet.address.toLowerCase()); + expect(parsed!.args.amount).to.equal(1234n); + }); +}); diff --git a/tests/eth_rpc/ethersjs/test/events.test.ts b/tests/eth_rpc/ethersjs/test/events.test.ts new file mode 100644 index 0000000..4114de2 --- /dev/null +++ b/tests/eth_rpc/ethersjs/test/events.test.ts @@ -0,0 +1,213 @@ +import { expect } from 'chai'; +import { + Contract, + ContractFactory, + EventLog, + JsonRpcProvider, + Wallet, + id, + toBeHex, +} from 'ethers'; +import { + loadStorageArtifact, + makeProvider, + makeWallet, + NODE2_ADDRESS, + NODE2_KEY, + TEST_SENDER_ADDRESS, + TEST_SENDER_KEY, +} from '../src/fixtures'; + +describe('Events — subscriptions & historical filters', () => { + const artifact = loadStorageArtifact(); + let provider: JsonRpcProvider; + let wallet: Wallet; + let contract: Contract; + let deployBlock: number; + + before(async () => { + provider = makeProvider(); + wallet = makeWallet(TEST_SENDER_KEY, provider); + // ethers v6 defaults to 4s polling; tighten so this suite finishes quickly. + provider.pollingInterval = 500; + + const factory = new ContractFactory(artifact.abi, artifact.bytecode, wallet); + const deployed = await factory.deploy(); + const receipt = await deployed.deploymentTransaction()!.wait(); + deployBlock = receipt!.blockNumber; + const address = await deployed.getAddress(); + contract = new Contract(address, artifact.abi, wallet); + }); + + afterEach(async () => { + await contract.removeAllListeners(); + provider.removeAllListeners(); + }); + + it('contract.on("Set") fires when set() is called', async function () { + this.timeout(60_000); + + const seen = new Promise<{ who: string; value: bigint }>((resolve) => { + contract.on('Set', (who: string, value: bigint) => { + resolve({ who, value }); + }); + }); + + const tx = await contract.set(7n); + await tx.wait(); + + const ev = await seen; + expect(ev.who.toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + expect(ev.value).to.equal(7n); + }); + + it('queryFilter returns historical Set logs since deploy', async () => { + const tx = await contract.set(123n); + await tx.wait(); + + const events = await contract.queryFilter( + contract.filters.Set!(), + deployBlock, + 'latest', + ); + expect(events.length).to.be.greaterThan(0); + const last = events[events.length - 1] as EventLog; + expect(last.args).to.exist; + expect(last.args.value).to.equal(123n); + expect((last.args.who as string).toLowerCase()).to.equal( + TEST_SENDER_ADDRESS.toLowerCase(), + ); + }); + + it('provider.on("block") observes at least one new block', async function () { + this.timeout(60_000); + + const seen = new Promise((resolve) => { + provider.on('block', (n: number) => resolve(n)); + }); + + const n = await seen; + expect(n).to.be.a('number').and.greaterThan(0); + }); + + it('HTTP filter trio (eth_newFilter / eth_getFilterChanges / eth_uninstallFilter)', async function () { + this.timeout(60_000); + + const address = await contract.getAddress(); + const topic = id('Set(address,uint256)'); + const fromBlock = toBeHex(await provider.getBlockNumber()); + + // 1. eth_newFilter — register interest in Set events from our contract + const filterId = (await provider.send('eth_newFilter', [ + { fromBlock, toBlock: 'latest', address, topics: [topic] }, + ])) as string; + expect(filterId).to.match(/^0x[0-9a-fA-F]+$/); + + try { + // 2. trigger a Set, then poll eth_getFilterChanges + const tx = await contract.set(8675309n); + await tx.wait(); + + let changes: unknown[] = []; + const deadline = Date.now() + 30_000; + while (Date.now() < deadline) { + changes = (await provider.send('eth_getFilterChanges', [filterId])) as unknown[]; + if (changes.length > 0) break; + await new Promise((r) => setTimeout(r, 500)); + } + expect(changes.length, 'eth_getFilterChanges').to.be.greaterThan(0); + + const log = changes[0] as { topics: string[]; data: string; address: string }; + expect(log.address.toLowerCase()).to.equal(address.toLowerCase()); + expect(log.topics[0]).to.equal(topic); + } finally { + // 3. eth_uninstallFilter — must return true + const removed = (await provider.send('eth_uninstallFilter', [filterId])) as boolean; + expect(removed, 'eth_uninstallFilter').to.equal(true); + } + }); + + it('eth_getLogs accepts address[] and OR-of-topic / null-slot filter shapes', async function () { + this.timeout(60_000); + // ethers v6's high-level provider.getLogs forwards address arrays and + // topic-array (OR-of-topic) / null-slot (wildcard) filter shapes verbatim + // to eth_getLogs. Exercise all three on a real emitter so the wire + // serialization is verified end-to-end, not just the well-formed-response + // path. + const address = await contract.getAddress(); + const setTopic = id('Set(address,uint256)'); + const tippedTopic = id('Tipped(address,uint256)'); + + // Emit one of each event so the OR-filter has matches on both signatures. + await (await contract.set(2024n)).wait(); + await (await contract.tip({ value: 5n })).wait(); + + // address[]: real contract + zero address. Zero matches nothing, so every + // returned log must still belong to the real contract. + const multiAddr = await provider.getLogs({ + fromBlock: deployBlock, + toBlock: 'latest', + address: [address, '0x0000000000000000000000000000000000000000'], + }); + expect(multiAddr, 'multi-address result').to.be.an('array').and.length.greaterThan(0); + for (const l of multiAddr) { + expect(l.address.toLowerCase()).to.equal(address.toLowerCase()); + } + + // topics: [[setTopic, tippedTopic]] — OR at position 0. Result must + // include at least one log for each signature. + const orTopic = await provider.getLogs({ + fromBlock: deployBlock, + toBlock: 'latest', + address, + topics: [[setTopic, tippedTopic]], + }); + const sigs = new Set(orTopic.map((l) => l.topics[0])); + expect(sigs.has(setTopic), 'OR-of-topic must include Set').to.equal(true); + expect(sigs.has(tippedTopic), 'OR-of-topic must include Tipped').to.equal(true); + + // topics: [setTopic, null] — null in position 1 is a wildcard on the + // indexed-sender topic; must still match Set events. + const nullSlot = await provider.getLogs({ + fromBlock: deployBlock, + toBlock: 'latest', + address, + topics: [setTopic, null], + }); + expect(nullSlot.length, 'null-slot must match Set logs').to.be.greaterThan(0); + for (const l of nullSlot) { + expect(l.topics[0]).to.equal(setTopic); + } + }); + + it('queryFilter with an indexed-arg filter matches only that address', async function () { + this.timeout(90_000); + // Send Set from TEST_SENDER and Set from NODE2; filter on TEST_SENDER must + // see only its own emission. + const node2Wallet = makeWallet(NODE2_KEY, provider); + const senderTx = await contract.set(1001n); + const senderReceipt = await senderTx.wait(); + const node2Tx = await (contract.connect(node2Wallet) as Contract).set(1002n); + const node2Receipt = await node2Tx.wait(); + const fromBlock = Math.min(senderReceipt!.blockNumber, node2Receipt!.blockNumber); + + const senderOnly = await contract.queryFilter( + contract.filters.Set!(TEST_SENDER_ADDRESS), + fromBlock, + 'latest', + ); + const node2Only = await contract.queryFilter( + contract.filters.Set!(NODE2_ADDRESS), + fromBlock, + 'latest', + ); + + const senderValues = senderOnly.map((e) => (e as EventLog).args.value); + const node2Values = node2Only.map((e) => (e as EventLog).args.value); + + expect(senderValues, 'sender-only').to.include(1001n); + expect(senderValues, 'sender-only').to.not.include(1002n); + expect(node2Values, 'node2-only').to.include(1002n); + expect(node2Values, 'node2-only').to.not.include(1001n); + }); +}); diff --git a/tests/eth_rpc/ethersjs/test/provider.test.ts b/tests/eth_rpc/ethersjs/test/provider.test.ts new file mode 100644 index 0000000..6270dea --- /dev/null +++ b/tests/eth_rpc/ethersjs/test/provider.test.ts @@ -0,0 +1,440 @@ +import { expect } from 'chai'; +import { JsonRpcProvider, Wallet, ZeroAddress, id, toBeHex } from 'ethers'; +import { + getHttpUrl, + makeProvider, + makeWallet, + TEST_SENDER_ADDRESS, + NODE2_ADDRESS, + TEST_SENDER_KEY, +} from '../src/fixtures'; + +// Walk an unknown error / response and collect every string-valued leaf — used +// when an RPC error's diagnostic text may live at .message, .shortMessage, +// .info.error.message, etc. +function collectStrings(obj: unknown): string[] { + const out: string[] = []; + const walk = (o: unknown, depth: number) => { + if (depth > 3 || o == null) return; + if (typeof o === 'string') out.push(o); + else if (typeof o === 'object') + for (const v of Object.values(o as Record)) walk(v, depth + 1); + }; + walk(obj, 0); + return out; +} + +describe('Provider read-only RPC', () => { + let provider: JsonRpcProvider; + before(() => { + provider = makeProvider(); + }); + + it('getBlockNumber returns a positive integer', async () => { + const n = await provider.getBlockNumber(); + expect(n).to.be.a('number').and.to.be.greaterThan(0); + }); + + it('getBlock("latest") returns a block with expected fields', async () => { + const block = await provider.getBlock('latest'); + expect(block, 'latest block').to.not.be.null; + expect(block!.number).to.be.greaterThan(0); + expect(block!.hash).to.match(/^0x[0-9a-fA-F]{64}$/); + expect(block!.parentHash).to.match(/^0x[0-9a-fA-F]{64}$/); + expect(block!.timestamp).to.be.a('number').and.greaterThan(0); + }); + + it('getBalance returns a non-negative bigint for funded sender', async () => { + const bal = await provider.getBalance(TEST_SENDER_ADDRESS); + expect(bal).to.be.a('bigint'); + expect(bal > 0n, `balance was ${bal}`).to.equal(true); + }); + + it('getTransactionCount returns a non-negative number', async () => { + const n = await provider.getTransactionCount(TEST_SENDER_ADDRESS); + expect(n).to.be.a('number').and.to.be.at.least(0); + }); + + it('getCode for a non-contract address returns 0x', async () => { + const code = await provider.getCode(NODE2_ADDRESS); + expect(code).to.equal('0x'); + }); + + it('call returns 0x for a no-op call to EOA', async () => { + const result = await provider.call({ to: ZeroAddress, data: '0x' }); + expect(result).to.equal('0x'); + }); + + it('estimateGas returns a positive bigint for a plain value transfer', async () => { + const gas = await provider.estimateGas({ + from: TEST_SENDER_ADDRESS, + to: NODE2_ADDRESS, + value: 1n, + }); + expect(gas).to.be.a('bigint'); + expect(gas > 0n, `gas was ${gas}`).to.equal(true); + }); + + it('chainId from getNetwork matches direct eth_chainId', async () => { + const fromNet = (await provider.getNetwork()).chainId; + const direct = await provider.send('eth_chainId', []); + expect(toBeHex(fromNet)).to.equal(toBeHex(BigInt(direct))); + }); + + it('getFeeData returns gasPrice and/or EIP-1559 fee fields', async () => { + const fee = await provider.getFeeData(); + // At minimum one of these must be populated; EIP-1559 chains return all three. + const hasAny = + fee.gasPrice !== null || + fee.maxFeePerGas !== null || + fee.maxPriorityFeePerGas !== null; + expect(hasAny, JSON.stringify(fee, (_, v) => (typeof v === 'bigint' ? v.toString() : v))) + .to.equal(true); + }); + + describe('tx & log lookups (after a real send)', () => { + let wallet: Wallet; + let txHash: string; + let blockNumber: number; + + before(async () => { + wallet = makeWallet(TEST_SENDER_KEY, provider); + const tx = await wallet.sendTransaction({ + to: NODE2_ADDRESS, + value: 1n, + type: 2, + }); + const r = await tx.wait(); + txHash = tx.hash; + blockNumber = r!.blockNumber; + }); + + it('getTransaction by hash returns the sent tx', async () => { + const t = await provider.getTransaction(txHash); + expect(t, 'tx lookup').to.not.be.null; + expect(t!.hash).to.equal(txHash); + expect(t!.from.toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + expect(t!.to!.toLowerCase()).to.equal(NODE2_ADDRESS.toLowerCase()); + expect(t!.value).to.equal(1n); + }); + + it('getTransactionReceipt by hash returns a status-1 receipt', async () => { + const r = await provider.getTransactionReceipt(txHash); + expect(r, 'receipt lookup').to.not.be.null; + expect(r!.status).to.equal(1); + expect(r!.blockNumber).to.equal(blockNumber); + }); + + it('waitForTransaction resolves with the receipt', async () => { + const r = await provider.waitForTransaction(txHash, 1, 30_000); + expect(r, 'waitForTransaction').to.not.be.null; + expect(r!.hash).to.equal(txHash); + }); + + it('getLogs returns logs for a known block range', async () => { + // Plain value transfer doesn't emit logs, but the call itself must succeed + // and return an array — the Set events test exercises non-empty cases. + const logs = await provider.getLogs({ + fromBlock: blockNumber, + toBlock: blockNumber, + }); + expect(logs).to.be.an('array'); + }); + + it('getStorage returns 0x00..00 for an empty EOA slot 0', async () => { + const slot = await provider.getStorage(NODE2_ADDRESS, 0); + expect(slot).to.match(/^0x0+$/); + }); + }); + + it('eth_feeHistory returns baseFee and gasUsedRatio (no percentiles)', async () => { + // Thor accepts eth_feeHistory but rejects rewardPercentiles with + // "reward percentiles are not yet supported" — call without the third arg. + // The non-percentile shape is the path ethers' getFeeData uses internally. + const raw = await provider.send('eth_feeHistory', ['0x4', 'latest', []]); + expect(raw, 'eth_feeHistory result').to.be.an('object'); + expect(raw.oldestBlock, 'oldestBlock').to.match(/^0x[0-9a-fA-F]+$/); + expect(raw.baseFeePerGas).to.be.an('array').and.to.have.length.greaterThan(0); + expect(raw.gasUsedRatio).to.be.an('array').and.to.have.length.greaterThan(0); + }); + + it('eth_feeHistory with rewardPercentiles is rejected by Thor', async () => { + let caught: unknown; + try { + await provider.send('eth_feeHistory', ['0x4', 'latest', [25, 50, 75]]); + } catch (err) { + caught = err; + } + expect(caught, 'expected percentile request to be rejected').to.not.be.undefined; + const haystack: string[] = []; + const walk = (obj: unknown, depth: number) => { + if (depth > 3 || obj == null) return; + if (typeof obj === 'string') haystack.push(obj); + else if (typeof obj === 'object') + for (const v of Object.values(obj as Record)) walk(v, depth + 1); + }; + walk(caught, 0); + expect(haystack.join(' || ')).to.match(/percentile|not yet supported|coalesce/i); + }); + + it('eth_getBlockReceipts returns the receipt array for the latest block', async () => { + const receipts = await provider.send('eth_getBlockReceipts', ['latest']); + expect(receipts).to.be.an('array'); + for (const r of receipts as Array>) { + expect(r.blockHash, 'receipt.blockHash').to.match(/^0x[0-9a-fA-F]{64}$/); + expect(r.transactionHash, 'receipt.transactionHash').to.match(/^0x[0-9a-fA-F]{64}$/); + expect(r.status, 'receipt.status').to.match(/^0x[01]$/); + } + }); + + it('getLogs with topic filter returns event sig matches', async () => { + // Topic for keccak256("Set(address,uint256)") + const topic = id('Set(address,uint256)'); + const latest = await provider.getBlockNumber(); + const from = Math.max(0, latest - 100); + const logs = await provider.getLogs({ + fromBlock: from, + toBlock: 'latest', + topics: [topic], + }); + expect(logs).to.be.an('array'); + // We don't assert non-empty — this provider test may run before the contract + // suite emits any Set events. The point is that the topic filter is accepted + // and produces a well-formed response. + }); + + describe('getBlock variants', () => { + it('getBlock() round-trips with the latest block', async () => { + const latest = await provider.getBlock('latest'); + expect(latest, 'latest block').to.not.be.null; + const byHash = await provider.getBlock(latest!.hash!); + expect(byHash, 'block by hash').to.not.be.null; + expect(byHash!.number).to.equal(latest!.number); + expect(byHash!.hash).to.equal(latest!.hash); + }); + + it('getBlock() matches getBlock("latest") at the same height', async () => { + const latest = await provider.getBlock('latest'); + expect(latest, 'latest block').to.not.be.null; + const byNumber = await provider.getBlock(latest!.number); + expect(byNumber, 'block by number').to.not.be.null; + expect(byNumber!.hash).to.equal(latest!.hash); + }); + + it('getBlock(, true) prefetches transaction objects when present', async () => { + // Send a tx to guarantee at least one in the resulting block, then re-fetch + // that block with prefetchTxs=true. ethers v6 exposes the prefetched + // objects via block.prefetchedTransactions / block.getPrefetchedTransaction. + const wallet = makeWallet(TEST_SENDER_KEY, provider); + const tx = await wallet.sendTransaction({ + to: NODE2_ADDRESS, + value: 1n, + type: 2, + }); + const receipt = await tx.wait(); + const block = await provider.getBlock(receipt!.blockNumber, true); + expect(block, 'block with txs').to.not.be.null; + expect(block!.transactions.length).to.be.greaterThan(0); + const prefetched = block!.prefetchedTransactions; + expect(prefetched, 'prefetchedTransactions').to.be.an('array').and.length.greaterThan(0); + const found = prefetched.find((p) => p.hash === tx.hash); + expect(found, `prefetched tx ${tx.hash}`).to.exist; + expect(found!.from.toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + }); + }); + + describe('block tag handling', () => { + it('getBlock("earliest") returns the genesis block at #0', async () => { + const block = await provider.getBlock('earliest'); + expect(block, 'earliest block').to.not.be.null; + expect(block!.number).to.equal(0); + }); + + it('getBlock("finalized") returns a block at or below latest', async () => { + const finalized = await provider.getBlock('finalized'); + const latest = await provider.getBlock('latest'); + expect(finalized, 'finalized block').to.not.be.null; + expect(latest, 'latest block').to.not.be.null; + expect(finalized!.number).to.be.at.most(latest!.number); + }); + + it('getBlock("safe") returns a block at or below latest', async () => { + const safe = await provider.getBlock('safe'); + const latest = await provider.getBlock('latest'); + expect(safe, 'safe block').to.not.be.null; + expect(latest, 'latest block').to.not.be.null; + expect(safe!.number).to.be.at.most(latest!.number); + }); + + it('getBlock("pending") returns a block (Thor mirrors latest — no separate mempool)', async () => { + // Thor's eth_eq_json_rpc accepts the 'pending' tag but has no public + // mempool, so the response is a block-shaped object tracking the head + // (typically equal to latest at fetch time, possibly +1 if a new block + // was packed between the two reads). We only assert that the call + // succeeds and returns a sane block — if Thor ever surfaces a real + // mempool view, tighten this. + const latest = await provider.getBlock('latest'); + const pending = await provider.getBlock('pending'); + expect(latest, 'latest block').to.not.be.null; + expect(pending, 'pending block').to.not.be.null; + expect(pending!.number).to.be.a('number').and.at.least(0); + }); + }); + + describe('JsonRpcSigner — node-side keystore path (expected absent on Thor)', () => { + it('eth_accounts is reachable and returns an empty array (no unlocked keys)', async () => { + // Thor's RPC implements eth_accounts but the node holds no signing keys — + // every dApp that tries provider.getSigner() ends up here. Asserting [] keeps + // the gap pinned down; if Thor ever exposes node-side keys this flips. + const accounts = (await provider.send('eth_accounts', [])) as unknown; + expect(accounts).to.be.an('array'); + expect((accounts as unknown[]).length).to.equal(0); + }); + + it('eth_sendTransaction is rejected — no node-side signer to deliver to', async () => { + let caught: unknown; + try { + await provider.send('eth_sendTransaction', [ + { from: TEST_SENDER_ADDRESS, to: NODE2_ADDRESS, value: '0x1' }, + ]); + } catch (err) { + caught = err; + } + expect(caught, 'expected eth_sendTransaction to be rejected').to.not.be.undefined; + const blob = collectStrings(caught).join(' || '); + expect(blob.length, 'error must carry a diagnostic message').to.be.greaterThan(0); + }); + + it('personal_sign is rejected — no node-side keys to sign with', async () => { + let caught: unknown; + try { + await provider.send('personal_sign', ['0x68656c6c6f', TEST_SENDER_ADDRESS]); + } catch (err) { + caught = err; + } + expect(caught, 'expected personal_sign to be rejected').to.not.be.undefined; + }); + }); + + describe('EIP-4844 / blob fees (expected unsupported on Thor)', () => { + it('eth_blobBaseFee is rejected — Thor has not implemented EIP-4844', async () => { + // Standard go-ethereum returns a QUANTITY hex string here. Thor's + // Ethereum-compat RPC does not register this method; the call must + // surface as a JSON-RPC error. Flip to a success path when Thor + // ships 4844 (and add a schema entry on the Go side). + let caught: unknown; + try { + await provider.send('eth_blobBaseFee', []); + } catch (err) { + caught = err; + } + expect(caught, 'expected eth_blobBaseFee to be rejected').to.not.be.undefined; + const blob = collectStrings(caught).join(' || '); + expect(blob.length, 'error must carry a diagnostic message').to.be.greaterThan(0); + }); + }); + + describe('JsonRpcProvider batching (HTTP)', () => { + it('batchMaxCount=5 carries 3 concurrent reads in a single batched POST', async () => { + // ethers v6 collates concurrent send() calls into a JSON array up to + // batchMaxCount, then flushes after batchStallTime ms. We can't trivially + // assert the framing without intercepting fetch — but if Thor handles + // the batch envelope, every promise resolves with its own result. + const batched = new JsonRpcProvider(getHttpUrl(), undefined, { + batchMaxCount: 5, + batchStallTime: 10, + staticNetwork: true, + }); + try { + const [bn, chainIdRaw, gp] = await Promise.all([ + batched.send('eth_blockNumber', []), + batched.send('eth_chainId', []), + batched.send('eth_gasPrice', []), + ]); + expect(bn).to.match(/^0x[0-9a-fA-F]+$/); + expect(chainIdRaw).to.match(/^0x[0-9a-fA-F]+$/); + expect(gp).to.match(/^0x[0-9a-fA-F]+$/); + } finally { + await batched.destroy(); + } + }); + + it('raw 11-request batch exceeds Thor maxBatchRequests=10 and the whole batch is rejected', async () => { + // Thor's HTTP jsonrpc dispatcher caps batches at 10 requests + // (thor/rpc/jsonrpc.maxBatchRequests). An 11-request batch posted raw + // must either be rejected with an HTTP 4xx or come back as a JSON-RPC + // error response, NOT as 11 successful results. + const batch = Array.from({ length: 11 }, (_, i) => ({ + jsonrpc: '2.0', + id: i, + method: 'eth_blockNumber', + params: [], + })); + const resp = await fetch(getHttpUrl(), { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(batch), + }); + const text = await resp.text(); + let parsed: unknown; + try { + parsed = JSON.parse(text); + } catch { + parsed = null; + } + + // Three valid shapes Thor might use, all of which count as rejection: + // 1. HTTP non-2xx (e.g. 400/413) + // 2. A single error envelope: { error: { code, message } } + // 3. An array shorter than 11 (truncated) — Thor refused some entries + const okHttp = resp.ok; + const isErrorEnvelope = + parsed != null && typeof parsed === 'object' && !Array.isArray(parsed) && + 'error' in (parsed as Record); + const isAllSuccess = + Array.isArray(parsed) && (parsed as unknown[]).length === 11 && + (parsed as Array>).every((r) => 'result' in r && !('error' in r)); + + expect(!okHttp || isErrorEnvelope || !isAllSuccess, + `expected Thor to reject an 11-request batch, got ok=${okHttp} body=${text.slice(0, 200)}`) + .to.equal(true); + }); + }); + + describe('EIP-1898 block reference forms', () => { + it('eth_getBalance accepts {blockNumber: tag} object form', async () => { + const bal = await provider.send('eth_getBalance', [ + TEST_SENDER_ADDRESS, + { blockNumber: 'latest' }, + ]); + expect(bal).to.match(/^0x[0-9a-fA-F]+$/); + }); + + it('eth_getBalance accepts {blockHash: ...} object form', async () => { + const latest = await provider.getBlock('latest'); + expect(latest, 'latest block').to.not.be.null; + const bal = await provider.send('eth_getBalance', [ + TEST_SENDER_ADDRESS, + { blockHash: latest!.hash }, + ]); + expect(bal).to.match(/^0x[0-9a-fA-F]+$/); + }); + + it('eth_call accepts {blockNumber: tag} object form', async () => { + const result = await provider.send('eth_call', [ + { to: ZeroAddress, data: '0x' }, + { blockNumber: 'latest' }, + ]); + expect(result).to.equal('0x'); + }); + + it('eth_getTransactionCount accepts {blockNumber: 0x} numeric object form', async () => { + const latestBn = await provider.getBlockNumber(); + const n = await provider.send('eth_getTransactionCount', [ + TEST_SENDER_ADDRESS, + { blockNumber: toBeHex(latestBn) }, + ]); + expect(n).to.match(/^0x[0-9a-fA-F]+$/); + }); + }); +}); diff --git a/tests/eth_rpc/ethersjs/test/wallet.test.ts b/tests/eth_rpc/ethersjs/test/wallet.test.ts new file mode 100644 index 0000000..1c50f54 --- /dev/null +++ b/tests/eth_rpc/ethersjs/test/wallet.test.ts @@ -0,0 +1,250 @@ +import { expect } from 'chai'; +import { + HDNodeWallet, + Mnemonic, + Transaction, + verifyMessage, + verifyTypedData, +} from 'ethers'; +import { + makeProvider, + makeWallet, + NODE2_ADDRESS, + TEST_SENDER_ADDRESS, + TEST_SENDER_KEY, +} from '../src/fixtures'; + +describe('Wallet — sign & send EIP-1559 tx', () => { + it('sends 1 wei and receives a successful receipt with correct balance delta', async () => { + const provider = makeProvider(); + const wallet = makeWallet(TEST_SENDER_KEY, provider); + + const before = await provider.getBalance(NODE2_ADDRESS); + + const tx = await wallet.sendTransaction({ + to: NODE2_ADDRESS, + value: 1n, + type: 2, + }); + const receipt = await tx.wait(); + + expect(receipt, 'receipt').to.not.be.null; + expect(receipt!.status).to.equal(1); + expect(receipt!.from.toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + expect(receipt!.to!.toLowerCase()).to.equal(NODE2_ADDRESS.toLowerCase()); + expect(receipt!.hash).to.match(/^0x[0-9a-fA-F]{64}$/); + + const after = await provider.getBalance(NODE2_ADDRESS); + expect(after - before).to.equal(1n); + }); + + it('rejects an unfunded address with insufficient funds error', async () => { + const provider = makeProvider(); + // Random unfunded key + const unfundedKey = + '0x' + '11'.repeat(32); + const wallet = makeWallet(unfundedKey, provider); + + let threw = false; + try { + const tx = await wallet.sendTransaction({ + to: NODE2_ADDRESS, + value: 1n, + type: 2, + }); + await tx.wait(); + } catch (err) { + threw = true; + } + expect(threw, 'expected unfunded send to throw').to.equal(true); + }); + + it('EIP-2930 (type 1) access-list transactions are rejected by Thor — only EIP-1559 is accepted', async () => { + // Same gap as the legacy rejection below, just one tx-type up. Thor's + // eth_sendRawTransaction enforces TypeEthDynamicFee on the wire; everything + // else round-trips back with an RLP / unsupported-type error. If Thor ever + // accepts EIP-2930, drop this assertion and update README. + const provider = makeProvider(); + const wallet = makeWallet(TEST_SENDER_KEY, provider); + + let caught: unknown; + try { + const tx = await wallet.sendTransaction({ + to: NODE2_ADDRESS, + value: 1n, + type: 1, + accessList: [ + { address: NODE2_ADDRESS, storageKeys: [] }, + ], + }); + await tx.wait(); + } catch (err) { + caught = err; + } + expect(caught, 'expected EIP-2930 tx to be rejected').to.not.be.undefined; + const haystack: string[] = []; + const walk = (obj: unknown, depth: number) => { + if (depth > 3 || obj == null) return; + if (typeof obj === 'string') haystack.push(obj); + else if (typeof obj === 'object') { + for (const v of Object.values(obj as Record)) walk(v, depth + 1); + } + }; + walk(caught, 0); + const blob = haystack.join(' || '); + expect(blob).to.match(/rlp|access|unsupported|expected List|coalesce|type/i); + }); + + it('Legacy (type 0) transactions are rejected by Thor — only EIP-1559 is accepted', async () => { + // Documenting a real compatibility gap: Thor's Ethereum-compat RPC only + // accepts EIP-1559 (type 2) envelopes. A legacy RLP submission comes back + // as `rlp: expected List` from eth_sendRawTransaction. If this assertion + // ever flips to a successful send, drop the test and update the README. + const provider = makeProvider(); + const wallet = makeWallet(TEST_SENDER_KEY, provider); + + let caught: unknown; + try { + const tx = await wallet.sendTransaction({ + to: NODE2_ADDRESS, + value: 1n, + type: 0, + }); + await tx.wait(); + } catch (err) { + caught = err; + } + expect(caught, 'expected legacy tx to be rejected').to.not.be.undefined; + // The diagnostic text we care about can live in .message, .shortMessage, + // .info.error.message, etc. — scan everything and assert the RLP gripe is + // somewhere in there. + const haystack: string[] = []; + const walk = (obj: unknown, depth: number) => { + if (depth > 3 || obj == null) return; + if (typeof obj === 'string') haystack.push(obj); + else if (typeof obj === 'object') { + for (const v of Object.values(obj as Record)) walk(v, depth + 1); + } + }; + walk(caught, 0); + const blob = haystack.join(' || '); + expect(blob).to.match(/rlp|legacy|unsupported|expected List|coalesce/i); + }); + + it('signMessage produces a signature that verifyMessage recovers', async () => { + const provider = makeProvider(); + const wallet = makeWallet(TEST_SENDER_KEY, provider); + const message = 'hello thor'; + + const sig = await wallet.signMessage(message); + expect(sig).to.match(/^0x[0-9a-fA-F]{130}$/); + + const recovered = verifyMessage(message, sig); + expect(recovered.toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + }); + + it('signTypedData (EIP-712) produces a signature that verifyTypedData recovers', async () => { + const provider = makeProvider(); + const wallet = makeWallet(TEST_SENDER_KEY, provider); + + const chainId = (await provider.getNetwork()).chainId; + const domain = { + name: 'InterstellarTest', + version: '1', + chainId, + verifyingContract: '0x0000000000000000000000000000000000000000', + }; + const types = { + Mail: [ + { name: 'from', type: 'address' }, + { name: 'to', type: 'address' }, + { name: 'contents', type: 'string' }, + ], + }; + const message = { + from: TEST_SENDER_ADDRESS, + to: NODE2_ADDRESS, + contents: 'hi', + }; + + const sig = await wallet.signTypedData(domain, types, message); + expect(sig).to.match(/^0x[0-9a-fA-F]{130}$/); + + const recovered = verifyTypedData(domain, types, message, sig); + expect(recovered.toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + }); + + it('populateTransaction fills nonce, gasLimit, and EIP-1559 fee fields', async () => { + const provider = makeProvider(); + const wallet = makeWallet(TEST_SENDER_KEY, provider); + + const populated = await wallet.populateTransaction({ + to: NODE2_ADDRESS, + value: 1n, + type: 2, + }); + + expect(populated.nonce).to.be.a('number').and.to.be.at.least(0); + expect(typeof populated.gasLimit === 'bigint' || typeof populated.gasLimit === 'string') + .to.equal(true); + expect(populated.maxFeePerGas, 'maxFeePerGas').to.not.be.undefined; + expect(populated.maxPriorityFeePerGas, 'maxPriorityFeePerGas').to.not.be.undefined; + expect(Number(populated.chainId)).to.be.greaterThan(0); + expect(populated.from!.toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + }); + + it('signTransaction produces a raw RLP that Transaction.from parses back', async () => { + const provider = makeProvider(); + const wallet = makeWallet(TEST_SENDER_KEY, provider); + + const populated = await wallet.populateTransaction({ + to: NODE2_ADDRESS, + value: 2n, + type: 2, + }); + // signTransaction needs `from` stripped — ethers v6 rejects the populated `from`. + const { from: _from, ...toSign } = populated; + const raw = await wallet.signTransaction(toSign); + expect(raw).to.match(/^0x[0-9a-fA-F]+$/); + + const parsed = Transaction.from(raw); + expect(parsed.to!.toLowerCase()).to.equal(NODE2_ADDRESS.toLowerCase()); + expect(parsed.value).to.equal(2n); + expect(parsed.type).to.equal(2); + expect(parsed.from!.toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + }); + + it('broadcastTransaction accepts an offline-signed raw tx and confirms it', async () => { + const provider = makeProvider(); + const wallet = makeWallet(TEST_SENDER_KEY, provider); + + const before = await provider.getBalance(NODE2_ADDRESS); + + const populated = await wallet.populateTransaction({ + to: NODE2_ADDRESS, + value: 3n, + type: 2, + }); + const { from: _from, ...toSign } = populated; + const raw = await wallet.signTransaction(toSign); + + const sent = await provider.broadcastTransaction(raw); + const receipt = await sent.wait(); + expect(receipt!.status).to.equal(1); + expect(sent.hash).to.match(/^0x[0-9a-fA-F]{64}$/); + + const after = await provider.getBalance(NODE2_ADDRESS); + expect(after - before).to.equal(3n); + }); + + it('HDNodeWallet.fromPhrase produces a deterministic address from a mnemonic', () => { + const phrase = + 'test test test test test test test test test test test junk'; + // fromPhrase defaults to the standard Ethereum path m/44'/60'/0'/0/0, + // which is what the well-known Hardhat/Anvil "test junk" mnemonic uses. + const child = HDNodeWallet.fromPhrase(phrase); + expect(child.address).to.equal('0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'); + // Sanity-check the Mnemonic struct round-trips the same phrase. + expect(Mnemonic.fromPhrase(phrase).phrase).to.equal(phrase); + }); +}); diff --git a/tests/eth_rpc/ethersjs/test/websocket.test.ts b/tests/eth_rpc/ethersjs/test/websocket.test.ts new file mode 100644 index 0000000..152bc9a --- /dev/null +++ b/tests/eth_rpc/ethersjs/test/websocket.test.ts @@ -0,0 +1,138 @@ +import { expect } from 'chai'; +import { + Contract, + ContractFactory, + WebSocketProvider, + Wallet, +} from 'ethers'; +import { + loadStorageArtifact, + makeProvider, + makeWallet, + makeWsProvider, + NODE2_ADDRESS, + TEST_SENDER_ADDRESS, + TEST_SENDER_KEY, +} from '../src/fixtures'; + +// ethers v6 `WebSocketProvider` is functionally equivalent to JsonRpcProvider +// for read RPCs, but `provider.on('block')` / `contract.on('Event')` use +// `eth_subscribe('newHeads' | 'logs')` instead of the HTTP filter trio +// (eth_newFilter + eth_getFilterChanges polling). Thor's pedro/eth_eq_json_rpc +// branch handles the WS upgrade on the same /rpc path (cmd/thor/httpserver/api_server.go:161) +// and supports newHeads / logs subscriptions (rpc/ws/conn.go:183-200). +describe('WebSocketProvider — eth_subscribe (newHeads / logs)', () => { + it('getChainId works over a WebSocket transport', async () => { + const wsProvider = makeWsProvider(); + try { + const httpChainId = (await makeProvider().getNetwork()).chainId; + const wsChainId = (await wsProvider.getNetwork()).chainId; + expect(wsChainId).to.equal(httpChainId); + } finally { + await wsProvider.destroy(); + } + }); + + it('provider.on("block") receives a notification over eth_subscribe', async function () { + this.timeout(60_000); + const wsProvider = makeWsProvider(); + try { + const seen = new Promise((resolve) => { + wsProvider.on('block', (n: number) => resolve(n)); + }); + const observed = await seen; + expect(observed).to.be.a('number').and.greaterThan(0); + } finally { + wsProvider.removeAllListeners(); + await wsProvider.destroy(); + } + }); + + it('contract.on("Set") receives a notification over eth_subscribe(logs)', async function () { + this.timeout(60_000); + // We need a deployed contract; deploy it via the HTTP provider so the WS + // path only carries the subscription traffic we're actually testing. + const httpProvider = makeProvider(); + const wallet: Wallet = makeWallet(TEST_SENDER_KEY, httpProvider); + const artifact = loadStorageArtifact(); + const factory = new ContractFactory(artifact.abi, artifact.bytecode, wallet); + const deployed = await factory.deploy(); + await deployed.waitForDeployment(); + const address = await deployed.getAddress(); + + const wsProvider = makeWsProvider(); + try { + const contract = new Contract(address, artifact.abi, wsProvider); + const seen = new Promise<{ who: string; value: bigint }>((resolve) => { + contract.on('Set', (who: string, value: bigint) => { + resolve({ who, value }); + }); + }); + + // Send the Set tx through the HTTP wallet — the WS contract handle is + // listen-only. ethers' `contract.connect(httpWallet)` clones with a writer. + const writableContract = contract.connect(wallet) as Contract; + const tx = await writableContract.set(4242n); + await tx.wait(); + + const ev = await seen; + expect(ev.who.toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + expect(ev.value).to.equal(4242n); + } finally { + await wsProvider.destroy(); + } + }); + + it('provider.on("pending") receives a tx-hash notification over eth_subscribe(newPendingTransactions)', async function () { + this.timeout(60_000); + // ethers v6 maps the high-level 'pending' event to + // eth_subscribe('newPendingTransactions'). Thor pushes the tx hash for + // every executable TypeEthDynamicFee tx that enters the pool + // (rpc/ws/subscriptions.go:118), so an EIP-1559 transfer from the HTTP + // wallet must produce a callback on the WS provider before the receipt + // lands. + const wsProvider = makeWsProvider(); + const httpProvider = makeProvider(); + const wallet = makeWallet(TEST_SENDER_KEY, httpProvider); + + try { + const seen = new Promise((resolve) => { + wsProvider.on('pending', (hash: string) => resolve(hash)); + }); + + const tx = await wallet.sendTransaction({ + to: NODE2_ADDRESS, + value: 1n, + type: 2, + }); + + const observed = await seen; + expect(observed).to.match(/^0x[0-9a-fA-F]{64}$/); + expect(observed.toLowerCase()).to.equal(tx.hash.toLowerCase()); + } finally { + wsProvider.removeAllListeners(); + await wsProvider.destroy(); + } + }); + + it('provider.destroy() closes the websocket cleanly', async () => { + const wsProvider = makeWsProvider(); + // One successful call to confirm the connection is live... + const bn1 = await wsProvider.getBlockNumber(); + expect(bn1).to.be.greaterThan(0); + + // ethers v6 exposes the underlying socket via `websocket`; we capture its + // pre-destroy readyState (1 = OPEN) and then check that destroy moves it + // to CLOSING (2) or CLOSED (3). Subsequent RPC behavior is timing-dependent + // (ethers may resolve in-flight requests or reject immediately), so we + // anchor on the socket-level state transition instead. + const ws = (wsProvider as unknown as { websocket: { readyState: number } }).websocket; + expect(ws.readyState, 'OPEN before destroy').to.equal(1); + + await wsProvider.destroy(); + + // Give the underlying close handshake a tick to settle. + await new Promise((r) => setTimeout(r, 100)); + expect(ws.readyState, 'CLOSING (2) or CLOSED (3) after destroy').to.be.oneOf([2, 3]); + }); +}); diff --git a/tests/eth_rpc/ethersjs/tsconfig.json b/tests/eth_rpc/ethersjs/tsconfig.json new file mode 100644 index 0000000..5af06dc --- /dev/null +++ b/tests/eth_rpc/ethersjs/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "node", + "ignoreDeprecations": "5.0", + "esModuleInterop": true, + "resolveJsonModule": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": false, + "sourceMap": true, + "outDir": "dist", + "types": ["node", "mocha"] + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/tests/go.mod b/tests/go.mod index dd211bb..412f5e4 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -4,6 +4,8 @@ go 1.26.1 require ( github.com/ethereum/go-ethereum v1.8.14 + github.com/gorilla/websocket v1.4.1 + github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/stretchr/testify v1.11.1 github.com/vechain/thor/v2 v2.4.4-0.20260327111901-302778878883 ) @@ -20,7 +22,6 @@ require ( github.com/go-stack/stack v1.7.0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/gorilla/websocket v1.4.1 // indirect github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad // indirect github.com/holiman/uint256 v1.2.4 // indirect github.com/huin/goupnp v0.0.0-20171109214107-dceda08e705b // indirect @@ -36,7 +37,6 @@ require ( github.com/prometheus/procfs v0.12.0 // indirect github.com/qianbin/directcache v0.9.7 // indirect github.com/qianbin/drlp v0.0.0-20240102101024-e0e02518b5f9 // indirect - github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect github.com/vechain/go-ecvrf v0.0.0-20251211112124-5d5a3ef70fc9 // indirect golang.org/x/crypto v0.49.0 // indirect From 076a337b50ea23f2a7c4713d47e9dcf255ce4e6a Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Thu, 18 Jun 2026 15:06:43 +0800 Subject: [PATCH 07/14] Delete src/globalSetup.ts. Remove TestWsSubscribeSyncingRejected test case. --- .../eth_rpc_schema/eth_rpc_ws_schema_test.go | 16 --- tests/eth_rpc/ethersjs/.mocharc.cjs | 2 +- tests/eth_rpc/ethersjs/README.md | 41 +++--- tests/eth_rpc/ethersjs/package.json | 2 - tests/eth_rpc/ethersjs/src/fixtures.ts | 14 ++- tests/eth_rpc/ethersjs/src/globalSetup.ts | 119 ------------------ 6 files changed, 30 insertions(+), 164 deletions(-) delete mode 100644 tests/eth_rpc/ethersjs/src/globalSetup.ts diff --git a/tests/eth_rpc/eth_rpc_schema/eth_rpc_ws_schema_test.go b/tests/eth_rpc/eth_rpc_schema/eth_rpc_ws_schema_test.go index dd9d8d2..1e7defb 100644 --- a/tests/eth_rpc/eth_rpc_schema/eth_rpc_ws_schema_test.go +++ b/tests/eth_rpc/eth_rpc_schema/eth_rpc_ws_schema_test.go @@ -146,22 +146,6 @@ func TestWsSubscribeNewPendingTransactions(t *testing.T) { validateResult(t, "eth_unsubscribe", unsubRaw) } -// TestWsSubscribeSyncingRejected pins down that Thor's eth_subscribe rejects -// the 'syncing' subtype with InvalidParams (-32602). Standard go-ethereum -// nodes accept 'syncing'; if Thor catches up, flip this test to a success path. -// Reference: rpc/ws/conn.go:206 ('unsupported subscription type ...'). -func TestWsSubscribeSyncingRejected(t *testing.T) { - wc := wsDial(t) - - _, err := wsCall(t, wc, 1, "eth_subscribe", "syncing") - require.Error(t, err, "expected eth_subscribe('syncing') to be rejected") - - var rpcErr *jsonRPCError - require.ErrorAs(t, err, &rpcErr, "error must be a jsonRPCError") - assert.Equal(t, jsonRPCInvalidParams, rpcErr.Code, "expected InvalidParams (-32602)") - assert.Contains(t, strings.ToLower(rpcErr.Message), "unsupported subscription type") -} - // broadcastEthTx signs and submits an EIP-1559 transaction from helper.TestSenderKey. // Returns the tx hash from eth_sendRawTransaction. Used by the logs and pending // subscription tests to trigger a server-side notification. diff --git a/tests/eth_rpc/ethersjs/.mocharc.cjs b/tests/eth_rpc/ethersjs/.mocharc.cjs index bb480f5..693e14c 100644 --- a/tests/eth_rpc/ethersjs/.mocharc.cjs +++ b/tests/eth_rpc/ethersjs/.mocharc.cjs @@ -1,5 +1,5 @@ module.exports = { - require: ['ts-node/register', 'src/globalSetup.ts'], + require: ['ts-node/register'], extensions: ['ts'], spec: ['test/**/*.test.ts'], timeout: 120000, diff --git a/tests/eth_rpc/ethersjs/README.md b/tests/eth_rpc/ethersjs/README.md index 8f04821..157efa7 100644 --- a/tests/eth_rpc/ethersjs/README.md +++ b/tests/eth_rpc/ethersjs/README.md @@ -25,42 +25,33 @@ catches up — flip the assertion when that happens. ## Run +The suite runs as part of `go test ./...` via the Go wrapper in +`ethersjs_test.go`. The wrapper calls `helper.RunTestMain` to start the network +(or reuse one when `NODE_URL` is exported), then invokes `npx mocha` with +`NODE_URL` injected into the child env. + ```sh -npm install -npm test +go test ./tests/eth_rpc/ethersjs/... ``` -`pretest` rebuilds `/tmp/interstellar-network` from the workspace; the global -fixture in `src/globalSetup.ts` spawns it, reads the JSON ready-line from -stdout, and SIGTERMs it on teardown. - -To target an externally-managed node, set `NODE_URL`: +To run mocha directly (e.g. while iterating on a single test file), export +`NODE_URL` yourself against an already-running node: ```sh +cd tests/eth_rpc/ethersjs NODE_URL=http://127.0.0.1:8131 npm test ``` -## Can Mocha call `helper.RunTestMain`? - -No — `helper.RunTestMain` is a Go function and Mocha is JavaScript. The -fixture in `src/globalSetup.ts` mirrors the same protocol the Go helper uses: - -| `helper.RunTestMain` (Go) | `mochaGlobalSetup` (TS) | -| -------------------------------------------------- | --------------------------------------------- | -| Honors `NODE_URL` env var | same | -| Spawns `/tmp/interstellar-network start` | same | -| Line-scans stdout for `{"nodes":[...],...}` JSON | same | -| 15-minute startup timeout (first-run thor compile) | same | -| `defer stop()` → SIGTERM the child | `mochaGlobalTeardown` → SIGTERM + await exit | - -So the JS fixture is a one-to-one port of the Go helper. +There is no JS-side network spawn anymore — `src/fixtures.ts:getNodeUrl()` +just reads `process.env.NODE_URL` and throws if it is missing. ## Thor branch requirement Thor's Ethereum-compat RPC (`POST /rpc`) ships on the `pedro/eth_eq_json_rpc` branch. The default `evm-upgrades` branch wired into `network/setup/network.go` -does not expose it. `globalSetup.ts` sets `THOR_BRANCH=pedro/eth_eq_json_rpc` -for the spawned binary unless the caller has already exported it. +does not expose it. `ethersjs_test.go` sets `THOR_BRANCH=pedro/eth_eq_json_rpc` +in `TestMain` before calling `helper.RunTestMain`, so the network the Go +wrapper builds uses the right branch. ## Regenerating the contract artifact @@ -74,8 +65,8 @@ npm run compile:contracts ## Layout ```text -src/globalSetup.ts # spawns network binary, exposes node URL via env -src/fixtures.ts # provider/wallet factories + pre-funded test keys +src/fixtures.ts # provider/wallet factories + pre-funded test keys, + # reads NODE_URL from env (set by the Go wrapper) contracts/Storage.sol, Storage.json scripts/compile.cjs # one-shot solc → Storage.json test/provider.test.ts # 16 tests — block/tx/log lookups, getFeeData, diff --git a/tests/eth_rpc/ethersjs/package.json b/tests/eth_rpc/ethersjs/package.json index 6370c32..23d81ec 100644 --- a/tests/eth_rpc/ethersjs/package.json +++ b/tests/eth_rpc/ethersjs/package.json @@ -4,9 +4,7 @@ "version": "0.0.0", "description": "ethers.js v6 compatibility tests against Thor's Ethereum-compatible RPC.", "scripts": { - "build:network": "cd ../../.. && go build -o /tmp/interstellar-network github.com/vechain/interstellar-e2e/network", "compile:contracts": "node scripts/compile.cjs", - "pretest": "npm run build:network", "test": "mocha" }, "devDependencies": { diff --git a/tests/eth_rpc/ethersjs/src/fixtures.ts b/tests/eth_rpc/ethersjs/src/fixtures.ts index 0113d48..a69f273 100644 --- a/tests/eth_rpc/ethersjs/src/fixtures.ts +++ b/tests/eth_rpc/ethersjs/src/fixtures.ts @@ -1,7 +1,19 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { ethers, JsonRpcProvider, Wallet, WebSocketProvider } from 'ethers'; -import { getNodeUrl } from './globalSetup'; + +// NODE_URL is exported by the Go wrapper (tests/eth_rpc/ethersjs/ethersjs_test.go) +// which manages the network lifecycle via helper.RunTestMain. Running `npx mocha` +// or `npm test` directly requires the caller to export NODE_URL themselves. +export function getNodeUrl(): string { + const url = process.env.NODE_URL; + if (!url) { + throw new Error( + 'NODE_URL not set — run the suite via `go test` (which starts the network) or export NODE_URL manually', + ); + } + return url; +} // Pre-funded master accounts from LocalThreeNodesNetwork genesis. // Mirrors tests/helper/client.go:16-21. diff --git a/tests/eth_rpc/ethersjs/src/globalSetup.ts b/tests/eth_rpc/ethersjs/src/globalSetup.ts deleted file mode 100644 index 46b17f7..0000000 --- a/tests/eth_rpc/ethersjs/src/globalSetup.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { spawn, ChildProcess } from 'node:child_process'; -import { Readable } from 'node:stream'; -import * as readline from 'node:readline'; - -const NETWORK_BINARY = '/tmp/interstellar-network'; -const STARTUP_TIMEOUT_MS = 15 * 60 * 1000; -const RUNTIME_URL_ENV = 'INTERSTELLAR_RUNTIME_NODE_URL'; -const RUNTIME_P2P_ENV = 'INTERSTELLAR_RUNTIME_P2P_PORTS'; - -let networkProc: ChildProcess | null = null; - -interface ReadyLine { - nodes: string[]; - p2pPorts: number[]; -} - -function readReadyLine(proc: ChildProcess): Promise { - return new Promise((resolve, reject) => { - const stdout = proc.stdout; - if (!stdout) { - reject(new Error('network process has no stdout pipe')); - return; - } - const rl = readline.createInterface({ input: stdout as Readable }); - let timer: NodeJS.Timeout; - - const cleanup = () => { - clearTimeout(timer); - rl.close(); - }; - - timer = setTimeout(() => { - cleanup(); - reject(new Error(`network did not emit ready line within ${STARTUP_TIMEOUT_MS}ms`)); - }, STARTUP_TIMEOUT_MS); - - rl.on('line', (line) => { - try { - const parsed = JSON.parse(line); - if (Array.isArray(parsed.nodes) && parsed.nodes.length > 0) { - cleanup(); - resolve(parsed as ReadyLine); - return; - } - } catch { - // Non-JSON stdout line (e.g. git output from ThorBuilder) — forward to stderr - // so it remains visible without blocking the ready-line scan. - process.stderr.write(`[network] ${line}\n`); - } - }); - - proc.once('exit', (code, signal) => { - cleanup(); - reject(new Error(`network exited before ready (code=${code}, signal=${signal})`)); - }); - - proc.once('error', (err) => { - cleanup(); - reject(err); - }); - }); -} - -export async function mochaGlobalSetup(): Promise { - if (process.env.NODE_URL) { - process.env[RUNTIME_URL_ENV] = process.env.NODE_URL; - console.log(`[ethersjs] using external NODE_URL: ${process.env.NODE_URL}`); - return; - } - - // The Ethereum-compatible RPC endpoint (POST /rpc) lives on the thor - // `pedro/eth_eq_json_rpc` branch. The default `evm-upgrades` branch built by - // network/setup/network.go does not expose it, so set THOR_BRANCH here unless - // the caller explicitly overrode it. Same trick the schema TestMain used - // before commit 416eed1 — kept here so ethers tests are self-contained. - const childEnv = { ...process.env }; - if (!childEnv.THOR_BRANCH) { - childEnv.THOR_BRANCH = 'pedro/eth_eq_json_rpc'; - } - - console.log( - `[ethersjs] spawning ${NETWORK_BINARY} start (THOR_BRANCH=${childEnv.THOR_BRANCH})`, - ); - networkProc = spawn(NETWORK_BINARY, ['start'], { - stdio: ['ignore', 'pipe', 'inherit'], - env: childEnv, - }); - - const ready = await readReadyLine(networkProc); - process.env[RUNTIME_URL_ENV] = ready.nodes[0]; - process.env[RUNTIME_P2P_ENV] = ready.p2pPorts.join(','); - console.log( - `[ethersjs] network ready — node: ${ready.nodes[0]}, p2p: ${ready.p2pPorts.join(',')}`, - ); -} - -export async function mochaGlobalTeardown(): Promise { - if (!networkProc) return; - console.log('[ethersjs] stopping network'); - await new Promise((resolve) => { - networkProc!.once('exit', () => resolve()); - networkProc!.kill('SIGTERM'); - }); - networkProc = null; -} - -export function getNodeUrl(): string { - const url = process.env[RUNTIME_URL_ENV]; - if (!url) { - throw new Error('node URL not initialized — globalSetup must run first'); - } - return url; -} - -export function getP2PPorts(): number[] { - const raw = process.env[RUNTIME_P2P_ENV]; - if (!raw) return []; - return raw.split(',').map((s) => Number(s)); -} From 28649702302ca8cd13334e3f88c6387dae72f616 Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Thu, 18 Jun 2026 17:30:02 +0800 Subject: [PATCH 08/14] Update tests --- Makefile | 2 +- tests/eth_rpc/eth_rpc_schema/main_test.go | 7 ------- tests/eth_rpc/ethersjs/README.md | 8 -------- tests/eth_rpc/ethersjs/ethersjs_test.go | 1 - tests/eth_rpc/ethersjs/test/websocket.test.ts | 6 ------ 5 files changed, 1 insertion(+), 23 deletions(-) diff --git a/Makefile b/Makefile index 837e1d4..0dc8fa0 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ test: build-network ethersjs-deps @/tmp/interstellar-network start & \ NODE_URL=$$(/tmp/interstellar-network node-url) && \ NODE_P2P_PORT=$$(/tmp/interstellar-network node-p2p-port) && \ - cd tests && NODE_URL=$$NODE_URL NODE_P2P_PORT=$$NODE_P2P_PORT go test -v -count=1 -timeout 20m ./... ; \ + cd tests && NODE_URL=$$NODE_URL NODE_P2P_PORT=$$NODE_P2P_PORT go test -v -count=1 -p 1 -timeout 20m ./... ; \ CODE=$$? ; \ /tmp/interstellar-network stop 2>/dev/null || true ; \ exit $$CODE diff --git a/tests/eth_rpc/eth_rpc_schema/main_test.go b/tests/eth_rpc/eth_rpc_schema/main_test.go index ad596f8..a2aabcf 100644 --- a/tests/eth_rpc/eth_rpc_schema/main_test.go +++ b/tests/eth_rpc/eth_rpc_schema/main_test.go @@ -10,12 +10,5 @@ import ( var nodeURL string func TestMain(m *testing.M) { - // The Ethereum-compatible JSON-RPC (POST/WS /rpc) lives on the thor - // pedro/eth_eq_json_rpc branch; the default evm-upgrades branch built by - // network/setup/network.go does not expose it. Set it unless the caller - // already did. Same approach as tests/eth_rpc/ethersjs/ethersjs_test.go. - if os.Getenv("THOR_BRANCH") == "" { - os.Setenv("THOR_BRANCH", "pedro/eth_eq_json_rpc") - } os.Exit(helper.RunTestMain(m, &nodeURL, nil)) } diff --git a/tests/eth_rpc/ethersjs/README.md b/tests/eth_rpc/ethersjs/README.md index 157efa7..2d63079 100644 --- a/tests/eth_rpc/ethersjs/README.md +++ b/tests/eth_rpc/ethersjs/README.md @@ -45,14 +45,6 @@ NODE_URL=http://127.0.0.1:8131 npm test There is no JS-side network spawn anymore — `src/fixtures.ts:getNodeUrl()` just reads `process.env.NODE_URL` and throws if it is missing. -## Thor branch requirement - -Thor's Ethereum-compat RPC (`POST /rpc`) ships on the `pedro/eth_eq_json_rpc` -branch. The default `evm-upgrades` branch wired into `network/setup/network.go` -does not expose it. `ethersjs_test.go` sets `THOR_BRANCH=pedro/eth_eq_json_rpc` -in `TestMain` before calling `helper.RunTestMain`, so the network the Go -wrapper builds uses the right branch. - ## Regenerating the contract artifact `contracts/Storage.json` is checked in. To rebuild it after changing diff --git a/tests/eth_rpc/ethersjs/ethersjs_test.go b/tests/eth_rpc/ethersjs/ethersjs_test.go index c685b05..e21db1a 100644 --- a/tests/eth_rpc/ethersjs/ethersjs_test.go +++ b/tests/eth_rpc/ethersjs/ethersjs_test.go @@ -21,7 +21,6 @@ import ( var nodeURL string func TestMain(m *testing.M) { - os.Setenv("THOR_BRANCH", "pedro/eth_eq_json_rpc") os.Exit(helper.RunTestMain(m, &nodeURL, nil)) } diff --git a/tests/eth_rpc/ethersjs/test/websocket.test.ts b/tests/eth_rpc/ethersjs/test/websocket.test.ts index 152bc9a..378fdaf 100644 --- a/tests/eth_rpc/ethersjs/test/websocket.test.ts +++ b/tests/eth_rpc/ethersjs/test/websocket.test.ts @@ -15,12 +15,6 @@ import { TEST_SENDER_KEY, } from '../src/fixtures'; -// ethers v6 `WebSocketProvider` is functionally equivalent to JsonRpcProvider -// for read RPCs, but `provider.on('block')` / `contract.on('Event')` use -// `eth_subscribe('newHeads' | 'logs')` instead of the HTTP filter trio -// (eth_newFilter + eth_getFilterChanges polling). Thor's pedro/eth_eq_json_rpc -// branch handles the WS upgrade on the same /rpc path (cmd/thor/httpserver/api_server.go:161) -// and supports newHeads / logs subscriptions (rpc/ws/conn.go:183-200). describe('WebSocketProvider — eth_subscribe (newHeads / logs)', () => { it('getChainId works over a WebSocket transport', async () => { const wsProvider = makeWsProvider(); From 56e585097adcfa09ce8f26855b1b18d1159af2b3 Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Mon, 29 Jun 2026 15:42:41 +0800 Subject: [PATCH 09/14] test(eth_rpc): add viem suite and expand RPC/WS compatibility coverage Add a viem v2 compatibility suite mirroring ethersjs (provider/wallet/ contract/events/websocket + rpc-extra) and broaden the ethersjs and eth_rpc_schema suites. All suites follow the same three-category scheme: implemented methods asserted fully, unimplemented methods skipped, and divergences (eth_feeHistory rewardPercentiles) skipped until Thor aligns. - viem: new suite + Go wrapper, wired into Makefile (viem-deps) - eth_rpc_schema: schemas + tests for chain stubs, uncles, filter family, WS syncing subscription; skip table for unimplemented methods - ethersjs: block/uncle/index methods, filter family, net/web3 metadata, ENS skips, WS once/syncing --- Makefile | 10 +- .../eth_rpc_schema_extra_test.go | 304 ++ .../eth_rpc_schema/eth_rpc_ws_schema_test.go | 13 +- .../eth_rpc_schema/schemas/eth_accounts.json | 8 + .../eth_rpc_schema/schemas/eth_coinbase.json | 7 + .../schemas/eth_getFilterChanges.json | 13 + .../schemas/eth_getFilterLogs.json | 8 + .../eth_getUncleByBlockHashAndIndex.json | 10 + .../eth_getUncleByBlockNumberAndIndex.json | 10 + .../schemas/eth_getUncleCountByBlockHash.json | 7 + .../eth_getUncleCountByBlockNumber.json | 7 + .../eth_rpc_schema/schemas/eth_hashrate.json | 7 + .../eth_rpc_schema/schemas/eth_mining.json | 7 + .../schemas/eth_newBlockFilter.json | 7 + .../eth_rpc_schema/schemas/eth_newFilter.json | 7 + .../eth_newPendingTransactionFilter.json | 7 + .../schemas/eth_subscription_syncing.json | 25 + .../schemas/eth_uninstallFilter.json | 7 + tests/eth_rpc/ethersjs/test/events.test.ts | 115 + tests/eth_rpc/ethersjs/test/provider.test.ts | 308 ++- tests/eth_rpc/ethersjs/test/wallet.test.ts | 21 + tests/eth_rpc/ethersjs/test/websocket.test.ts | 81 + tests/eth_rpc/viem/.gitignore | 3 + tests/eth_rpc/viem/.mocharc.cjs | 8 + .../viem/contracts/Create2Factory.json | 43 + .../eth_rpc/viem/contracts/Create2Factory.sol | 16 + tests/eth_rpc/viem/contracts/Storage.json | 140 + tests/eth_rpc/viem/contracts/Storage.sol | 41 + tests/eth_rpc/viem/package-lock.json | 1474 ++++++++++ tests/eth_rpc/viem/package.json | 19 + tests/eth_rpc/viem/src/fixtures.ts | 138 + tests/eth_rpc/viem/test/contract.test.ts | 206 ++ tests/eth_rpc/viem/test/events.test.ts | 216 ++ tests/eth_rpc/viem/test/provider.test.ts | 295 ++ tests/eth_rpc/viem/test/rpc-extra.test.ts | 248 ++ tests/eth_rpc/viem/test/wallet.test.ts | 224 ++ tests/eth_rpc/viem/test/websocket.test.ts | 144 + tests/eth_rpc/viem/tsconfig.json | 21 + tests/eth_rpc/viem/viem_test.go | 38 + tests/eth_rpc/web3js/.gitignore | 3 + tests/eth_rpc/web3js/.mocharc.cjs | 8 + tests/eth_rpc/web3js/README.md | 48 + .../web3js/contracts/Create2Factory.json | 43 + .../web3js/contracts/Create2Factory.sol | 16 + tests/eth_rpc/web3js/contracts/Storage.json | 140 + tests/eth_rpc/web3js/contracts/Storage.sol | 41 + tests/eth_rpc/web3js/package-lock.json | 2444 +++++++++++++++++ tests/eth_rpc/web3js/package.json | 21 + tests/eth_rpc/web3js/scripts/compile.cjs | 49 + tests/eth_rpc/web3js/src/fixtures.ts | 206 ++ tests/eth_rpc/web3js/test/contract.test.ts | 210 ++ tests/eth_rpc/web3js/test/events.test.ts | 189 ++ tests/eth_rpc/web3js/test/provider.test.ts | 357 +++ tests/eth_rpc/web3js/test/rpc-extra.test.ts | 244 ++ tests/eth_rpc/web3js/test/wallet.test.ts | 171 ++ tests/eth_rpc/web3js/test/websocket.test.ts | 134 + tests/eth_rpc/web3js/tsconfig.json | 21 + tests/eth_rpc/web3js/web3js_test.go | 38 + 58 files changed, 8635 insertions(+), 11 deletions(-) create mode 100644 tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_extra_test.go create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_accounts.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_coinbase.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getFilterChanges.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getFilterLogs.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getUncleByBlockHashAndIndex.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getUncleByBlockNumberAndIndex.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getUncleCountByBlockHash.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_getUncleCountByBlockNumber.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_hashrate.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_mining.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_newBlockFilter.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_newFilter.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_newPendingTransactionFilter.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_subscription_syncing.json create mode 100644 tests/eth_rpc/eth_rpc_schema/schemas/eth_uninstallFilter.json create mode 100644 tests/eth_rpc/viem/.gitignore create mode 100644 tests/eth_rpc/viem/.mocharc.cjs create mode 100644 tests/eth_rpc/viem/contracts/Create2Factory.json create mode 100644 tests/eth_rpc/viem/contracts/Create2Factory.sol create mode 100644 tests/eth_rpc/viem/contracts/Storage.json create mode 100644 tests/eth_rpc/viem/contracts/Storage.sol create mode 100644 tests/eth_rpc/viem/package-lock.json create mode 100644 tests/eth_rpc/viem/package.json create mode 100644 tests/eth_rpc/viem/src/fixtures.ts create mode 100644 tests/eth_rpc/viem/test/contract.test.ts create mode 100644 tests/eth_rpc/viem/test/events.test.ts create mode 100644 tests/eth_rpc/viem/test/provider.test.ts create mode 100644 tests/eth_rpc/viem/test/rpc-extra.test.ts create mode 100644 tests/eth_rpc/viem/test/wallet.test.ts create mode 100644 tests/eth_rpc/viem/test/websocket.test.ts create mode 100644 tests/eth_rpc/viem/tsconfig.json create mode 100644 tests/eth_rpc/viem/viem_test.go create mode 100644 tests/eth_rpc/web3js/.gitignore create mode 100644 tests/eth_rpc/web3js/.mocharc.cjs create mode 100644 tests/eth_rpc/web3js/README.md create mode 100644 tests/eth_rpc/web3js/contracts/Create2Factory.json create mode 100644 tests/eth_rpc/web3js/contracts/Create2Factory.sol create mode 100644 tests/eth_rpc/web3js/contracts/Storage.json create mode 100644 tests/eth_rpc/web3js/contracts/Storage.sol create mode 100644 tests/eth_rpc/web3js/package-lock.json create mode 100644 tests/eth_rpc/web3js/package.json create mode 100644 tests/eth_rpc/web3js/scripts/compile.cjs create mode 100644 tests/eth_rpc/web3js/src/fixtures.ts create mode 100644 tests/eth_rpc/web3js/test/contract.test.ts create mode 100644 tests/eth_rpc/web3js/test/events.test.ts create mode 100644 tests/eth_rpc/web3js/test/provider.test.ts create mode 100644 tests/eth_rpc/web3js/test/rpc-extra.test.ts create mode 100644 tests/eth_rpc/web3js/test/wallet.test.ts create mode 100644 tests/eth_rpc/web3js/test/websocket.test.ts create mode 100644 tests/eth_rpc/web3js/tsconfig.json create mode 100644 tests/eth_rpc/web3js/web3js_test.go diff --git a/Makefile b/Makefile index 0dc8fa0..109999b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build-network ethersjs-deps test clean stop status lint +.PHONY: build-network ethersjs-deps web3js-deps viem-deps test clean stop status lint build-network: cd network && go build -o /tmp/interstellar-network github.com/vechain/interstellar-e2e/network && cd .. @@ -6,7 +6,13 @@ build-network: ethersjs-deps: @[ -d tests/eth_rpc/ethersjs/node_modules ] || (cd tests/eth_rpc/ethersjs && npm ci) -test: build-network ethersjs-deps +web3js-deps: + @[ -d tests/eth_rpc/web3js/node_modules ] || (cd tests/eth_rpc/web3js && npm ci) + +viem-deps: + @[ -d tests/eth_rpc/viem/node_modules ] || (cd tests/eth_rpc/viem && npm ci) + +test: build-network ethersjs-deps web3js-deps viem-deps @/tmp/interstellar-network start & \ NODE_URL=$$(/tmp/interstellar-network node-url) && \ NODE_P2P_PORT=$$(/tmp/interstellar-network node-p2p-port) && \ diff --git a/tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_extra_test.go b/tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_extra_test.go new file mode 100644 index 0000000..e47100f --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_extra_test.go @@ -0,0 +1,304 @@ +// Additional schema-conformance coverage, organized by the same three-category +// scheme used in the ethersjs / web3js suites: +// +// Cat-1 Implemented on Thor → full request + JSON-Schema validation of the +// result (chain stubs, uncles, the filter family, the WS 'syncing' +// subscription). +// Cat-2 Not registered by thor's dispatcher → the call is attempted and the +// test SKIPS on a "method not found" surface (auto-activates if Thor +// ever ships it). +// Cat-3 Implemented but divergent from go-ethereum → a geth-parity assertion +// that is LEFT FAILING for manual review. +// +// Authoritative method set comes from thor's rpc/*/handler.go Mount() funcs on +// branch pedro/eth_eq_json_rpc. + +package ethrpcschema + +import ( + "context" + "encoding/json" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// ----------------------------------------------------------------------------- +// Cat-1 — chain / node metadata stubs (implemented, PoA-constant values) +// ----------------------------------------------------------------------------- + +// TestEthCoinbase asserts eth_coinbase returns an address (zero on PoA). +func TestEthCoinbase(t *testing.T) { + result := rpcCallAndValidate(t, "eth_coinbase") + var addr string + require.NoError(t, json.Unmarshal(result, &addr), "unmarshal coinbase") + assert.Equal(t, common.Address{}.Hex(), common.HexToAddress(addr).Hex(), + "Thor PoA coinbase must be the zero address") +} + +// TestEthMining asserts eth_mining returns a boolean (false on PoA). +func TestEthMining(t *testing.T) { + result := rpcCallAndValidate(t, "eth_mining") + var mining bool + require.NoError(t, json.Unmarshal(result, &mining), "unmarshal mining") + assert.False(t, mining, "Thor PoA must report mining=false") +} + +// TestEthHashrate asserts eth_hashrate returns a QUANTITY (0x0 on PoA). +func TestEthHashrate(t *testing.T) { + result := rpcCallAndValidate(t, "eth_hashrate") + assert.Equal(t, 0, hexQuantityToInt(t, result).Sign(), "Thor PoA hashrate must be 0") +} + +// TestEthAccounts asserts eth_accounts returns an (empty) address array — Thor +// holds no node-side keystore. +func TestEthAccounts(t *testing.T) { + result := rpcCallAndValidate(t, "eth_accounts") + var accounts []string + require.NoError(t, json.Unmarshal(result, &accounts), "unmarshal accounts") + assert.Empty(t, accounts, "Thor exposes no unlocked node accounts") +} + +// ----------------------------------------------------------------------------- +// Cat-1 — uncles (VeChain has none; count is 0x0, by-index is null) +// ----------------------------------------------------------------------------- + +// TestEthGetUncleCountByBlockNumber asserts the uncle count at latest is 0x0. +func TestEthGetUncleCountByBlockNumber(t *testing.T) { + result := rpcCallAndValidate(t, "eth_getUncleCountByBlockNumber", "latest") + assert.Equal(t, 0, hexQuantityToInt(t, result).Sign(), "VeChain has no uncles") +} + +// TestEthGetUncleCountByBlockHash asserts the uncle count for the latest-block +// hash is 0x0. +func TestEthGetUncleCountByBlockHash(t *testing.T) { + hash := fetchLatestBlockHash(t) + result := rpcCallAndValidate(t, "eth_getUncleCountByBlockHash", hash) + assert.Equal(t, 0, hexQuantityToInt(t, result).Sign(), "VeChain has no uncles") +} + +// TestEthGetUncleByBlockNumberAndIndex asserts the uncle at (latest, 0) is null. +func TestEthGetUncleByBlockNumberAndIndex(t *testing.T) { + result, err := rpcCall(t, "eth_getUncleByBlockNumberAndIndex", "latest", "0x0") + require.NoError(t, err, "eth_getUncleByBlockNumberAndIndex call") + assert.JSONEq(t, "null", string(result), "VeChain has no uncles") + validateResult(t, "eth_getUncleByBlockNumberAndIndex", result) +} + +// TestEthGetUncleByBlockHashAndIndex asserts the uncle at (latest hash, 0) is null. +func TestEthGetUncleByBlockHashAndIndex(t *testing.T) { + hash := fetchLatestBlockHash(t) + result, err := rpcCall(t, "eth_getUncleByBlockHashAndIndex", hash, "0x0") + require.NoError(t, err, "eth_getUncleByBlockHashAndIndex call") + assert.JSONEq(t, "null", string(result), "VeChain has no uncles") + validateResult(t, "eth_getUncleByBlockHashAndIndex", result) +} + +// ----------------------------------------------------------------------------- +// Cat-1 — filter family (newFilter / getFilterLogs / getFilterChanges / +// newBlockFilter / newPendingTransactionFilter / uninstallFilter) +// ----------------------------------------------------------------------------- + +// filterLogTopic and filterLogInitCode deploy a 43-byte init blob that LOG1's a +// fixed topic and returns empty runtime code — used to seed a log the filter +// family can match. Mirrors the LogOnDeploy blob in the WS logs test, with a +// distinct topic so concurrent subscriptions don't cross-match. +const ( + filterLogTopic = "0x3434343434343434343434343434343434343434343434343434343434343434" + filterLogInitCode = "7f3434343434343434343434343434343434343434343434343434343434343434" + + "60006000a160006000f3" +) + +// TestEthFilterLogLifecycle drives eth_newFilter → eth_getFilterLogs → +// eth_getFilterChanges → eth_uninstallFilter against a real LOG1 emission, and +// schema-validates each result. +func TestEthFilterLogLifecycle(t *testing.T) { + // Emit a LOG1 with our topic and wait for the receipt so the log is mined + // before the filter is installed. + broadcastEthTx(t, common.Hex2Bytes(filterLogInitCode), nil, 200_000) + + filterIDRaw := rpcCallAndValidate(t, "eth_newFilter", map[string]any{ + "fromBlock": "0x0", + "toBlock": "latest", + "topics": []any{filterLogTopic}, + }) + var filterID string + require.NoError(t, json.Unmarshal(filterIDRaw, &filterID), "unmarshal filter id") + + defer func() { + removedRaw, err := rpcCall(t, "eth_uninstallFilter", filterID) + require.NoError(t, err, "eth_uninstallFilter call") + validateResult(t, "eth_uninstallFilter", removedRaw) + var removed bool + require.NoError(t, json.Unmarshal(removedRaw, &removed), "unmarshal uninstall result") + assert.True(t, removed, "eth_uninstallFilter must return true for an active filter") + }() + + // eth_getFilterLogs returns ALL matching logs regardless of poll cursor. + var logs []map[string]any + deadline := time.Now().Add(30 * time.Second) + for time.Now().Before(deadline) { + raw := rpcCallAndValidate(t, "eth_getFilterLogs", filterID) + require.NoError(t, json.Unmarshal(raw, &logs), "unmarshal filter logs") + if len(logs) > 0 { + break + } + time.Sleep(500 * time.Millisecond) + } + require.NotEmpty(t, logs, "eth_getFilterLogs must return the emitted LOG1") + topics, ok := logs[0]["topics"].([]any) + require.True(t, ok, "log.topics must be an array") + require.NotEmpty(t, topics, "log must carry a topic") + assert.Equal(t, filterLogTopic, topics[0], "log.topics[0] must match the emitted topic") + + // eth_getFilterChanges shape-validates (may be empty depending on the poll + // cursor relative to the emission — we only assert the schema here). + rpcCallAndValidate(t, "eth_getFilterChanges", filterID) +} + +// TestEthNewBlockFilter installs a block filter, schema-validates the id and a +// subsequent getFilterChanges poll (an array of block hashes), then uninstalls. +func TestEthNewBlockFilter(t *testing.T) { + filterIDRaw := rpcCallAndValidate(t, "eth_newBlockFilter") + var filterID string + require.NoError(t, json.Unmarshal(filterIDRaw, &filterID), "unmarshal filter id") + + // Poll until at least one new block hash arrives (validates the non-empty + // hash-array shape), bounded so a stalled chain can't hang the test. + var hashes []string + deadline := time.Now().Add(30 * time.Second) + for time.Now().Before(deadline) { + raw := rpcCallAndValidate(t, "eth_getFilterChanges", filterID) + require.NoError(t, json.Unmarshal(raw, &hashes), "unmarshal block-hash changes") + if len(hashes) > 0 { + break + } + time.Sleep(500 * time.Millisecond) + } + require.NotEmpty(t, hashes, "eth_getFilterChanges on a block filter must yield block hashes") + require.Regexp(t, "^0x[0-9a-fA-F]{64}$", hashes[0], "block-filter change must be a 32-byte hash") + + removedRaw, err := rpcCall(t, "eth_uninstallFilter", filterID) + require.NoError(t, err, "eth_uninstallFilter call") + validateResult(t, "eth_uninstallFilter", removedRaw) +} + +// TestEthNewPendingTransactionFilter installs a pending-tx filter, schema- +// validates the id, then uninstalls. +func TestEthNewPendingTransactionFilter(t *testing.T) { + filterIDRaw := rpcCallAndValidate(t, "eth_newPendingTransactionFilter") + var filterID string + require.NoError(t, json.Unmarshal(filterIDRaw, &filterID), "unmarshal filter id") + + removedRaw, err := rpcCall(t, "eth_uninstallFilter", filterID) + require.NoError(t, err, "eth_uninstallFilter call") + validateResult(t, "eth_uninstallFilter", removedRaw) + var removed bool + require.NoError(t, json.Unmarshal(removedRaw, &removed), "unmarshal uninstall result") + assert.True(t, removed, "eth_uninstallFilter must return true for an active filter") +} + +// ----------------------------------------------------------------------------- +// Cat-1 (WS) — eth_subscribe('syncing') +// ----------------------------------------------------------------------------- + +// TestWsSubscribeSyncing validates the 'syncing' subscription Thor's +// rpc/ws/conn.go now implements (the old TestWsSubscribeSyncingRejected was +// removed once syncing shipped). On an already-synced local network runSyncing +// emits a single boolean `false` immediately; while syncing it would emit a +// {syncing,status} object — the schema accepts both. +func TestWsSubscribeSyncing(t *testing.T) { + wc := wsDial(t) + + subIDRaw := wsCallAndValidate(t, wc, 1, "eth_subscribe", "eth_subscribe", "syncing") + var subID string + require.NoError(t, json.Unmarshal(subIDRaw, &subID), "unmarshal subID") + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + notif, err := wsReadNotification(t, wc, ctx, subID) + require.NoError(t, err, "wait for syncing notification") + validateResult(t, "eth_subscription_syncing", notif) + + unsubRaw, err := wsCall(t, wc, 2, "eth_unsubscribe", subID) + require.NoError(t, err, "eth_unsubscribe") + validateResult(t, "eth_unsubscribe", unsubRaw) +} + +// ----------------------------------------------------------------------------- +// Cat-2 — standard eth_* methods thor does NOT register (skipped until shipped) +// ----------------------------------------------------------------------------- + +// TestUnimplementedMethods attempts each standard Ethereum method thor's +// dispatcher does not register and skips on a "method not found" surface. If +// Thor ever registers one, the success path (no error, non-null result) keeps +// the test honest; a non-"not found" error fails loudly. +func TestUnimplementedMethods(t *testing.T) { + const senderAddr = "0x61fF580B63D3845934610222245C116E013717ec" + const node2Addr = "0x327931085B4cCbCE0baABb5a5E1C678707C51d90" + zeroHash := "0x" + strings.Repeat("00", 32) + + cases := []struct { + method string + params []any + }{ + {"eth_getProof", []any{senderAddr, []any{}, "latest"}}, + {"eth_createAccessList", []any{map[string]any{"from": senderAddr, "to": node2Addr}, "latest"}}, + {"eth_protocolVersion", []any{}}, + {"eth_pendingTransactions", []any{}}, + {"eth_sign", []any{senderAddr, "0x68656c6c6f"}}, + {"eth_signTransaction", []any{map[string]any{"from": senderAddr, "to": node2Addr, "value": "0x1"}}}, + {"eth_getRawTransactionByHash", []any{zeroHash}}, + {"debug_traceTransaction", []any{zeroHash}}, + } + + for _, tc := range cases { + t.Run(tc.method, func(t *testing.T) { + result, err := rpcCall(t, tc.method, tc.params...) + if isMethodNotFound(err) { + t.Skipf("%s not implemented by Thor: %v", tc.method, err) + } + require.NoError(t, err, "%s errored for a non-\"not found\" reason", tc.method) + require.NotNil(t, result, "%s returned a nil result without an error", tc.method) + }) + } +} + +// ----------------------------------------------------------------------------- +// Cat-3 — divergences from go-ethereum (skipped until Thor aligns) +// ----------------------------------------------------------------------------- + +// TestEthFeeHistory_RewardPercentiles probes the rewardPercentiles form of +// eth_feeHistory. +// +// geth returns a per-block × per-percentile `reward` matrix when called with +// rewardPercentiles. Thor (rpc/fees/handler.go) currently rejects the percentile +// form — "reward percentiles are not yet supported" (code -32000) — so a fee +// estimator that requests percentiles can't use it. We SKIP on that documented +// gap; if Thor ever ships it, the call succeeds and the reward-matrix assertion +// keeps it honest. TestEthFeeHistory (no percentiles) covers the supported path. +func TestEthFeeHistory_RewardPercentiles(t *testing.T) { + result, err := rpcCall(t, "eth_feeHistory", "0x4", "latest", []float64{25, 50, 75}) + if isRewardPercentilesUnsupported(err) { + t.Skipf("eth_feeHistory rewardPercentiles not supported by Thor: %v", err) + } + require.NoError(t, err, "eth_feeHistory with rewardPercentiles") + var fh map[string]any + require.NoError(t, json.Unmarshal(result, &fh), "unmarshal feeHistory") + require.Contains(t, fh, "reward", + "feeHistory must include a per-block reward matrix when rewardPercentiles is requested") +} + +// isRewardPercentilesUnsupported reports whether err is Thor's documented +// rejection of the eth_feeHistory rewardPercentiles parameter. +func isRewardPercentilesUnsupported(err error) bool { + if err == nil { + return false + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "percentile") || strings.Contains(msg, "not yet supported") +} diff --git a/tests/eth_rpc/eth_rpc_schema/eth_rpc_ws_schema_test.go b/tests/eth_rpc/eth_rpc_schema/eth_rpc_ws_schema_test.go index 1e7defb..6a6d23f 100644 --- a/tests/eth_rpc/eth_rpc_schema/eth_rpc_ws_schema_test.go +++ b/tests/eth_rpc/eth_rpc_schema/eth_rpc_ws_schema_test.go @@ -13,10 +13,11 @@ // 5. Issues eth_unsubscribe and validates the boolean against // schemas/eth_unsubscribe.json. // -// The syncing subscription is documented as a rejection — Thor's switch in -// rpc/ws/conn.go:182-207 only implements newHeads/logs/newPendingTransactions; -// any other subtype returns InvalidParams (-32602). If Thor ever ships -// 'syncing' that test flips. +// Thor's switch in rpc/ws/conn.go implements newHeads/logs/ +// newPendingTransactions/syncing; any other subtype returns InvalidParams +// (-32602). The 'syncing' subscription is covered by TestWsSubscribeSyncing in +// eth_rpc_schema_extra_test.go (the old TestWsSubscribeSyncingRejected was +// removed once Thor shipped it). package ethrpcschema @@ -39,10 +40,6 @@ import ( "github.com/vechain/interstellar-e2e/tests/helper" ) -// jsonRPCInvalidParams is the JSON-RPC 2.0 code reserved for parameter errors. -// Thor's rpc/ws/conn.go:206 returns this for unsupported subscription subtypes. -const jsonRPCInvalidParams = -32602 - // TestWsSubscribeNewHeads validates that an eth_subscribe('newHeads') call // returns a hex subID, pushes a block-shaped notification on the next packed // block, and a subsequent eth_unsubscribe returns true. diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_accounts.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_accounts.json new file mode 100644 index 0000000..2a6d805 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_accounts.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_accounts.json", + "title": "eth_accounts result", + "description": "Addresses owned by the client. Thor holds no node-side keystore, so this is always an empty array.", + "type": "array", + "items": { "$ref": "_defs.json#/$defs/address" } +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_coinbase.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_coinbase.json new file mode 100644 index 0000000..422f213 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_coinbase.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_coinbase.json", + "title": "eth_coinbase result", + "description": "Client coinbase address. Thor (PoA) returns the zero address — it has no local mining-reward account (rpc/chain/handler.go:ethCoinbase).", + "$ref": "_defs.json#/$defs/address" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getFilterChanges.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getFilterChanges.json new file mode 100644 index 0000000..37851d7 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getFilterChanges.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getFilterChanges.json", + "title": "eth_getFilterChanges result", + "description": "Changes since the last poll. For a log filter: an array of log objects. For a block filter: an array of 32-byte block hashes. For a pending-tx filter: an array of 32-byte tx hashes. May be empty.", + "type": "array", + "items": { + "oneOf": [ + { "$ref": "_defs.json#/$defs/hash32" }, + { "$ref": "_defs.json#/$defs/log" } + ] + } +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getFilterLogs.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getFilterLogs.json new file mode 100644 index 0000000..67de17b --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getFilterLogs.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getFilterLogs.json", + "title": "eth_getFilterLogs result", + "description": "All logs matching a log filter (same shape as eth_getLogs).", + "type": "array", + "items": { "$ref": "_defs.json#/$defs/log" } +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getUncleByBlockHashAndIndex.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getUncleByBlockHashAndIndex.json new file mode 100644 index 0000000..9f90320 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getUncleByBlockHashAndIndex.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getUncleByBlockHashAndIndex.json", + "title": "eth_getUncleByBlockHashAndIndex result", + "description": "An uncle block header, or null. VeChain has no uncles, so Thor always returns null; an uncle block object is allowed for geth parity.", + "oneOf": [ + { "type": "null" }, + { "$ref": "_defs.json#/$defs/blockBase" } + ] +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getUncleByBlockNumberAndIndex.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getUncleByBlockNumberAndIndex.json new file mode 100644 index 0000000..db2988e --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getUncleByBlockNumberAndIndex.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getUncleByBlockNumberAndIndex.json", + "title": "eth_getUncleByBlockNumberAndIndex result", + "description": "An uncle block header, or null. VeChain has no uncles, so Thor always returns null; an uncle block object is allowed for geth parity.", + "oneOf": [ + { "type": "null" }, + { "$ref": "_defs.json#/$defs/blockBase" } + ] +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getUncleCountByBlockHash.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getUncleCountByBlockHash.json new file mode 100644 index 0000000..8e281d4 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getUncleCountByBlockHash.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getUncleCountByBlockHash.json", + "title": "eth_getUncleCountByBlockHash result", + "description": "Number of uncles in a block by hash. VeChain has no uncles, so this is always 0x0.", + "$ref": "_defs.json#/$defs/quantityOrNull" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_getUncleCountByBlockNumber.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getUncleCountByBlockNumber.json new file mode 100644 index 0000000..681872f --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_getUncleCountByBlockNumber.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_getUncleCountByBlockNumber.json", + "title": "eth_getUncleCountByBlockNumber result", + "description": "Number of uncles in a block by tag/number. VeChain has no uncles, so this is always 0x0.", + "$ref": "_defs.json#/$defs/quantityOrNull" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_hashrate.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_hashrate.json new file mode 100644 index 0000000..e676c45 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_hashrate.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_hashrate.json", + "title": "eth_hashrate result", + "description": "Hashes per second the node is mining at. Thor (PoA) always returns 0x0.", + "$ref": "_defs.json#/$defs/quantity" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_mining.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_mining.json new file mode 100644 index 0000000..b6b035a --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_mining.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_mining.json", + "title": "eth_mining result", + "description": "Whether the client is actively mining. Thor (PoA) always returns false.", + "type": "boolean" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_newBlockFilter.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_newBlockFilter.json new file mode 100644 index 0000000..60fb580 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_newBlockFilter.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_newBlockFilter.json", + "title": "eth_newBlockFilter result", + "description": "Newly-allocated filter id (an opaque QUANTITY handle for the eth_getFilterChanges / eth_getFilterLogs / eth_uninstallFilter family).", + "$ref": "_defs.json#/$defs/quantity" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_newFilter.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_newFilter.json new file mode 100644 index 0000000..7ec89f3 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_newFilter.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_newFilter.json", + "title": "eth_newFilter result", + "description": "Newly-allocated filter id (an opaque QUANTITY handle for the eth_getFilterChanges / eth_getFilterLogs / eth_uninstallFilter family).", + "$ref": "_defs.json#/$defs/quantity" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_newPendingTransactionFilter.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_newPendingTransactionFilter.json new file mode 100644 index 0000000..5c6f638 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_newPendingTransactionFilter.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_newPendingTransactionFilter.json", + "title": "eth_newPendingTransactionFilter result", + "description": "Newly-allocated filter id (an opaque QUANTITY handle for the eth_getFilterChanges / eth_getFilterLogs / eth_uninstallFilter family).", + "$ref": "_defs.json#/$defs/quantity" +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_subscription_syncing.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_subscription_syncing.json new file mode 100644 index 0000000..33b811b --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_subscription_syncing.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_subscription_syncing.json", + "title": "eth_subscription syncing payload", + "description": "params.result for an eth_subscription notification with subscription type 'syncing'. While syncing, Thor pushes {syncing:true, status:{startingBlock,currentBlock,highestBlock}}; once synced it pushes a single boolean false (rpc/ws/subscriptions.go:runSyncing).", + "oneOf": [ + { "type": "boolean" }, + { + "type": "object", + "properties": { + "syncing": { "type": "boolean" }, + "status": { + "type": "object", + "properties": { + "startingBlock": { "$ref": "_defs.json#/$defs/quantity" }, + "currentBlock": { "$ref": "_defs.json#/$defs/quantity" }, + "highestBlock": { "$ref": "_defs.json#/$defs/quantity" } + }, + "required": ["startingBlock", "currentBlock", "highestBlock"] + } + }, + "required": ["syncing", "status"] + } + ] +} diff --git a/tests/eth_rpc/eth_rpc_schema/schemas/eth_uninstallFilter.json b/tests/eth_rpc/eth_rpc_schema/schemas/eth_uninstallFilter.json new file mode 100644 index 0000000..ffe10a5 --- /dev/null +++ b/tests/eth_rpc/eth_rpc_schema/schemas/eth_uninstallFilter.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "eth_uninstallFilter.json", + "title": "eth_uninstallFilter result", + "description": "true if the filter existed and was removed, false otherwise.", + "type": "boolean" +} diff --git a/tests/eth_rpc/ethersjs/test/events.test.ts b/tests/eth_rpc/ethersjs/test/events.test.ts index 4114de2..933c02e 100644 --- a/tests/eth_rpc/ethersjs/test/events.test.ts +++ b/tests/eth_rpc/ethersjs/test/events.test.ts @@ -61,6 +61,66 @@ describe('Events — subscriptions & historical filters', () => { expect(ev.value).to.equal(7n); }); + it('provider.once("block") fires exactly once', async function () { + this.timeout(60_000); + let count = 0; + const first = await new Promise((resolve) => { + provider.once('block', (n: number) => { + count += 1; + resolve(n); + }); + }); + expect(first).to.be.a('number').and.greaterThan(0); + // Let several 500ms poll cycles elapse; a one-shot listener must not refire. + await new Promise((r) => setTimeout(r, 2_000)); + expect(count, 'once("block") fired more than once').to.equal(1); + }); + + it('provider.on(txHash) delivers the receipt once the tx is mined', async function () { + this.timeout(60_000); + // ethers maps provider.on() to a PollingTransactionSubscriber that + // emits the TransactionReceipt when getTransactionReceipt first returns + // non-null — a distinct path from waitForTransaction / tx.wait(). + const tx = await wallet.sendTransaction({ to: NODE2_ADDRESS, value: 1n, type: 2 }); + const receipt = await new Promise<{ hash: string; status: number | null }>((resolve) => { + provider.on(tx.hash, (r: { hash: string; status: number | null }) => resolve(r)); + }); + expect(receipt.hash).to.equal(tx.hash); + expect(receipt.status).to.equal(1); + }); + + it('provider.on(filter) delivers matching logs at the provider level', async function () { + this.timeout(60_000); + // contract.on('Set') is covered elsewhere; this exercises the bare + // provider-level filter subscription (PollingEventSubscriber over + // eth_getLogs) with an explicit {address, topics} object. + const address = await contract.getAddress(); + const setTopic = id('Set(address,uint256)'); + const seen = new Promise<{ address: string; topics: ReadonlyArray }>((resolve) => { + provider.on({ address, topics: [setTopic] }, (log: { + address: string; + topics: ReadonlyArray; + }) => resolve(log)); + }); + // Give the polling subscriber a cycle to latch its starting block before + // the emitting tx lands, so the log can't slip into an earlier block. + await new Promise((r) => setTimeout(r, 700)); + await (await contract.set(555n)).wait(); + + const log = await seen; + expect(log.address.toLowerCase()).to.equal(address.toLowerCase()); + expect(log.topics[0]).to.equal(setTopic); + }); + + it('listenerCount / listeners / off manage registered block handlers', async () => { + const handler = (): void => {}; + await provider.on('block', handler); + expect(await provider.listenerCount('block')).to.be.greaterThan(0); + expect(await provider.listeners('block')).to.include(handler); + await provider.off('block', handler); + expect(await provider.listenerCount('block')).to.equal(0); + }); + it('queryFilter returns historical Set logs since deploy', async () => { const tx = await contract.set(123n); await tx.wait(); @@ -127,6 +187,61 @@ describe('Events — subscriptions & historical filters', () => { } }); + it('eth_newBlockFilter + eth_getFilterChanges yields new block hashes', async function () { + this.timeout(60_000); + const filterId = (await provider.send('eth_newBlockFilter', [])) as string; + expect(filterId).to.match(/^0x[0-9a-fA-F]+$/); + try { + let changes: string[] = []; + const deadline = Date.now() + 30_000; + while (Date.now() < deadline) { + changes = (await provider.send('eth_getFilterChanges', [filterId])) as string[]; + if (changes.length > 0) break; + await new Promise((r) => setTimeout(r, 500)); + } + expect(changes.length, 'block hashes from eth_getFilterChanges').to.be.greaterThan(0); + expect(changes[0]).to.match(/^0x[0-9a-fA-F]{64}$/); + } finally { + expect(await provider.send('eth_uninstallFilter', [filterId])).to.equal(true); + } + }); + + it('eth_newPendingTransactionFilter returns a filter id and uninstalls', async () => { + const filterId = (await provider.send('eth_newPendingTransactionFilter', [])) as string; + expect(filterId).to.match(/^0x[0-9a-fA-F]+$/); + expect(await provider.send('eth_uninstallFilter', [filterId])).to.equal(true); + }); + + it('eth_getFilterLogs returns the full matching log set for a log filter', async function () { + this.timeout(60_000); + // Unlike eth_getFilterChanges (incremental since last poll), eth_getFilterLogs + // returns every log matching the filter criteria regardless of poll cursor. + const address = await contract.getAddress(); + const topic = id('Set(address,uint256)'); + const fromBlock = toBeHex(await provider.getBlockNumber()); + const filterId = (await provider.send('eth_newFilter', [ + { fromBlock, toBlock: 'latest', address, topics: [topic] }, + ])) as string; + try { + await (await contract.set(13579n)).wait(); + let logs: Array<{ topics: string[]; address: string }> = []; + const deadline = Date.now() + 30_000; + while (Date.now() < deadline) { + logs = (await provider.send('eth_getFilterLogs', [filterId])) as Array<{ + topics: string[]; + address: string; + }>; + if (logs.length > 0) break; + await new Promise((r) => setTimeout(r, 500)); + } + expect(logs.length, 'eth_getFilterLogs result').to.be.greaterThan(0); + expect(logs[0].topics[0]).to.equal(topic); + expect(logs[0].address.toLowerCase()).to.equal(address.toLowerCase()); + } finally { + await provider.send('eth_uninstallFilter', [filterId]); + } + }); + it('eth_getLogs accepts address[] and OR-of-topic / null-slot filter shapes', async function () { this.timeout(60_000); // ethers v6's high-level provider.getLogs forwards address arrays and diff --git a/tests/eth_rpc/ethersjs/test/provider.test.ts b/tests/eth_rpc/ethersjs/test/provider.test.ts index 6270dea..ac2383a 100644 --- a/tests/eth_rpc/ethersjs/test/provider.test.ts +++ b/tests/eth_rpc/ethersjs/test/provider.test.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { JsonRpcProvider, Wallet, ZeroAddress, id, toBeHex } from 'ethers'; +import { JsonRpcProvider, Wallet, ZeroAddress, id, toBeHex, toQuantity } from 'ethers'; import { getHttpUrl, makeProvider, @@ -291,6 +291,34 @@ describe('Provider read-only RPC', () => { expect((accounts as unknown[]).length).to.equal(0); }); + it('listAccounts() (high-level wrapper) returns an empty signer array', async () => { + // JsonRpcApiProvider.listAccounts() maps eth_accounts into JsonRpcSigner[]. + // The lower-level send('eth_accounts') is asserted above; this pins the + // high-level path dApps actually call. + const accounts = await provider.listAccounts(); + expect(accounts).to.be.an('array'); + expect(accounts.length).to.equal(0); + }); + + it('getSigner() rejects — index 0 has no unlocked node account behind it', async () => { + // getSigner() defaults to account index 0 and reads eth_accounts; with an + // empty keystore ethers throws "no such account" before returning a + // JsonRpcSigner. This is the high-level failure every getSigner()-based + // dApp hits on Thor. Flip to a success path if Thor ever unlocks keys. + let caught: unknown; + try { + await provider.getSigner(); + } catch (err) { + caught = err; + } + expect(caught, 'expected getSigner() to reject with no node accounts').to.not.be.undefined; + // ethers throws a plain Error("no such account"); its message is a + // non-enumerable own property, so read it directly rather than via the + // enumerable-leaf walker used for richer JSON-RPC error objects. + const msg = String((caught as Error).message ?? ''); + expect(msg, 'error should explain the missing node account').to.match(/account/i); + }); + it('eth_sendTransaction is rejected — no node-side signer to deliver to', async () => { let caught: unknown; try { @@ -437,4 +465,282 @@ describe('Provider read-only RPC', () => { expect(n).to.match(/^0x[0-9a-fA-F]+$/); }); }); + + describe('ENS resolution (expected unsupported — Thor network carries no ENS plugin)', () => { + // ethers v6 throws UNSUPPORTED_OPERATION "network does not support ENS" + // because the Network object for Thor's chainId has no Ens plugin + // registered. Each test attempts the real call and *skips* (rather than + // failing the suite) when that client-side gap is detected — if Thor ever + // ships an ENS registry + a matching network plugin, these flip to hard + // assertions on the resolved value. + const ensUnsupported = (err: unknown): boolean => + /does not support ENS|UNSUPPORTED_OPERATION/i.test(collectStrings(err).join(' || ')); + + it('resolveName(name) — forward resolution', async function () { + try { + const addr = await provider.resolveName('vitalik.eth'); + expect(addr === null || /^0x[0-9a-fA-F]{40}$/.test(addr)).to.equal(true); + } catch (err) { + if (ensUnsupported(err)) this.skip(); + throw err; + } + }); + + it('lookupAddress(address) — reverse resolution', async function () { + try { + const name = await provider.lookupAddress(TEST_SENDER_ADDRESS); + expect(name === null || typeof name === 'string').to.equal(true); + } catch (err) { + if (ensUnsupported(err)) this.skip(); + throw err; + } + }); + + it('getResolver(name) — resolver lookup', async function () { + try { + const resolver = await provider.getResolver('vitalik.eth'); + expect(resolver === null || typeof resolver === 'object').to.equal(true); + } catch (err) { + if (ensUnsupported(err)) this.skip(); + throw err; + } + }); + + it('getAvatar(name) — avatar lookup', async function () { + try { + const avatar = await provider.getAvatar('vitalik.eth'); + expect(avatar === null || typeof avatar === 'string').to.equal(true); + } catch (err) { + if (ensUnsupported(err)) this.skip(); + throw err; + } + }); + }); +}); + +// --------------------------------------------------------------------------- +// Coverage for eth_* RPC methods Thor implements but ethers exposes no +// high-level wrapper for — exercised via provider.send and asserted against +// the Ethereum JSON-RPC contract. Cross-checked against thor's rpc/ handlers +// on branch pedro/eth_eq_json_rpc. +// --------------------------------------------------------------------------- +describe('Block & transaction index methods (implemented on Thor)', () => { + let provider: JsonRpcProvider; + let txHash: string; + let blockNumber: number; + let blockHash: string; + let txIndex: number; + + before(async () => { + provider = makeProvider(); + const wallet = makeWallet(TEST_SENDER_KEY, provider); + const tx = await wallet.sendTransaction({ to: NODE2_ADDRESS, value: 1n, type: 2 }); + const r = await tx.wait(); + txHash = tx.hash; + blockNumber = r!.blockNumber; + blockHash = r!.blockHash; + txIndex = r!.index; // ethers v6 exposes the tx position as receipt.index + }); + + it('eth_getBlockTransactionCountByNumber matches the block tx array length', async () => { + const block = await provider.getBlock(blockNumber, false); + const count = (await provider.send('eth_getBlockTransactionCountByNumber', [ + toQuantity(blockNumber), + ])) as string; + expect(count).to.match(/^0x[0-9a-fA-F]+$/); + expect(Number(BigInt(count))).to.equal(block!.transactions.length); + }); + + it('eth_getBlockTransactionCountByHash matches the block tx array length', async () => { + const block = await provider.getBlock(blockNumber, false); + const count = (await provider.send('eth_getBlockTransactionCountByHash', [blockHash])) as string; + expect(count).to.match(/^0x[0-9a-fA-F]+$/); + expect(Number(BigInt(count))).to.equal(block!.transactions.length); + }); + + it('eth_getTransactionByBlockNumberAndIndex returns the sent tx at its index', async () => { + const t = (await provider.send('eth_getTransactionByBlockNumberAndIndex', [ + toQuantity(blockNumber), + toQuantity(txIndex), + ])) as { hash: string; blockHash: string } | null; + expect(t, 'tx by (number,index)').to.not.be.null; + expect(t!.hash.toLowerCase()).to.equal(txHash.toLowerCase()); + expect(t!.blockHash.toLowerCase()).to.equal(blockHash.toLowerCase()); + }); + + it('eth_getTransactionByBlockHashAndIndex returns the sent tx at its index', async () => { + const t = (await provider.send('eth_getTransactionByBlockHashAndIndex', [ + blockHash, + toQuantity(txIndex), + ])) as { hash: string } | null; + expect(t, 'tx by (hash,index)').to.not.be.null; + expect(t!.hash.toLowerCase()).to.equal(txHash.toLowerCase()); + }); + + it('eth_getTransactionByBlockNumberAndIndex returns null for an out-of-range index', async () => { + const t = await provider.send('eth_getTransactionByBlockNumberAndIndex', [ + toQuantity(blockNumber), + '0xffff', + ]); + expect(t).to.equal(null); + }); +}); + +describe('Uncle methods (VeChain has no uncles — implemented as empty)', () => { + let provider: JsonRpcProvider; + before(() => { + provider = makeProvider(); + }); + + it('eth_getUncleCountByBlockNumber returns 0x0', async () => { + const c = (await provider.send('eth_getUncleCountByBlockNumber', ['latest'])) as string; + expect(c).to.match(/^0x0+$/); + }); + + it('eth_getUncleCountByBlockHash returns 0x0', async () => { + const latest = await provider.getBlock('latest'); + const c = (await provider.send('eth_getUncleCountByBlockHash', [latest!.hash])) as string; + expect(c).to.match(/^0x0+$/); + }); + + it('eth_getUncleByBlockNumberAndIndex returns null', async () => { + const u = await provider.send('eth_getUncleByBlockNumberAndIndex', ['latest', '0x0']); + expect(u).to.equal(null); + }); + + it('eth_getUncleByBlockHashAndIndex returns null', async () => { + const latest = await provider.getBlock('latest'); + const u = await provider.send('eth_getUncleByBlockHashAndIndex', [latest!.hash, '0x0']); + expect(u).to.equal(null); + }); +}); + +describe('Chain & node metadata (implemented on Thor)', () => { + let provider: JsonRpcProvider; + before(() => { + provider = makeProvider(); + }); + + it('net_version equals the decimal chainId', async () => { + const netV = (await provider.send('net_version', [])) as string; + const chainId = (await provider.getNetwork()).chainId; + expect(netV).to.be.a('string'); + expect(BigInt(netV)).to.equal(chainId); + }); + + it('net_listening returns true', async () => { + expect(await provider.send('net_listening', [])).to.equal(true); + }); + + it('net_peerCount returns a hex quantity', async () => { + const pc = (await provider.send('net_peerCount', [])) as string; + expect(pc).to.match(/^0x[0-9a-fA-F]+$/); + }); + + it('web3_clientVersion returns a Thor/* string', async () => { + const v = (await provider.send('web3_clientVersion', [])) as string; + expect(v).to.be.a('string').and.match(/thor/i); + }); + + it('eth_coinbase returns the zero address (PoA — no coinbase reward addr)', async () => { + const cb = (await provider.send('eth_coinbase', [])) as string; + expect(cb).to.match(/^0x0{40}$/); + }); + + it('eth_mining returns false (PoA — no local mining)', async () => { + expect(await provider.send('eth_mining', [])).to.equal(false); + }); + + it('eth_hashrate returns 0x0 (PoA — no hashrate)', async () => { + const hr = (await provider.send('eth_hashrate', [])) as string; + expect(hr).to.match(/^0x0+$/); + }); + + it('eth_syncing returns false or a syncing-status object', async () => { + const s = await provider.send('eth_syncing', []); + expect(s === false || (typeof s === 'object' && s !== null)).to.equal(true); + }); +}); + +describe('Unimplemented standard eth_* methods (skipped until Thor ships them)', () => { + let provider: JsonRpcProvider; + before(() => { + provider = makeProvider(); + }); + + // True when an error reads as a JSON-RPC "method not found" / unsupported gap + // rather than a transport or params failure. + const notFound = (err: unknown): boolean => + /not found|not supported|unsupported|does not exist|not available|method .*missing/i.test( + collectStrings(err).join(' || '), + ); + + // Standard Ethereum methods thor's pedro/eth_eq_json_rpc dispatcher does NOT + // register. Each test attempts the call and skips while it 404s at the method + // level; if Thor ever registers one, the call succeeds and the assertion + // (result is defined) keeps it honest. A non-"not found" error fails loudly. + const unimplemented: Array<{ method: string; params: unknown[] }> = [ + { method: 'eth_getProof', params: [TEST_SENDER_ADDRESS, [], 'latest'] }, + { method: 'eth_createAccessList', params: [{ from: TEST_SENDER_ADDRESS, to: NODE2_ADDRESS }, 'latest'] }, + { method: 'eth_protocolVersion', params: [] }, + { method: 'eth_pendingTransactions', params: [] }, + { method: 'eth_sign', params: [TEST_SENDER_ADDRESS, '0x68656c6c6f'] }, + { method: 'eth_signTransaction', params: [{ from: TEST_SENDER_ADDRESS, to: NODE2_ADDRESS, value: '0x1' }] }, + { method: 'eth_getRawTransactionByHash', params: ['0x' + '00'.repeat(32)] }, + { method: 'debug_traceTransaction', params: ['0x' + '00'.repeat(32)] }, + ]; + + for (const { method, params } of unimplemented) { + it(`${method} — skipped while unimplemented`, async function () { + let result: unknown; + let caught: unknown; + try { + result = await provider.send(method, params); + } catch (err) { + caught = err; + } + if (caught !== undefined && notFound(caught)) { + this.skip(); + } + // Reached only if Thor answered or errored for some *other* reason. + expect( + caught, + `${method} errored for a non-"not found" reason: ${collectStrings(caught).join(' || ')}`, + ).to.be.undefined; + expect(result, `${method} unexpectedly returned undefined without an error`).to.not.be.undefined; + }); + } +}); + +describe('Category-3 divergences from Ethereum (skipped until Thor aligns)', () => { + let provider: JsonRpcProvider; + before(() => { + provider = makeProvider(); + }); + + // geth's eth_feeHistory returns a per-block × per-percentile `reward` matrix + // when called with rewardPercentiles. Thor (rpc/fees/handler.go) currently + // rejects the percentile form — "reward percentiles are not yet supported" — + // so a fee estimator that requests percentiles can't use it. We SKIP on that + // documented gap; if Thor ever ships it, the call succeeds and the + // reward-matrix assertion keeps it honest. The sibling "is rejected by Thor" + // test above covers the current behavior. + it('eth_feeHistory with rewardPercentiles returns a reward matrix (geth parity)', async function () { + let raw: { reward?: string[][] }; + try { + raw = (await provider.send('eth_feeHistory', ['0x4', 'latest', [25, 50, 75]])) as { + reward?: string[][]; + }; + } catch (err) { + if (/percentile|not yet supported/i.test(collectStrings(err).join(' || '))) { + this.skip(); + } + throw err; + } + expect(raw, 'feeHistory result').to.be.an('object'); + expect(raw.reward, 'reward matrix').to.be.an('array').and.length.greaterThan(0); + for (const row of raw.reward!) { + expect(row, 'per-block reward row').to.be.an('array').and.length(3); + } + }); }); diff --git a/tests/eth_rpc/ethersjs/test/wallet.test.ts b/tests/eth_rpc/ethersjs/test/wallet.test.ts index 1c50f54..732685e 100644 --- a/tests/eth_rpc/ethersjs/test/wallet.test.ts +++ b/tests/eth_rpc/ethersjs/test/wallet.test.ts @@ -237,6 +237,27 @@ describe('Wallet — sign & send EIP-1559 tx', () => { expect(after - before).to.equal(3n); }); + it('signer-level getNonce / estimateGas / call route through the provider', async () => { + // Wallet (BaseWallet → AbstractSigner) exposes its own getNonce/estimateGas/ + // call wrappers that thread `from` through the provider's RPC. They overlap + // with the provider-level methods covered in provider.test.ts but exercise + // the signer code path dApps use via `wallet.*`. + const provider = makeProvider(); + const wallet = makeWallet(TEST_SENDER_KEY, provider); + + const signerNonce = await wallet.getNonce(); + const providerNonce = await provider.getTransactionCount(TEST_SENDER_ADDRESS); + expect(signerNonce).to.equal(providerNonce); + + const gas = await wallet.estimateGas({ to: NODE2_ADDRESS, value: 1n }); + expect(gas).to.be.a('bigint'); + expect(gas > 0n, `gas was ${gas}`).to.equal(true); + + // A no-op call to an EOA returns 0x; the signer fills in `from` for us. + const result = await wallet.call({ to: NODE2_ADDRESS, value: 0n }); + expect(result).to.equal('0x'); + }); + it('HDNodeWallet.fromPhrase produces a deterministic address from a mnemonic', () => { const phrase = 'test test test test test test test test test test test junk'; diff --git a/tests/eth_rpc/ethersjs/test/websocket.test.ts b/tests/eth_rpc/ethersjs/test/websocket.test.ts index 378fdaf..22df4a8 100644 --- a/tests/eth_rpc/ethersjs/test/websocket.test.ts +++ b/tests/eth_rpc/ethersjs/test/websocket.test.ts @@ -6,6 +6,7 @@ import { Wallet, } from 'ethers'; import { + getWsUrl, loadStorageArtifact, makeProvider, makeWallet, @@ -15,6 +16,19 @@ import { TEST_SENDER_KEY, } from '../src/fixtures'; +// Minimal structural type for the Node 22+ global WebSocket — @types/node@20 +// doesn't declare it, so we reach for it through globalThis with our own shape +// rather than relying on the lib typings. +interface RawWebSocket { + send(data: string): void; + close(): void; + addEventListener(type: 'open' | 'error', listener: () => void): void; + addEventListener(type: 'message', listener: (ev: { data: unknown }) => void): void; +} +const RawWebSocket = (globalThis as unknown as { + WebSocket: new (url: string) => RawWebSocket; +}).WebSocket; + describe('WebSocketProvider — eth_subscribe (newHeads / logs)', () => { it('getChainId works over a WebSocket transport', async () => { const wsProvider = makeWsProvider(); @@ -109,6 +123,73 @@ describe('WebSocketProvider — eth_subscribe (newHeads / logs)', () => { } }); + it('wsProvider.once("block") fires exactly once over the socket', async function () { + this.timeout(60_000); + const wsProvider = makeWsProvider(); + try { + let count = 0; + const n = await new Promise((resolve) => { + wsProvider.once('block', (b: number) => { + count += 1; + resolve(b); + }); + }); + expect(n).to.be.a('number').and.greaterThan(0); + // A couple of head notifications should arrive in the next 2s; a one-shot + // listener must not refire on them. + await new Promise((r) => setTimeout(r, 2_000)); + expect(count, 'ws once("block") fired more than once').to.equal(1); + } finally { + wsProvider.removeAllListeners(); + await wsProvider.destroy(); + } + }); + + it('eth_subscribe("syncing") over the socket — skip if Thor does not register it', async function () { + this.timeout(30_000); + // ethers exposes no high-level "syncing" event and routing a raw + // eth_subscribe through its SocketProvider fights the internal subscription + // manager — so drive a bare WebSocket and speak JSON-RPC directly. Thor has + // historically not implemented the "syncing" subscription topic; if it + // still rejects the request we skip (documenting the gap) instead of + // failing. If Thor adds it, this asserts a well-formed subscription id. + const ws = new RawWebSocket(getWsUrl()); + let response: { result?: unknown; error?: unknown }; + try { + response = await new Promise((resolve, reject) => { + const timer = setTimeout( + () => reject(new Error('eth_subscribe(syncing) timed out')), + 15_000, + ); + ws.addEventListener('open', () => { + ws.send( + JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_subscribe', params: ['syncing'] }), + ); + }); + ws.addEventListener('message', (ev: { data: unknown }) => { + clearTimeout(timer); + try { + resolve(JSON.parse(String(ev.data)) as { result?: unknown; error?: unknown }); + } catch (e) { + reject(e as Error); + } + }); + ws.addEventListener('error', () => { + clearTimeout(timer); + reject(new Error('websocket transport error')); + }); + }); + } finally { + ws.close(); + } + + if (response.error != null) { + // Thor rejected the subscription topic — documented gap, not a failure. + this.skip(); + } + expect(response.result, 'subscription id').to.match(/^0x[0-9a-fA-F]+$/); + }); + it('provider.destroy() closes the websocket cleanly', async () => { const wsProvider = makeWsProvider(); // One successful call to confirm the connection is live... diff --git a/tests/eth_rpc/viem/.gitignore b/tests/eth_rpc/viem/.gitignore new file mode 100644 index 0000000..3c25e1e --- /dev/null +++ b/tests/eth_rpc/viem/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.log diff --git a/tests/eth_rpc/viem/.mocharc.cjs b/tests/eth_rpc/viem/.mocharc.cjs new file mode 100644 index 0000000..693e14c --- /dev/null +++ b/tests/eth_rpc/viem/.mocharc.cjs @@ -0,0 +1,8 @@ +module.exports = { + require: ['ts-node/register'], + extensions: ['ts'], + spec: ['test/**/*.test.ts'], + timeout: 120000, + reporter: 'spec', + exit: true, +}; diff --git a/tests/eth_rpc/viem/contracts/Create2Factory.json b/tests/eth_rpc/viem/contracts/Create2Factory.json new file mode 100644 index 0000000..2c317ee --- /dev/null +++ b/tests/eth_rpc/viem/contracts/Create2Factory.json @@ -0,0 +1,43 @@ +{ + "contractName": "Create2Factory", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "Deployed", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "initCode", + "type": "bytes" + } + ], + "name": "deploy", + "outputs": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x6080604052348015600f57600080fd5b506101b38061001f6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063cdcb760a14610030575b600080fd5b61004361003e3660046100ff565b61005f565b6040516001600160a01b03909116815260200160405180910390f35b6000604051828482378483826000f59150506001600160a01b0381166100bc5760405162461bcd60e51b815260206004820152600e60248201526d18dc99585d194c8819985a5b195960921b604482015260640160405180910390fd5b6040516001600160a01b03821681527ff40fcec21964ffb566044d083b4073f29f7f7929110ea19e1b3ebe375d89055e9060200160405180910390a19392505050565b60008060006040848603121561011457600080fd5b83359250602084013567ffffffffffffffff81111561013257600080fd5b8401601f8101861361014357600080fd5b803567ffffffffffffffff81111561015a57600080fd5b86602082840101111561016c57600080fd5b93966020919091019550929350505056fea264697066735822122063bb8db7af6a7c0d1a24b676438d45379658b1e0ba102228bcd89cff515b6ad964736f6c63430008230033" +} diff --git a/tests/eth_rpc/viem/contracts/Create2Factory.sol b/tests/eth_rpc/viem/contracts/Create2Factory.sol new file mode 100644 index 0000000..521624f --- /dev/null +++ b/tests/eth_rpc/viem/contracts/Create2Factory.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract Create2Factory { + event Deployed(address addr); + + function deploy(bytes32 salt, bytes calldata initCode) external returns (address addr) { + assembly { + let memPtr := mload(0x40) + calldatacopy(memPtr, initCode.offset, initCode.length) + addr := create2(0, memPtr, initCode.length, salt) + } + require(addr != address(0), "create2 failed"); + emit Deployed(addr); + } +} diff --git a/tests/eth_rpc/viem/contracts/Storage.json b/tests/eth_rpc/viem/contracts/Storage.json new file mode 100644 index 0000000..380606f --- /dev/null +++ b/tests/eth_rpc/viem/contracts/Storage.json @@ -0,0 +1,140 @@ +{ + "contractName": "Storage", + "abi": [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "given", + "type": "uint256" + } + ], + "name": "MustBeNonZero", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "who", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Set", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "who", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Tipped", + "type": "event" + }, + { + "inputs": [], + "name": "get", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "v", + "type": "uint256" + } + ], + "name": "set", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "v", + "type": "uint256" + } + ], + "name": "setStrict", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "v", + "type": "uint256" + } + ], + "name": "setStrictCustomError", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "tip", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "totalTipped", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "value", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x6080604052348015600f57600080fd5b506102648061001f6000396000f3fe6080604052600436106100705760003560e01c80635e170bd51161004e5780635e170bd5146100c757806360fe47b1146100e75780636d4ce63c14610107578063b814b7ed1461011c57600080fd5b80632755cd2d146100755780633fa4f2451461007f57806358dd18d3146100a7575b600080fd5b61007d61012f565b005b34801561008b57600080fd5b5061009560005481565b60405190815260200160405180910390f35b3480156100b357600080fd5b5061007d6100c2366004610215565b610166565b3480156100d357600080fd5b5061007d6100e2366004610215565b6101f1565b3480156100f357600080fd5b5061007d610102366004610215565b6101b4565b34801561011357600080fd5b50600054610095565b34801561012857600080fd5b5047610095565b60405134815233907f905516bf815c273f240e1d48d78ea7db3f1f0d00b912fc69522caf0ea70450a29060200160405180910390a2565b806000036101b45760405162461bcd60e51b815260206004820152601660248201527576616c7565206d757374206265206e6f6e2d7a65726f60501b60448201526064015b60405180910390fd5b600081905560405181815233907ffd28ec3ec2555238d8ad6f9faf3e4cd10e574ce7e7ef28b73caa53f9512f65b99060200160405180910390a250565b806000036101b45760405163251ed31d60e11b8152600481018290526024016101ab565b60006020828403121561022757600080fd5b503591905056fea2646970667358221220947e72bf8cff231dfcab3ec92546fb82ae35ca5ba5a041822cff8a7d7793e82364736f6c63430008230033" +} diff --git a/tests/eth_rpc/viem/contracts/Storage.sol b/tests/eth_rpc/viem/contracts/Storage.sol new file mode 100644 index 0000000..1b00926 --- /dev/null +++ b/tests/eth_rpc/viem/contracts/Storage.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract Storage { + event Set(address indexed who, uint256 value); + + uint256 public value; + + function set(uint256 v) external { + value = v; + emit Set(msg.sender, v); + } + + function get() external view returns (uint256) { + return value; + } + + function setStrict(uint256 v) external { + require(v != 0, "value must be non-zero"); + value = v; + emit Set(msg.sender, v); + } + + error MustBeNonZero(uint256 given); + + function setStrictCustomError(uint256 v) external { + if (v == 0) revert MustBeNonZero(v); + value = v; + emit Set(msg.sender, v); + } + + event Tipped(address indexed who, uint256 amount); + + function tip() external payable { + emit Tipped(msg.sender, msg.value); + } + + function totalTipped() external view returns (uint256) { + return address(this).balance; + } +} diff --git a/tests/eth_rpc/viem/package-lock.json b/tests/eth_rpc/viem/package-lock.json new file mode 100644 index 0000000..fa9a791 --- /dev/null +++ b/tests/eth_rpc/viem/package-lock.json @@ -0,0 +1,1474 @@ +{ + "name": "interstellar-eth-rpc-viem", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "interstellar-eth-rpc-viem", + "version": "0.0.0", + "devDependencies": { + "@types/chai": "^4.3.16", + "@types/mocha": "^10.0.6", + "@types/node": "^20.12.7", + "chai": "^4.4.1", + "mocha": "^10.4.0", + "ts-node": "^10.9.2", + "typescript": "^5.9.3", + "viem": "^2.21.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://mirrors.cloud.tencent.com/npm/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://mirrors.cloud.tencent.com/npm/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://mirrors.cloud.tencent.com/npm/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://mirrors.cloud.tencent.com/npm/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "4.3.20", + "resolved": "https://mirrors.cloud.tencent.com/npm/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://mirrors.cloud.tencent.com/npm/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://mirrors.cloud.tencent.com/npm/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://mirrors.cloud.tencent.com/npm/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://mirrors.cloud.tencent.com/npm/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://mirrors.cloud.tencent.com/npm/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://mirrors.cloud.tencent.com/npm/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://mirrors.cloud.tencent.com/npm/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/ox": { + "version": "0.14.29", + "resolved": "https://mirrors.cloud.tencent.com/npm/ox/-/ox-0.14.29.tgz", + "integrity": "sha512-M5j87Ec4V99MQdRct/g09eWXW60g6zhHTUs1lr4deUtrPDnezBdCJTgKd7pxqTpSZBFveV0ALi9jMMuT1qKyNg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/viem": { + "version": "2.53.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/viem/-/viem-2.53.1.tgz", + "integrity": "sha512-FhfJ/SW73CVosiyVLmIMVgKDRKYV1AGCLzZoHYvmNayyVff63Qi1ocPCk59LqC/cNw244RbBJjHnmxqXkE7NpA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.14.29", + "ws": "8.20.1" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://mirrors.cloud.tencent.com/npm/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://mirrors.cloud.tencent.com/npm/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/tests/eth_rpc/viem/package.json b/tests/eth_rpc/viem/package.json new file mode 100644 index 0000000..ee2f80a --- /dev/null +++ b/tests/eth_rpc/viem/package.json @@ -0,0 +1,19 @@ +{ + "name": "interstellar-eth-rpc-viem", + "private": true, + "version": "0.0.0", + "description": "viem v2 compatibility tests against Thor's Ethereum-compatible RPC.", + "scripts": { + "test": "mocha" + }, + "devDependencies": { + "@types/chai": "^4.3.16", + "@types/mocha": "^10.0.6", + "@types/node": "^20.12.7", + "chai": "^4.4.1", + "mocha": "^10.4.0", + "ts-node": "^10.9.2", + "typescript": "^5.9.3", + "viem": "^2.21.0" + } +} diff --git a/tests/eth_rpc/viem/src/fixtures.ts b/tests/eth_rpc/viem/src/fixtures.ts new file mode 100644 index 0000000..83de9ad --- /dev/null +++ b/tests/eth_rpc/viem/src/fixtures.ts @@ -0,0 +1,138 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { + createPublicClient, + createWalletClient, + defineChain, + http, + webSocket, + type Abi, + type Chain, +} from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; + +// NODE_URL is exported by the Go wrapper (tests/eth_rpc/viem/viem_test.go) which +// manages the network lifecycle via helper.RunTestMain. Running `npx mocha` or +// `npm test` directly requires the caller to export NODE_URL themselves. +export function getNodeUrl(): string { + const url = process.env.NODE_URL; + if (!url) { + throw new Error( + 'NODE_URL not set — run the suite via `go test` (which starts the network) or export NODE_URL manually', + ); + } + return url; +} + +// Pre-funded master accounts from LocalThreeNodesNetwork genesis. +// Mirrors tests/helper/client.go:16-21 and the ethersjs / web3js fixtures. +export const TEST_SENDER_KEY = + '0x01a4107bfb7d5141ec519e75788c34295741a1eefbfe460320efd2ada944071e' as const; +export const TEST_SENDER_ADDRESS = '0x61fF580B63D3845934610222245C116E013717ec' as const; + +export const NODE2_KEY = + '0x7072249b800ddac1d29a3cd06468cc1a917cbcd110dde358a905d03dad51748d' as const; +export const NODE2_ADDRESS = '0x327931085B4cCbCE0baABb5a5E1C678707C51d90' as const; + +export const NODE3_KEY = + '0xc55455943bf026dc44fcf189e8765eb0587c94e66029d580bae795386c0b737a' as const; +export const NODE3_ADDRESS = '0x084E48c8AE79656D7e27368AE5317b5c2D6a7497' as const; + +export function getHttpUrl(): string { + // Thor exposes the Ethereum-compatible JSON-RPC at /rpc — the bare + // URL returns 307. Mirrors tests/eth_rpc/eth_rpc_schema/rpc_test.go:71. + return getNodeUrl().replace(/\/$/, '') + '/rpc'; +} + +export function getWsUrl(): string { + // Thor accepts a WebSocket upgrade on the same /rpc path as HTTP POST + // (cmd/thor/httpserver/api_server.go — `router.PathPrefix("/rpc").Handler(rpcWs)`). + const base = getNodeUrl().replace(/\/$/, ''); + return base.replace(/^http/, 'ws') + '/rpc'; +} + +// makePublicClient builds a read-only client over the HTTP transport. +export function makePublicClient() { + return createPublicClient({ transport: http(getHttpUrl()) }); +} + +// makeWsClient builds a client over the WebSocket transport — viem routes +// watch* actions through eth_subscribe on this transport. +export function makeWsClient() { + return createPublicClient({ transport: webSocket(getWsUrl()) }); +} + +// getThorChain fetches eth_chainId once and wraps it in a viem Chain. The wallet +// client needs a Chain so local-account signing can stamp the right chainId; the +// id is dynamic for the local network so we read it rather than hardcode. +let cachedChain: Chain | undefined; +export async function getThorChain(): Promise { + if (cachedChain) return cachedChain; + const chainId = await makePublicClient().getChainId(); + cachedChain = defineChain({ + id: chainId, + name: 'thor-local', + nativeCurrency: { name: 'VeThor', symbol: 'VTHO', decimals: 18 }, + rpcUrls: { default: { http: [getHttpUrl()], webSocket: [getWsUrl()] } }, + }); + return cachedChain; +} + +// makeWalletClient builds a wallet client bound to a local account. viem signs +// locally and submits via eth_sendRawTransaction, which is exactly what Thor's +// EIP-1559-only RPC accepts. +export async function makeWalletClient(key: `0x${string}`) { + const account = privateKeyToAccount(key); + const chain = await getThorChain(); + return createWalletClient({ account, chain, transport: http(getHttpUrl()) }); +} + +// rpc is the raw JSON-RPC escape hatch — the viem analogue of ethers' +// provider.send() / web3's requestManager.send(). Used for methods that lack a +// high-level viem action (net_*, eth_coinbase, the uncle / filter family, +// EIP-1898 object forms, expected-rejection probes, ...). +export async function rpc( + client: { request: (args: { method: string; params?: unknown }) => Promise }, + method: string, + params: unknown[] = [], +): Promise { + return client.request({ method, params }); +} + +export interface ContractArtifact { + contractName: string; + abi: Abi; + bytecode: `0x${string}`; +} + +export function loadStorageArtifact(): ContractArtifact { + return loadArtifact('Storage'); +} + +export function loadCreate2FactoryArtifact(): ContractArtifact { + return loadArtifact('Create2Factory'); +} + +function loadArtifact(name: string): ContractArtifact { + const artifactPath = path.join(__dirname, '..', 'contracts', `${name}.json`); + const raw = fs.readFileSync(artifactPath, 'utf8'); + return JSON.parse(raw) as ContractArtifact; +} + +// collectStrings walks an unknown error/response and gathers every string-valued +// leaf — RPC error text can live at .message, .details, .cause.message, +// .shortMessage, etc. Used for skip / reject detection. +export function collectStrings(obj: unknown, depth = 0): string[] { + const out: string[] = []; + if (depth > 5 || obj == null) return out; + if (typeof obj === 'string') { + out.push(obj); + return out; + } + if (typeof obj === 'object') { + for (const v of Object.values(obj as Record)) { + out.push(...collectStrings(v, depth + 1)); + } + } + return out; +} diff --git a/tests/eth_rpc/viem/test/contract.test.ts b/tests/eth_rpc/viem/test/contract.test.ts new file mode 100644 index 0000000..dedf100 --- /dev/null +++ b/tests/eth_rpc/viem/test/contract.test.ts @@ -0,0 +1,206 @@ +import { expect } from 'chai'; +import { + encodeFunctionData, + getContract, + getContractAddress, + keccak256, + parseEventLogs, + toHex, + type Address, +} from 'viem'; +import { + loadCreate2FactoryArtifact, + loadStorageArtifact, + makePublicClient, + makeWalletClient, + collectStrings, + TEST_SENDER_KEY, +} from '../src/fixtures'; + +describe('Contract — deploy & call via viem', () => { + const artifact = loadStorageArtifact(); + let client: ReturnType; + let wallet: Awaited>; + let address: Address; + + before(async () => { + client = makePublicClient(); + wallet = await makeWalletClient(TEST_SENDER_KEY); + const hash = await wallet.deployContract({ abi: artifact.abi, bytecode: artifact.bytecode }); + const receipt = await client.waitForTransactionReceipt({ hash }); + address = receipt.contractAddress!; + }); + + it('deploys to a non-empty contract address', async () => { + expect(address).to.match(/^0x[0-9a-fA-F]{40}$/); + const code = await client.getCode({ address }); + expect(code, 'deployed code').to.match(/^0x[0-9a-fA-F]+$/); + expect(code!.length).to.be.greaterThan(2); + }); + + it('initial value() and get() both return 0n', async () => { + expect(await client.readContract({ address, abi: artifact.abi, functionName: 'value' })).to.equal(0n); + expect(await client.readContract({ address, abi: artifact.abi, functionName: 'get' })).to.equal(0n); + }); + + it('set(42) persists the value and the receipt status is success', async () => { + const hash = await wallet.writeContract({ + address, + abi: artifact.abi, + functionName: 'set', + args: [42n], + }); + const receipt = await client.waitForTransactionReceipt({ hash }); + expect(receipt.status).to.equal('success'); + expect(await client.readContract({ address, abi: artifact.abi, functionName: 'get' })).to.equal(42n); + }); + + it('setStrict(0) reverts with the declared reason (via simulateContract)', async () => { + let caught: unknown; + try { + await client.simulateContract({ + address, + abi: artifact.abi, + functionName: 'setStrict', + args: [0n], + account: wallet.account, + }); + } catch (err) { + caught = err; + } + expect(caught, 'expected setStrict(0) to revert').to.not.be.undefined; + const blob = collectStrings(caught).join(' || '); + expect(blob).to.match(/value must be non-zero|reverted/i); + }); + + it('setStrictCustomError(0) reverts with a decoded custom error', async () => { + let caught: unknown; + try { + await client.simulateContract({ + address, + abi: artifact.abi, + functionName: 'setStrictCustomError', + args: [0n], + account: wallet.account, + }); + } catch (err) { + caught = err; + } + expect(caught, 'expected custom-error revert').to.not.be.undefined; + const blob = collectStrings(caught).join(' || '); + expect(blob).to.match(/MustBeNonZero/); + }); + + it('simulateContract on a write returns without sending (no nonce change)', async () => { + const nonceBefore = await client.getTransactionCount({ address: wallet.account.address }); + await client.simulateContract({ + address, + abi: artifact.abi, + functionName: 'set', + args: [999n], + account: wallet.account, + }); + const nonceAfter = await client.getTransactionCount({ address: wallet.account.address }); + expect(nonceAfter).to.equal(nonceBefore); + expect(await client.readContract({ address, abi: artifact.abi, functionName: 'get' })).to.equal(42n); + }); + + it('parseEventLogs decodes a Set event from a raw receipt', async () => { + const hash = await wallet.writeContract({ + address, + abi: artifact.abi, + functionName: 'set', + args: [99n], + }); + const receipt = await client.waitForTransactionReceipt({ hash }); + const events = parseEventLogs({ abi: artifact.abi, logs: receipt.logs }); + const set = events.find((e) => e.eventName === 'Set'); + expect(set, 'Set event').to.exist; + expect((set!.args as { value: bigint }).value).to.equal(99n); + }); + + it('estimateContractGas returns a positive bigint via the method-level API', async () => { + const gas = await client.estimateContractGas({ + address, + abi: artifact.abi, + functionName: 'set', + args: [7n], + account: wallet.account, + }); + expect(gas).to.be.a('bigint'); + expect(gas > 0n, `gas was ${gas}`).to.equal(true); + }); + + it('encodeFunctionData returns ABI-encoded calldata with the right selector', () => { + const data = encodeFunctionData({ abi: artifact.abi, functionName: 'set', args: [99n] }); + // selector for set(uint256) = first 4 bytes of keccak256("set(uint256)") + expect(data.startsWith('0x60fe47b1'), 'selector mismatch').to.equal(true); + expect(data.length).to.equal(2 + 4 * 2 + 32 * 2); + }); + + it('getContract gives a bound handle that reads current state', async () => { + const contract = getContract({ address, abi: artifact.abi, client }); + expect(await contract.read.value()).to.equal(99n); + }); + + it('CREATE2 parity — getContractAddress matches the on-chain deployed address', async function () { + this.timeout(60_000); + const factoryArtifact = loadCreate2FactoryArtifact(); + const factoryHash = await wallet.deployContract({ + abi: factoryArtifact.abi, + bytecode: factoryArtifact.bytecode, + }); + const factoryReceipt = await client.waitForTransactionReceipt({ hash: factoryHash }); + const factoryAddress = factoryReceipt.contractAddress!; + + const salt = keccak256(toHex(`viem-create2-${factoryAddress}`)); + const initCode = artifact.bytecode; + const expected = getContractAddress({ + opcode: 'CREATE2', + from: factoryAddress, + salt, + bytecode: initCode, + }); + + // Thor's compat estimateGas undercounts CREATE2 + inner constructor; set an + // explicit generous gas like the ethersjs test does. + const deployHash = await wallet.writeContract({ + address: factoryAddress, + abi: factoryArtifact.abi, + functionName: 'deploy', + args: [salt, initCode], + gas: 3_000_000n, + }); + const receipt = await client.waitForTransactionReceipt({ hash: deployHash }); + expect(receipt.status).to.equal('success'); + + const events = parseEventLogs({ abi: factoryArtifact.abi, logs: receipt.logs }); + const deployed = events.find((e) => e.eventName === 'Deployed'); + expect(deployed, 'Deployed event').to.exist; + const onChainAddr = (deployed!.args as { addr: string }).addr; + + expect(onChainAddr.toLowerCase()).to.equal(expected.toLowerCase()); + const code = await client.getCode({ address: onChainAddr as Address }); + expect(code!.length).to.be.greaterThan(2); + }); + + it('payable tip() accepts value, increments contract balance, emits Tipped', async () => { + const balanceBefore = await client.getBalance({ address }); + const hash = await wallet.writeContract({ + address, + abi: artifact.abi, + functionName: 'tip', + value: 1234n, + }); + const receipt = await client.waitForTransactionReceipt({ hash }); + expect(receipt.status).to.equal('success'); + + const balanceAfter = await client.getBalance({ address }); + expect(balanceAfter - balanceBefore).to.equal(1234n); + + const events = parseEventLogs({ abi: artifact.abi, logs: receipt.logs }); + const tipped = events.find((e) => e.eventName === 'Tipped'); + expect(tipped, 'Tipped event').to.exist; + expect((tipped!.args as { amount: bigint }).amount).to.equal(1234n); + }); +}); diff --git a/tests/eth_rpc/viem/test/events.test.ts b/tests/eth_rpc/viem/test/events.test.ts new file mode 100644 index 0000000..54ff2b9 --- /dev/null +++ b/tests/eth_rpc/viem/test/events.test.ts @@ -0,0 +1,216 @@ +import { expect } from 'chai'; +import { + createPublicClient, + http, + toEventSelector, + zeroAddress, + type AbiEvent, + type Address, + type PublicClient, +} from 'viem'; +import { + getHttpUrl, + loadStorageArtifact, + makeWalletClient, + rpc, + TEST_SENDER_ADDRESS, + TEST_SENDER_KEY, + NODE2_ADDRESS, + NODE2_KEY, +} from '../src/fixtures'; + +const artifact = loadStorageArtifact(); +const setEvent = artifact.abi.find( + (x) => x.type === 'event' && x.name === 'Set', +) as AbiEvent; +const tippedEvent = artifact.abi.find( + (x) => x.type === 'event' && x.name === 'Tipped', +) as AbiEvent; +const setTopic = toEventSelector('Set(address,uint256)'); +const tippedTopic = toEventSelector('Tipped(address,uint256)'); + +describe('Events — subscriptions & historical filters', () => { + // Poll fast so watch* actions resolve quickly (default is 4s). + let client: PublicClient; + let wallet: Awaited>; + let address: Address; + let deployBlock: bigint; + + before(async () => { + client = createPublicClient({ transport: http(getHttpUrl()), pollingInterval: 500 }); + wallet = await makeWalletClient(TEST_SENDER_KEY); + const hash = await wallet.deployContract({ abi: artifact.abi, bytecode: artifact.bytecode }); + const receipt = await client.waitForTransactionReceipt({ hash }); + address = receipt.contractAddress!; + deployBlock = receipt.blockNumber; + }); + + async function set(value: bigint, key = TEST_SENDER_KEY): Promise { + const w = await makeWalletClient(key); + const hash = await w.writeContract({ address, abi: artifact.abi, functionName: 'set', args: [value] }); + await client.waitForTransactionReceipt({ hash }); + } + + it('watchContractEvent("Set") fires when set() is called', async function () { + this.timeout(60_000); + const seen = new Promise<{ who: string; value: bigint }>((resolve) => { + const unwatch = client.watchContractEvent({ + address, + abi: artifact.abi, + eventName: 'Set', + onLogs: (logs) => { + const args = (logs[0] as { args: { who: string; value: bigint } }).args; + unwatch(); + resolve(args); + }, + }); + }); + await set(7n); + const ev = await seen; + expect(ev.who.toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + expect(ev.value).to.equal(7n); + }); + + it('getContractEvents returns historical Set logs since deploy', async () => { + await set(123n); + const events = await client.getContractEvents({ + address, + abi: artifact.abi, + eventName: 'Set', + fromBlock: deployBlock, + toBlock: 'latest', + }); + expect(events.length).to.be.greaterThan(0); + const last = events[events.length - 1] as { args: { who: string; value: bigint } }; + expect(last.args.value).to.equal(123n); + expect(last.args.who.toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + }); + + it('HTTP filter trio (createContractEventFilter / getFilterChanges / uninstallFilter)', async function () { + this.timeout(60_000); + const filter = await client.createContractEventFilter({ + address, + abi: artifact.abi, + eventName: 'Set', + fromBlock: await client.getBlockNumber(), + }); + + try { + await set(8675309n); + let changes: unknown[] = []; + const deadline = Date.now() + 30_000; + while (Date.now() < deadline) { + changes = await client.getFilterChanges({ filter }); + if (changes.length > 0) break; + await new Promise((r) => setTimeout(r, 500)); + } + expect(changes.length, 'getFilterChanges').to.be.greaterThan(0); + const log = changes[0] as { address: string; topics: string[] }; + expect(log.address.toLowerCase()).to.equal(address.toLowerCase()); + expect(log.topics[0]).to.equal(setTopic); + } finally { + const removed = await client.uninstallFilter({ filter }); + expect(removed, 'uninstallFilter').to.equal(true); + } + }); + + it('getFilterLogs returns the full matching log set for a contract-event filter', async function () { + this.timeout(60_000); + const filter = await client.createContractEventFilter({ + address, + abi: artifact.abi, + eventName: 'Set', + fromBlock: deployBlock, + }); + try { + await set(13579n); + let logs: unknown[] = []; + const deadline = Date.now() + 30_000; + while (Date.now() < deadline) { + logs = await client.getFilterLogs({ filter }); + if (logs.length > 0) break; + await new Promise((r) => setTimeout(r, 500)); + } + expect(logs.length, 'getFilterLogs').to.be.greaterThan(0); + const log = logs[0] as { topics: string[] }; + expect(log.topics[0]).to.equal(setTopic); + } finally { + await client.uninstallFilter({ filter }); + } + }); + + it('getLogs accepts address[] and OR-of-event / null-slot filter shapes', async function () { + this.timeout(90_000); + await set(2024n); + const tipWallet = await makeWalletClient(TEST_SENDER_KEY); + const tipHash = await tipWallet.writeContract({ + address, + abi: artifact.abi, + functionName: 'tip', + value: 5n, + }); + await client.waitForTransactionReceipt({ hash: tipHash }); + + // address[]: real contract + zero address. Every returned log belongs to the + // real contract. + const multiAddr = await client.getLogs({ + address: [address, zeroAddress], + event: setEvent, + fromBlock: deployBlock, + toBlock: 'latest', + }); + expect(multiAddr, 'multi-address result').to.be.an('array').and.length.greaterThan(0); + for (const l of multiAddr) { + expect(l.address.toLowerCase()).to.equal(address.toLowerCase()); + } + + // events: [Set, Tipped] — OR at topic position 0. + const orTopic = await client.getLogs({ + address, + events: [setEvent, tippedEvent], + fromBlock: deployBlock, + toBlock: 'latest', + }); + const sigs = new Set(orTopic.map((l) => l.topics[0])); + expect(sigs.has(setTopic), 'OR must include Set').to.equal(true); + expect(sigs.has(tippedTopic), 'OR must include Tipped').to.equal(true); + + // null-slot wildcard via raw eth_getLogs (topics: [setTopic, null]). + const nullSlot = (await rpc(client, 'eth_getLogs', [ + { fromBlock: '0x0', toBlock: 'latest', address, topics: [setTopic, null] }, + ])) as Array<{ topics: string[] }>; + expect(nullSlot.length, 'null-slot must match Set logs').to.be.greaterThan(0); + for (const l of nullSlot) { + expect(l.topics[0]).to.equal(setTopic); + } + }); + + it('getLogs with an indexed-arg filter matches only that address', async function () { + this.timeout(90_000); + await set(1001n); + await set(1002n, NODE2_KEY); + + const senderOnly = await client.getLogs({ + address, + event: setEvent, + args: { who: TEST_SENDER_ADDRESS }, + fromBlock: deployBlock, + toBlock: 'latest', + }); + const node2Only = await client.getLogs({ + address, + event: setEvent, + args: { who: NODE2_ADDRESS }, + fromBlock: deployBlock, + toBlock: 'latest', + }); + + const senderValues = senderOnly.map((l) => (l as { args: { value: bigint } }).args.value); + const node2Values = node2Only.map((l) => (l as { args: { value: bigint } }).args.value); + + expect(senderValues, 'sender-only').to.include(1001n); + expect(senderValues, 'sender-only').to.not.include(1002n); + expect(node2Values, 'node2-only').to.include(1002n); + expect(node2Values, 'node2-only').to.not.include(1001n); + }); +}); diff --git a/tests/eth_rpc/viem/test/provider.test.ts b/tests/eth_rpc/viem/test/provider.test.ts new file mode 100644 index 0000000..2794354 --- /dev/null +++ b/tests/eth_rpc/viem/test/provider.test.ts @@ -0,0 +1,295 @@ +import { expect } from 'chai'; +import { numberToHex, zeroAddress, type PublicClient } from 'viem'; +import { + getHttpUrl, + makePublicClient, + makeWalletClient, + rpc, + collectStrings, + TEST_SENDER_ADDRESS, + TEST_SENDER_KEY, + NODE2_ADDRESS, +} from '../src/fixtures'; +import { createPublicClient, http } from 'viem'; + +describe('Public client read-only RPC', () => { + let client: PublicClient; + before(() => { + client = makePublicClient(); + }); + + it('getBlockNumber returns a positive bigint', async () => { + const n = await client.getBlockNumber(); + expect(n).to.be.a('bigint'); + expect(n > 0n, `blockNumber was ${n}`).to.equal(true); + }); + + it('getBlock("latest") returns a block with expected fields', async () => { + const block = await client.getBlock({ blockTag: 'latest' }); + expect(block.number).to.be.a('bigint'); + expect(block.number! > 0n, 'block.number > 0').to.equal(true); + expect(block.hash).to.match(/^0x[0-9a-fA-F]{64}$/); + expect(block.parentHash).to.match(/^0x[0-9a-fA-F]{64}$/); + expect(block.timestamp).to.be.a('bigint'); + expect(block.timestamp > 0n, 'block.timestamp > 0').to.equal(true); + }); + + it('getBalance returns a positive bigint for the funded sender', async () => { + const bal = await client.getBalance({ address: TEST_SENDER_ADDRESS }); + expect(bal).to.be.a('bigint'); + expect(bal > 0n, `balance was ${bal}`).to.equal(true); + }); + + it('getTransactionCount returns a non-negative number', async () => { + const n = await client.getTransactionCount({ address: TEST_SENDER_ADDRESS }); + expect(n).to.be.a('number').and.to.be.at.least(0); + }); + + it('getCode for a non-contract address is empty', async () => { + const code = await client.getCode({ address: NODE2_ADDRESS }); + // viem returns undefined (or '0x') when there is no contract code. + expect(code === undefined || code === '0x').to.equal(true); + }); + + it('call returns empty data for a no-op call to an EOA', async () => { + const { data } = await client.call({ to: zeroAddress, data: '0x' }); + expect(data === undefined || data === '0x').to.equal(true); + }); + + it('estimateGas returns a positive bigint for a plain value transfer', async () => { + const gas = await client.estimateGas({ + account: TEST_SENDER_ADDRESS, + to: NODE2_ADDRESS, + value: 1n, + }); + expect(gas).to.be.a('bigint'); + expect(gas > 0n, `gas was ${gas}`).to.equal(true); + }); + + it('getChainId matches a direct eth_chainId call', async () => { + const fromAction = await client.getChainId(); + const direct = (await rpc(client, 'eth_chainId')) as string; + expect(BigInt(fromAction)).to.equal(BigInt(direct)); + }); + + it('getGasPrice returns a positive bigint', async () => { + const gp = await client.getGasPrice(); + expect(gp).to.be.a('bigint'); + expect(gp > 0n, 'gasPrice > 0').to.equal(true); + }); + + it('estimateFeesPerGas returns EIP-1559 fee fields', async () => { + const fees = await client.estimateFeesPerGas(); + expect(fees.maxFeePerGas, 'maxFeePerGas').to.be.a('bigint'); + expect(fees.maxPriorityFeePerGas, 'maxPriorityFeePerGas').to.be.a('bigint'); + }); + + it('getFeeHistory returns baseFee and gasUsedRatio (no percentiles)', async () => { + const fh = await client.getFeeHistory({ blockCount: 4, rewardPercentiles: [] }); + expect(fh.baseFeePerGas).to.be.an('array').and.length.greaterThan(0); + expect(fh.gasUsedRatio).to.be.an('array').and.length.greaterThan(0); + }); + + it('eth_getBlockReceipts returns the receipt array for the latest block', async () => { + const receipts = (await rpc(client, 'eth_getBlockReceipts', ['latest'])) as Array< + Record + >; + expect(receipts).to.be.an('array'); + for (const r of receipts) { + expect(r.blockHash, 'receipt.blockHash').to.match(/^0x[0-9a-fA-F]{64}$/); + expect(r.transactionHash, 'receipt.transactionHash').to.match(/^0x[0-9a-fA-F]{64}$/); + expect(r.status, 'receipt.status').to.match(/^0x[01]$/); + } + }); + + describe('tx & log lookups (after a real send)', () => { + let txHash: `0x${string}`; + let blockNumber: bigint; + + before(async () => { + const wallet = await makeWalletClient(TEST_SENDER_KEY); + txHash = await wallet.sendTransaction({ to: NODE2_ADDRESS, value: 1n }); + const receipt = await client.waitForTransactionReceipt({ hash: txHash }); + blockNumber = receipt.blockNumber; + }); + + it('getTransaction by hash returns the sent tx', async () => { + const tx = await client.getTransaction({ hash: txHash }); + expect(tx.hash).to.equal(txHash); + expect(tx.from.toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + expect(tx.to!.toLowerCase()).to.equal(NODE2_ADDRESS.toLowerCase()); + expect(tx.value).to.equal(1n); + }); + + it('getTransactionReceipt by hash returns a success receipt', async () => { + const r = await client.getTransactionReceipt({ hash: txHash }); + expect(r.status).to.equal('success'); + expect(r.blockNumber).to.equal(blockNumber); + }); + + it('getLogs returns an array for a known block range', async () => { + const logs = await client.getLogs({ fromBlock: blockNumber, toBlock: blockNumber }); + expect(logs).to.be.an('array'); + }); + + it('getStorageAt returns a zero slot for an empty EOA slot 0', async () => { + const slot = await client.getStorageAt({ address: NODE2_ADDRESS, slot: '0x0' }); + expect(slot).to.match(/^0x0+$/); + }); + }); + + describe('getBlock variants', () => { + it('getBlock() round-trips with the latest block', async () => { + const latest = await client.getBlock({ blockTag: 'latest' }); + const byHash = await client.getBlock({ blockHash: latest.hash! }); + expect(byHash.number).to.equal(latest.number); + expect(byHash.hash).to.equal(latest.hash); + }); + + it('getBlock() matches getBlock("latest") at the same height', async () => { + const latest = await client.getBlock({ blockTag: 'latest' }); + const byNumber = await client.getBlock({ blockNumber: latest.number! }); + expect(byNumber.hash).to.equal(latest.hash); + }); + + it('getBlock(includeTransactions) hydrates full tx objects when present', async () => { + const wallet = await makeWalletClient(TEST_SENDER_KEY); + const hash = await wallet.sendTransaction({ to: NODE2_ADDRESS, value: 1n }); + const receipt = await client.waitForTransactionReceipt({ hash }); + const block = await client.getBlock({ + blockNumber: receipt.blockNumber, + includeTransactions: true, + }); + expect(block.transactions.length).to.be.greaterThan(0); + const found = block.transactions.find( + (t) => typeof t === 'object' && t.hash === hash, + ); + expect(found, `prefetched tx ${hash}`).to.exist; + }); + }); + + describe('block tag handling', () => { + it('getBlock("earliest") returns the genesis block at #0', async () => { + const block = await client.getBlock({ blockTag: 'earliest' }); + expect(block.number).to.equal(0n); + }); + + it('getBlock("finalized") returns a block at or below latest', async () => { + const finalized = await client.getBlock({ blockTag: 'finalized' }); + const latest = await client.getBlock({ blockTag: 'latest' }); + expect(finalized.number! <= latest.number!, 'finalized <= latest').to.equal(true); + }); + + it('getBlock("safe") returns a block at or below latest', async () => { + const safe = await client.getBlock({ blockTag: 'safe' }); + const latest = await client.getBlock({ blockTag: 'latest' }); + expect(safe.number! <= latest.number!, 'safe <= latest').to.equal(true); + }); + + it('getBlock("pending") returns a block (Thor mirrors latest — no mempool)', async () => { + const pending = await client.getBlock({ blockTag: 'pending' }); + // Thor's pending tracks the head; number may be null on a true pending + // block in geth, but Thor returns a concrete block-shaped object. + expect(pending).to.be.an('object'); + }); + }); + + describe('node-side keystore path (expected absent on Thor)', () => { + it('eth_accounts returns an empty array (no unlocked keys)', async () => { + const accounts = (await rpc(client, 'eth_accounts')) as unknown[]; + expect(accounts).to.be.an('array'); + expect(accounts.length).to.equal(0); + }); + + it('eth_sendTransaction is rejected — no node-side signer to deliver to', async () => { + let caught: unknown; + try { + await rpc(client, 'eth_sendTransaction', [ + { from: TEST_SENDER_ADDRESS, to: NODE2_ADDRESS, value: '0x1' }, + ]); + } catch (err) { + caught = err; + } + expect(caught, 'expected eth_sendTransaction to be rejected').to.not.be.undefined; + expect(collectStrings(caught).join(' || ').length).to.be.greaterThan(0); + }); + }); + + describe('JSON-RPC batching (HTTP transport)', () => { + it('batch:true collates 3 concurrent reads into a single batched POST', async () => { + const batched = createPublicClient({ transport: http(getHttpUrl(), { batch: true }) }); + const [bn, chainId, gp] = await Promise.all([ + batched.getBlockNumber(), + batched.getChainId(), + batched.getGasPrice(), + ]); + expect(bn).to.be.a('bigint'); + expect(bn > 0n, 'blockNumber > 0').to.equal(true); + expect(chainId).to.be.a('number').and.greaterThan(0); + expect(gp).to.be.a('bigint'); + expect(gp > 0n, 'gasPrice > 0').to.equal(true); + }); + + it('raw 11-request batch exceeds Thor maxBatchRequests=10 and is rejected', async () => { + const batch = Array.from({ length: 11 }, (_, i) => ({ + jsonrpc: '2.0', + id: i, + method: 'eth_blockNumber', + params: [], + })); + const resp = await fetch(getHttpUrl(), { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(batch), + }); + const text = await resp.text(); + let parsed: unknown; + try { + parsed = JSON.parse(text); + } catch { + parsed = null; + } + const isErrorEnvelope = + parsed != null && + typeof parsed === 'object' && + !Array.isArray(parsed) && + 'error' in (parsed as Record); + const isAllSuccess = + Array.isArray(parsed) && + parsed.length === 11 && + (parsed as Array>).every((r) => 'result' in r && !('error' in r)); + expect( + !resp.ok || isErrorEnvelope || !isAllSuccess, + `expected Thor to reject an 11-request batch, got ok=${resp.ok} body=${text.slice(0, 200)}`, + ).to.equal(true); + }); + }); + + describe('EIP-1898 block reference forms (via raw request)', () => { + it('eth_getBalance accepts {blockNumber: tag} object form', async () => { + const bal = (await rpc(client, 'eth_getBalance', [ + TEST_SENDER_ADDRESS, + { blockNumber: 'latest' }, + ])) as string; + expect(bal).to.match(/^0x[0-9a-fA-F]+$/); + }); + + it('eth_getBalance accepts {blockHash: ...} object form', async () => { + const latest = await client.getBlock({ blockTag: 'latest' }); + const bal = (await rpc(client, 'eth_getBalance', [ + TEST_SENDER_ADDRESS, + { blockHash: latest.hash }, + ])) as string; + expect(bal).to.match(/^0x[0-9a-fA-F]+$/); + }); + + it('eth_getTransactionCount accepts {blockNumber: 0x} numeric object form', async () => { + const latestBn = await client.getBlockNumber(); + const n = (await rpc(client, 'eth_getTransactionCount', [ + TEST_SENDER_ADDRESS, + { blockNumber: numberToHex(latestBn) }, + ])) as string; + expect(n).to.match(/^0x[0-9a-fA-F]+$/); + }); + }); +}); diff --git a/tests/eth_rpc/viem/test/rpc-extra.test.ts b/tests/eth_rpc/viem/test/rpc-extra.test.ts new file mode 100644 index 0000000..401b5a1 --- /dev/null +++ b/tests/eth_rpc/viem/test/rpc-extra.test.ts @@ -0,0 +1,248 @@ +import { expect } from 'chai'; +import { numberToHex, type PublicClient } from 'viem'; +import { + makePublicClient, + makeWalletClient, + rpc, + collectStrings, + TEST_SENDER_ADDRESS, + TEST_SENDER_KEY, + NODE2_ADDRESS, +} from '../src/fixtures'; + +describe('Network & node info (supported on Thor)', () => { + let client: PublicClient; + before(() => { + client = makePublicClient(); + }); + + it('net_version equals the decimal chainId', async () => { + const netV = (await rpc(client, 'net_version')) as string; + const chainId = await client.getChainId(); + expect(BigInt(netV)).to.equal(BigInt(chainId)); + }); + + it('net_listening returns true', async () => { + expect(await rpc(client, 'net_listening')).to.equal(true); + }); + + it('net_peerCount returns a hex quantity', async () => { + expect(await rpc(client, 'net_peerCount')).to.match(/^0x[0-9a-fA-F]+$/); + }); + + it('web3_clientVersion returns a Thor/* string', async () => { + const v = (await rpc(client, 'web3_clientVersion')) as string; + expect(v).to.be.a('string').and.match(/thor/i); + }); + + it('eth_syncing returns false or a syncing object', async () => { + const s = await rpc(client, 'eth_syncing'); + expect(s === false || (typeof s === 'object' && s !== null)).to.equal(true); + }); +}); + +describe('Misc eth_* methods supported on Thor', () => { + let client: PublicClient; + before(() => { + client = makePublicClient(); + }); + + it('eth_coinbase returns the zero address (PoA)', async () => { + expect(await rpc(client, 'eth_coinbase')).to.match(/^0x0{40}$/); + }); + + it('eth_mining returns false (PoA)', async () => { + expect(await rpc(client, 'eth_mining')).to.equal(false); + }); + + it('eth_hashrate returns 0x0 (PoA)', async () => { + expect(await rpc(client, 'eth_hashrate')).to.match(/^0x0+$/); + }); + + it('eth_accounts returns an empty array (no node-side keystore)', async () => { + const accounts = (await rpc(client, 'eth_accounts')) as unknown[]; + expect(accounts).to.be.an('array').and.length(0); + }); + + it('eth_getUncleCountByBlockNumber returns 0x0', async () => { + expect(await rpc(client, 'eth_getUncleCountByBlockNumber', ['latest'])).to.match(/^0x0+$/); + }); + + it('eth_getUncleCountByBlockHash returns 0x0', async () => { + const latest = await client.getBlock({ blockTag: 'latest' }); + expect(await rpc(client, 'eth_getUncleCountByBlockHash', [latest.hash])).to.match(/^0x0+$/); + }); + + it('eth_getUncleByBlockNumberAndIndex returns null', async () => { + expect(await rpc(client, 'eth_getUncleByBlockNumberAndIndex', ['latest', '0x0'])).to.equal(null); + }); + + it('eth_getUncleByBlockHashAndIndex returns null', async () => { + const latest = await client.getBlock({ blockTag: 'latest' }); + expect(await rpc(client, 'eth_getUncleByBlockHashAndIndex', [latest.hash, '0x0'])).to.equal(null); + }); + + it('eth_newBlockFilter returns a filter id and uninstalls', async () => { + const id = (await rpc(client, 'eth_newBlockFilter')) as string; + expect(id).to.match(/^0x[0-9a-fA-F]+$/); + expect(await rpc(client, 'eth_uninstallFilter', [id])).to.equal(true); + }); + + it('eth_newPendingTransactionFilter returns a filter id and uninstalls', async () => { + const id = (await rpc(client, 'eth_newPendingTransactionFilter')) as string; + expect(id).to.match(/^0x[0-9a-fA-F]+$/); + expect(await rpc(client, 'eth_uninstallFilter', [id])).to.equal(true); + }); +}); + +describe('Block & transaction index methods (implemented on Thor)', () => { + let client: PublicClient; + let txHash: `0x${string}`; + let blockNumber: bigint; + let blockHash: string; + let txIndex: number; + + before(async () => { + client = makePublicClient(); + const wallet = await makeWalletClient(TEST_SENDER_KEY); + txHash = await wallet.sendTransaction({ to: NODE2_ADDRESS, value: 1n }); + const receipt = await client.waitForTransactionReceipt({ hash: txHash }); + blockNumber = receipt.blockNumber; + blockHash = receipt.blockHash; + txIndex = receipt.transactionIndex; + }); + + it('eth_getBlockTransactionCountByNumber matches the block tx array length', async () => { + const block = await client.getBlock({ blockNumber }); + const count = (await rpc(client, 'eth_getBlockTransactionCountByNumber', [ + numberToHex(blockNumber), + ])) as string; + expect(BigInt(count)).to.equal(BigInt(block.transactions.length)); + }); + + it('eth_getBlockTransactionCountByHash matches the block tx array length', async () => { + const block = await client.getBlock({ blockNumber }); + const count = (await rpc(client, 'eth_getBlockTransactionCountByHash', [blockHash])) as string; + expect(BigInt(count)).to.equal(BigInt(block.transactions.length)); + }); + + it('eth_getTransactionByBlockNumberAndIndex returns the sent tx at its index', async () => { + const t = (await rpc(client, 'eth_getTransactionByBlockNumberAndIndex', [ + numberToHex(blockNumber), + numberToHex(txIndex), + ])) as { hash: string; blockHash: string } | null; + expect(t, 'tx by (number,index)').to.not.be.null; + expect(t!.hash.toLowerCase()).to.equal(txHash.toLowerCase()); + expect(t!.blockHash.toLowerCase()).to.equal(blockHash.toLowerCase()); + }); + + it('eth_getTransactionByBlockHashAndIndex returns the sent tx at its index', async () => { + const t = (await rpc(client, 'eth_getTransactionByBlockHashAndIndex', [ + blockHash, + numberToHex(txIndex), + ])) as { hash: string } | null; + expect(t, 'tx by (hash,index)').to.not.be.null; + expect(t!.hash.toLowerCase()).to.equal(txHash.toLowerCase()); + }); + + it('eth_getTransactionByBlockNumberAndIndex returns null for an out-of-range index', async () => { + const t = await rpc(client, 'eth_getTransactionByBlockNumberAndIndex', [ + numberToHex(blockNumber), + '0xffff', + ]); + expect(t).to.equal(null); + }); +}); + +describe('eth_* methods NOT implemented by Thor (skipped until shipped)', () => { + let client: PublicClient; + before(() => { + client = makePublicClient(); + }); + + const notFound = (err: unknown): boolean => + /not found|not supported|unsupported|does not exist|not available/i.test( + collectStrings(err).join(' | '), + ); + + const unimplemented: Array<{ name: string; params: unknown[] }> = [ + { name: 'eth_getProof', params: [TEST_SENDER_ADDRESS, [], 'latest'] }, + { name: 'eth_createAccessList', params: [{ from: TEST_SENDER_ADDRESS, to: NODE2_ADDRESS }, 'latest'] }, + { name: 'eth_protocolVersion', params: [] }, + { name: 'eth_pendingTransactions', params: [] }, + { name: 'eth_sign', params: [TEST_SENDER_ADDRESS, '0x68656c6c6f'] }, + { name: 'eth_signTransaction', params: [{ from: TEST_SENDER_ADDRESS, to: NODE2_ADDRESS, value: '0x1' }] }, + { name: 'eth_getRawTransactionByHash', params: ['0x' + '00'.repeat(32)] }, + { name: 'debug_traceTransaction', params: ['0x' + '00'.repeat(32)] }, + { name: 'eth_blobBaseFee', params: [] }, + ]; + + for (const c of unimplemented) { + it(`${c.name} — skipped while unimplemented`, async function () { + let result: unknown; + let caught: unknown; + try { + result = await rpc(client, c.name, c.params); + } catch (err) { + caught = err; + } + if (caught !== undefined && notFound(caught)) { + this.skip(); + } + expect( + caught, + `${c.name} errored for a non-"not found" reason: ${collectStrings(caught).join(' | ')}`, + ).to.be.undefined; + expect(result, `${c.name} unexpectedly returned undefined without an error`).to.not.be.undefined; + }); + } + + it('getEnsAddress — skipped (Thor chain has no ENS registry)', async function () { + try { + const addr = await client.getEnsAddress({ name: 'vitalik.eth' }); + expect(addr === null || /^0x[0-9a-fA-F]{40}$/.test(addr)).to.equal(true); + } catch (err) { + // viem refuses ENS on a chain with no resolver, throwing a plain Error + // ("client chain not configured. universalResolverAddress is required."). + // Its message is non-enumerable, so fold it in alongside collectStrings. + const haystack = [collectStrings(err).join(' | '), String((err as Error)?.message ?? '')].join( + ' | ', + ); + if (/ens|universalresolver|chain not configured|does not support|unsupported/i.test(haystack)) { + this.skip(); + } + throw err; + } + }); +}); + +describe('Category-3 divergences from Ethereum (skipped until Thor aligns)', () => { + let client: PublicClient; + before(() => { + client = makePublicClient(); + }); + + // geth's eth_feeHistory returns a per-block × per-percentile reward matrix when + // called with rewardPercentiles. Thor (rpc/fees/handler.go) currently rejects + // the percentile form — "reward percentiles are not yet supported" — so a fee + // estimator that requests percentiles can't use it. We SKIP on that documented + // gap; if Thor ever ships it, the reward-matrix assertion keeps it honest. + it('getFeeHistory with rewardPercentiles returns a reward matrix (geth parity)', async function () { + let fh: { reward?: bigint[][] }; + try { + fh = await client.getFeeHistory({ + blockCount: 4, + rewardPercentiles: [25, 50, 75], + }); + } catch (err) { + if (/percentile|not yet supported/i.test(collectStrings(err).join(' | '))) { + this.skip(); + } + throw err; + } + expect(fh.reward, 'reward matrix').to.be.an('array').and.length.greaterThan(0); + for (const row of fh.reward!) { + expect(row, 'per-block reward row').to.be.an('array').and.length(3); + } + }); +}); diff --git a/tests/eth_rpc/viem/test/wallet.test.ts b/tests/eth_rpc/viem/test/wallet.test.ts new file mode 100644 index 0000000..3312e27 --- /dev/null +++ b/tests/eth_rpc/viem/test/wallet.test.ts @@ -0,0 +1,224 @@ +import { expect } from 'chai'; +import { + parseTransaction, + recoverMessageAddress, + recoverTypedDataAddress, +} from 'viem'; +import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'; +import { + makePublicClient, + makeWalletClient, + collectStrings, + TEST_SENDER_ADDRESS, + TEST_SENDER_KEY, + NODE2_ADDRESS, +} from '../src/fixtures'; + +describe('Wallet — sign & send EIP-1559 tx', () => { + it('sends 1 wei and receives a successful receipt with correct balance delta', async () => { + const client = makePublicClient(); + const wallet = await makeWalletClient(TEST_SENDER_KEY); + + const before = await client.getBalance({ address: NODE2_ADDRESS }); + const hash = await wallet.sendTransaction({ to: NODE2_ADDRESS, value: 1n }); + const receipt = await client.waitForTransactionReceipt({ hash }); + + expect(receipt.status).to.equal('success'); + expect(receipt.from.toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + expect(receipt.to!.toLowerCase()).to.equal(NODE2_ADDRESS.toLowerCase()); + expect(hash).to.match(/^0x[0-9a-fA-F]{64}$/); + + const after = await client.getBalance({ address: NODE2_ADDRESS }); + expect(after - before).to.equal(1n); + }); + + it('rejects an unfunded address with an insufficient-funds error', async () => { + const unfundedKey = ('0x' + '11'.repeat(32)) as `0x${string}`; + const wallet = await makeWalletClient(unfundedKey); + + let threw = false; + try { + await wallet.sendTransaction({ to: NODE2_ADDRESS, value: 1n }); + } catch { + threw = true; + } + expect(threw, 'expected unfunded send to throw').to.equal(true); + }); + + it('Legacy (type 0) transactions are rejected by Thor — only EIP-1559 is accepted', async () => { + const client = makePublicClient(); + const account = privateKeyToAccount(TEST_SENDER_KEY); + const chainId = await client.getChainId(); + const nonce = await client.getTransactionCount({ address: account.address }); + const gasPrice = await client.getGasPrice(); + + const serialized = await account.signTransaction({ + type: 'legacy', + to: NODE2_ADDRESS, + value: 1n, + gas: 21_000n, + gasPrice, + nonce, + chainId, + }); + + let caught: unknown; + try { + await client.sendRawTransaction({ serializedTransaction: serialized }); + } catch (err) { + caught = err; + } + expect(caught, 'expected legacy tx to be rejected').to.not.be.undefined; + const blob = collectStrings(caught).join(' || '); + expect(blob).to.match(/rlp|legacy|unsupported|expected List|coalesce|type/i); + }); + + it('EIP-2930 (type 1) access-list transactions are rejected by Thor', async () => { + const client = makePublicClient(); + const account = privateKeyToAccount(TEST_SENDER_KEY); + const chainId = await client.getChainId(); + const nonce = await client.getTransactionCount({ address: account.address }); + const gasPrice = await client.getGasPrice(); + + const serialized = await account.signTransaction({ + type: 'eip2930', + to: NODE2_ADDRESS, + value: 1n, + gas: 21_000n, + gasPrice, + nonce, + chainId, + accessList: [{ address: NODE2_ADDRESS, storageKeys: [] }], + }); + + let caught: unknown; + try { + await client.sendRawTransaction({ serializedTransaction: serialized }); + } catch (err) { + caught = err; + } + expect(caught, 'expected EIP-2930 tx to be rejected').to.not.be.undefined; + const blob = collectStrings(caught).join(' || '); + expect(blob).to.match(/rlp|access|unsupported|expected List|coalesce|type/i); + }); + + it('signMessage produces a signature that recoverMessageAddress recovers', async () => { + const wallet = await makeWalletClient(TEST_SENDER_KEY); + const message = 'hello thor'; + const signature = await wallet.signMessage({ message }); + expect(signature).to.match(/^0x[0-9a-fA-F]{130}$/); + + const recovered = await recoverMessageAddress({ message, signature }); + expect(recovered.toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + }); + + it('signTypedData (EIP-712) produces a signature that recoverTypedDataAddress recovers', async () => { + const client = makePublicClient(); + const wallet = await makeWalletClient(TEST_SENDER_KEY); + const chainId = await client.getChainId(); + + const domain = { + name: 'InterstellarTest', + version: '1', + chainId, + verifyingContract: '0x0000000000000000000000000000000000000000' as const, + }; + const types = { + Mail: [ + { name: 'from', type: 'address' }, + { name: 'to', type: 'address' }, + { name: 'contents', type: 'string' }, + ], + } as const; + const message = { + from: TEST_SENDER_ADDRESS, + to: NODE2_ADDRESS, + contents: 'hi', + }; + + const signature = await wallet.signTypedData({ + domain, + types, + primaryType: 'Mail', + message, + }); + expect(signature).to.match(/^0x[0-9a-fA-F]{130}$/); + + const recovered = await recoverTypedDataAddress({ + domain, + types, + primaryType: 'Mail', + message, + signature, + }); + expect(recovered.toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + }); + + it('prepareTransactionRequest fills nonce, gas, and EIP-1559 fee fields', async () => { + const wallet = await makeWalletClient(TEST_SENDER_KEY); + const request = await wallet.prepareTransactionRequest({ to: NODE2_ADDRESS, value: 1n }); + + expect(request.nonce).to.be.a('number').and.to.be.at.least(0); + expect(request.gas, 'gas').to.be.a('bigint'); + expect(request.maxFeePerGas, 'maxFeePerGas').to.be.a('bigint'); + expect(request.maxPriorityFeePerGas, 'maxPriorityFeePerGas').to.be.a('bigint'); + }); + + it('account.signTransaction produces a raw tx that parseTransaction maps back', async () => { + const client = makePublicClient(); + const account = privateKeyToAccount(TEST_SENDER_KEY); + const chainId = await client.getChainId(); + const nonce = await client.getTransactionCount({ address: account.address }); + const fees = await client.estimateFeesPerGas(); + + const serialized = await account.signTransaction({ + type: 'eip1559', + to: NODE2_ADDRESS, + value: 2n, + gas: 21_000n, + maxFeePerGas: fees.maxFeePerGas, + maxPriorityFeePerGas: fees.maxPriorityFeePerGas, + nonce, + chainId, + }); + expect(serialized).to.match(/^0x02[0-9a-fA-F]+$/); + + const parsed = parseTransaction(serialized); + expect(parsed.to!.toLowerCase()).to.equal(NODE2_ADDRESS.toLowerCase()); + expect(parsed.value).to.equal(2n); + expect(parsed.type).to.equal('eip1559'); + }); + + it('sendRawTransaction accepts an offline-signed raw tx and confirms it', async () => { + const client = makePublicClient(); + const account = privateKeyToAccount(TEST_SENDER_KEY); + const chainId = await client.getChainId(); + const nonce = await client.getTransactionCount({ address: account.address }); + const fees = await client.estimateFeesPerGas(); + + const before = await client.getBalance({ address: NODE2_ADDRESS }); + + const serialized = await account.signTransaction({ + type: 'eip1559', + to: NODE2_ADDRESS, + value: 3n, + gas: 21_000n, + maxFeePerGas: fees.maxFeePerGas, + maxPriorityFeePerGas: fees.maxPriorityFeePerGas, + nonce, + chainId, + }); + const hash = await client.sendRawTransaction({ serializedTransaction: serialized }); + const receipt = await client.waitForTransactionReceipt({ hash }); + expect(receipt.status).to.equal('success'); + + const after = await client.getBalance({ address: NODE2_ADDRESS }); + expect(after - before).to.equal(3n); + }); + + it('mnemonicToAccount produces a deterministic address from a mnemonic', () => { + const phrase = 'test test test test test test test test test test test junk'; + const account = mnemonicToAccount(phrase); + expect(account.address).to.equal('0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'); + }); +}); diff --git a/tests/eth_rpc/viem/test/websocket.test.ts b/tests/eth_rpc/viem/test/websocket.test.ts new file mode 100644 index 0000000..dd4fc16 --- /dev/null +++ b/tests/eth_rpc/viem/test/websocket.test.ts @@ -0,0 +1,144 @@ +import { expect } from 'chai'; +import { + getWsUrl, + loadStorageArtifact, + makePublicClient, + makeWsClient, + makeWalletClient, + TEST_SENDER_ADDRESS, + TEST_SENDER_KEY, + NODE2_ADDRESS, +} from '../src/fixtures'; + +// Minimal structural type for the Node 22+ global WebSocket — @types/node@20 +// doesn't declare it, so we reach for it through globalThis with our own shape. +interface RawWebSocket { + send(data: string): void; + close(): void; + addEventListener(type: 'open' | 'error', listener: () => void): void; + addEventListener(type: 'message', listener: (ev: { data: unknown }) => void): void; +} +const RawWebSocket = (globalThis as unknown as { + WebSocket: new (url: string) => RawWebSocket; +}).WebSocket; + +describe('WebSocket transport — eth_subscribe (newHeads / logs / pending)', () => { + it('getChainId round-trips over a WebSocket transport', async () => { + const ws = makeWsClient(); + const httpChainId = await makePublicClient().getChainId(); + const wsChainId = await ws.getChainId(); + expect(wsChainId).to.equal(httpChainId); + }); + + it('watchBlocks receives a newHeads notification over eth_subscribe', async function () { + this.timeout(60_000); + const ws = makeWsClient(); + const observed = await new Promise((resolve) => { + const unwatch = ws.watchBlocks({ + onBlock: (block) => { + unwatch(); + resolve(block.number!); + }, + }); + }); + expect(observed).to.be.a('bigint'); + expect(observed > 0n, 'block number > 0').to.equal(true); + }); + + it('watchContractEvent receives a Set log over eth_subscribe(logs)', async function () { + this.timeout(60_000); + // Deploy + emit via HTTP; the WS client only carries the subscription. + const http = makePublicClient(); + const wallet = await makeWalletClient(TEST_SENDER_KEY); + const artifact = loadStorageArtifact(); + const deployHash = await wallet.deployContract({ + abi: artifact.abi, + bytecode: artifact.bytecode, + }); + const deployReceipt = await http.waitForTransactionReceipt({ hash: deployHash }); + const address = deployReceipt.contractAddress!; + + const ws = makeWsClient(); + const seen = new Promise<{ who: string; value: bigint }>((resolve) => { + const unwatch = ws.watchContractEvent({ + address, + abi: artifact.abi, + eventName: 'Set', + onLogs: (logs) => { + unwatch(); + resolve((logs[0] as { args: { who: string; value: bigint } }).args); + }, + }); + }); + + const setHash = await wallet.writeContract({ + address, + abi: artifact.abi, + functionName: 'set', + args: [4242n], + }); + await http.waitForTransactionReceipt({ hash: setHash }); + + const ev = await seen; + expect(ev.who.toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + expect(ev.value).to.equal(4242n); + }); + + it('watchPendingTransactions receives a tx-hash notification', async function () { + this.timeout(60_000); + const ws = makeWsClient(); + const wallet = await makeWalletClient(TEST_SENDER_KEY); + + const seen = new Promise((resolve) => { + const unwatch = ws.watchPendingTransactions({ + onTransactions: (hashes) => { + unwatch(); + resolve(hashes[0]); + }, + }); + }); + + const hash = await wallet.sendTransaction({ to: NODE2_ADDRESS, value: 1n }); + const observed = await seen; + expect(observed).to.match(/^0x[0-9a-fA-F]{64}$/); + expect(observed.toLowerCase()).to.equal(hash.toLowerCase()); + }); + + it('eth_subscribe("syncing") over the socket — skip if Thor does not register it', async function () { + this.timeout(30_000); + // viem has no high-level syncing watcher, so drive a bare WebSocket and + // speak JSON-RPC directly. Thor's eth_eq_json_rpc branch implements the + // 'syncing' subtype; if it ever rejects, skip rather than fail. + const ws = new RawWebSocket(getWsUrl()); + let response: { result?: unknown; error?: unknown }; + try { + response = await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('eth_subscribe(syncing) timed out')), 15_000); + ws.addEventListener('open', () => { + ws.send( + JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_subscribe', params: ['syncing'] }), + ); + }); + ws.addEventListener('message', (ev: { data: unknown }) => { + clearTimeout(timer); + try { + resolve(JSON.parse(String(ev.data)) as { result?: unknown; error?: unknown }); + } catch (e) { + reject(e as Error); + } + }); + ws.addEventListener('error', () => { + clearTimeout(timer); + reject(new Error('websocket transport error')); + }); + }); + } finally { + ws.close(); + } + + if (response.error != null) { + this.skip(); + } + expect(response.result, 'subscription id').to.match(/^0x[0-9a-fA-F]+$/); + }); +}); diff --git a/tests/eth_rpc/viem/tsconfig.json b/tests/eth_rpc/viem/tsconfig.json new file mode 100644 index 0000000..7dce8c3 --- /dev/null +++ b/tests/eth_rpc/viem/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "node", + "ignoreDeprecations": "5.0", + "esModuleInterop": true, + "resolveJsonModule": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": false, + "sourceMap": true, + "outDir": "dist", + "types": ["node", "mocha"] + }, + "ts-node": { + "transpileOnly": true + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/tests/eth_rpc/viem/viem_test.go b/tests/eth_rpc/viem/viem_test.go new file mode 100644 index 0000000..d1e7587 --- /dev/null +++ b/tests/eth_rpc/viem/viem_test.go @@ -0,0 +1,38 @@ +// Thin Go wrapper that launches the mocha + viem v2 suite as part of +// `go test ./...`. The wrapper shares the network lifecycle with every other +// Go test package via helper.RunTestMain — when NODE_URL is exported (e.g. by +// `make test`), the existing network is reused; otherwise RunTestMain starts a +// fresh one. +// +// We invoke `npx mocha` directly (not `npm test`) to skip any pretest hook — +// network build work is already done by `make build-network` or by RunTestMain. + +package viem + +import ( + "os" + "os/exec" + "testing" + + "github.com/vechain/interstellar-e2e/tests/helper" +) + +var nodeURL string + +func TestMain(m *testing.M) { + os.Exit(helper.RunTestMain(m, &nodeURL, nil)) +} + +func TestViem(t *testing.T) { + if _, err := os.Stat("node_modules"); os.IsNotExist(err) { + t.Fatal("node_modules missing — run `make test` (auto-installs) or `npm ci` in tests/eth_rpc/viem/") + } + + cmd := exec.CommandContext(t.Context(), "npx", "mocha") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = append(os.Environ(), "NODE_URL="+nodeURL) + if err := cmd.Run(); err != nil { + t.Fatalf("viem mocha suite failed: %v", err) + } +} diff --git a/tests/eth_rpc/web3js/.gitignore b/tests/eth_rpc/web3js/.gitignore new file mode 100644 index 0000000..3c25e1e --- /dev/null +++ b/tests/eth_rpc/web3js/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.log diff --git a/tests/eth_rpc/web3js/.mocharc.cjs b/tests/eth_rpc/web3js/.mocharc.cjs new file mode 100644 index 0000000..693e14c --- /dev/null +++ b/tests/eth_rpc/web3js/.mocharc.cjs @@ -0,0 +1,8 @@ +module.exports = { + require: ['ts-node/register'], + extensions: ['ts'], + spec: ['test/**/*.test.ts'], + timeout: 120000, + reporter: 'spec', + exit: true, +}; diff --git a/tests/eth_rpc/web3js/README.md b/tests/eth_rpc/web3js/README.md new file mode 100644 index 0000000..7290747 --- /dev/null +++ b/tests/eth_rpc/web3js/README.md @@ -0,0 +1,48 @@ +# web3.js v4 compatibility tests + +Exercises Thor's Ethereum-compatible JSON-RPC (`/rpc`, HTTP + WebSocket) +through the [web3.js](https://docs.web3js.org) v4 client. The sibling +[`../ethersjs`](../ethersjs) suite covers the same surface via ethers v6; this +suite is the web3.js counterpart so both major JS clients are validated. + +## Running + +The suite is driven by a thin Go wrapper (`web3js_test.go`) so it runs as part +of the top-level `make test`, sharing the local three-node network lifecycle +with every other Go package (`helper.RunTestMain`). Dependencies install +automatically via the `web3js-deps` Make target. + +Run just this suite (network must already be up and `NODE_URL` exported): + +```sh +cd tests/eth_rpc/web3js +npm ci +NODE_URL=http://127.0.0.1:8131 npm test +``` + +## Layout + +| File | Surface | +|---|---| +| `test/provider.test.ts` | read-only RPC, fee history, block tags, EIP-1898, batching, node-side/EIP-4844 rejections | +| `test/wallet.test.ts` | offline EIP-1559 signing & send, legacy/EIP-2930 rejection, message signing & recover | +| `test/contract.test.ts` | deploy, `call`, reverts (string + custom error), estimateGas, CREATE2 parity, payable | +| `test/events.test.ts` | `getPastEvents`, HTTP filter trio (`eth_newFilter`/`getFilterChanges`/`uninstallFilter`), `getLogs` shapes | +| `test/websocket.test.ts` | `eth_subscribe` newHeads / logs / pendingTransactions, syncing-rejected, clean disconnect | +| `test/rpc-extra.test.ts` | net/version (supported) + compatibility probes for methods Thor's support is TBD on | + +## Thor-specific notes + +- **EIP-1559 only.** `eth_sendRawTransaction` accepts only type-2 envelopes; + legacy (type 0) and EIP-2930 (type 1) are rejected. Every state-changing + helper signs type-2 offline and submits via `sendSignedTransaction` + (`src/fixtures.ts: sendEip1559`) rather than web3.js wallet auto-signing, + which may emit a legacy type. +- **No node-side keystore.** `eth_accounts` is `[]`; `eth_sendTransaction`, + `personal_sign`, `eth_signTypedData_v4` are rejected. +- **No EIP-4844.** `eth_blobBaseFee` is rejected. +- **`eth_feeHistory`** rejects `rewardPercentiles`. +- **WS `syncing`** subscription is rejected (Thor implements only + newHeads / logs / newPendingTransactions). +- **Probes** in `rpc-extra.test.ts` log each method's observed status; tighten + them to a strict success/rejection assertion once Thor's behavior is settled. diff --git a/tests/eth_rpc/web3js/contracts/Create2Factory.json b/tests/eth_rpc/web3js/contracts/Create2Factory.json new file mode 100644 index 0000000..2c317ee --- /dev/null +++ b/tests/eth_rpc/web3js/contracts/Create2Factory.json @@ -0,0 +1,43 @@ +{ + "contractName": "Create2Factory", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "Deployed", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "initCode", + "type": "bytes" + } + ], + "name": "deploy", + "outputs": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x6080604052348015600f57600080fd5b506101b38061001f6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063cdcb760a14610030575b600080fd5b61004361003e3660046100ff565b61005f565b6040516001600160a01b03909116815260200160405180910390f35b6000604051828482378483826000f59150506001600160a01b0381166100bc5760405162461bcd60e51b815260206004820152600e60248201526d18dc99585d194c8819985a5b195960921b604482015260640160405180910390fd5b6040516001600160a01b03821681527ff40fcec21964ffb566044d083b4073f29f7f7929110ea19e1b3ebe375d89055e9060200160405180910390a19392505050565b60008060006040848603121561011457600080fd5b83359250602084013567ffffffffffffffff81111561013257600080fd5b8401601f8101861361014357600080fd5b803567ffffffffffffffff81111561015a57600080fd5b86602082840101111561016c57600080fd5b93966020919091019550929350505056fea264697066735822122063bb8db7af6a7c0d1a24b676438d45379658b1e0ba102228bcd89cff515b6ad964736f6c63430008230033" +} diff --git a/tests/eth_rpc/web3js/contracts/Create2Factory.sol b/tests/eth_rpc/web3js/contracts/Create2Factory.sol new file mode 100644 index 0000000..521624f --- /dev/null +++ b/tests/eth_rpc/web3js/contracts/Create2Factory.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract Create2Factory { + event Deployed(address addr); + + function deploy(bytes32 salt, bytes calldata initCode) external returns (address addr) { + assembly { + let memPtr := mload(0x40) + calldatacopy(memPtr, initCode.offset, initCode.length) + addr := create2(0, memPtr, initCode.length, salt) + } + require(addr != address(0), "create2 failed"); + emit Deployed(addr); + } +} diff --git a/tests/eth_rpc/web3js/contracts/Storage.json b/tests/eth_rpc/web3js/contracts/Storage.json new file mode 100644 index 0000000..380606f --- /dev/null +++ b/tests/eth_rpc/web3js/contracts/Storage.json @@ -0,0 +1,140 @@ +{ + "contractName": "Storage", + "abi": [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "given", + "type": "uint256" + } + ], + "name": "MustBeNonZero", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "who", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Set", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "who", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Tipped", + "type": "event" + }, + { + "inputs": [], + "name": "get", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "v", + "type": "uint256" + } + ], + "name": "set", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "v", + "type": "uint256" + } + ], + "name": "setStrict", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "v", + "type": "uint256" + } + ], + "name": "setStrictCustomError", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "tip", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "totalTipped", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "value", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x6080604052348015600f57600080fd5b506102648061001f6000396000f3fe6080604052600436106100705760003560e01c80635e170bd51161004e5780635e170bd5146100c757806360fe47b1146100e75780636d4ce63c14610107578063b814b7ed1461011c57600080fd5b80632755cd2d146100755780633fa4f2451461007f57806358dd18d3146100a7575b600080fd5b61007d61012f565b005b34801561008b57600080fd5b5061009560005481565b60405190815260200160405180910390f35b3480156100b357600080fd5b5061007d6100c2366004610215565b610166565b3480156100d357600080fd5b5061007d6100e2366004610215565b6101f1565b3480156100f357600080fd5b5061007d610102366004610215565b6101b4565b34801561011357600080fd5b50600054610095565b34801561012857600080fd5b5047610095565b60405134815233907f905516bf815c273f240e1d48d78ea7db3f1f0d00b912fc69522caf0ea70450a29060200160405180910390a2565b806000036101b45760405162461bcd60e51b815260206004820152601660248201527576616c7565206d757374206265206e6f6e2d7a65726f60501b60448201526064015b60405180910390fd5b600081905560405181815233907ffd28ec3ec2555238d8ad6f9faf3e4cd10e574ce7e7ef28b73caa53f9512f65b99060200160405180910390a250565b806000036101b45760405163251ed31d60e11b8152600481018290526024016101ab565b60006020828403121561022757600080fd5b503591905056fea2646970667358221220947e72bf8cff231dfcab3ec92546fb82ae35ca5ba5a041822cff8a7d7793e82364736f6c63430008230033" +} diff --git a/tests/eth_rpc/web3js/contracts/Storage.sol b/tests/eth_rpc/web3js/contracts/Storage.sol new file mode 100644 index 0000000..1b00926 --- /dev/null +++ b/tests/eth_rpc/web3js/contracts/Storage.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract Storage { + event Set(address indexed who, uint256 value); + + uint256 public value; + + function set(uint256 v) external { + value = v; + emit Set(msg.sender, v); + } + + function get() external view returns (uint256) { + return value; + } + + function setStrict(uint256 v) external { + require(v != 0, "value must be non-zero"); + value = v; + emit Set(msg.sender, v); + } + + error MustBeNonZero(uint256 given); + + function setStrictCustomError(uint256 v) external { + if (v == 0) revert MustBeNonZero(v); + value = v; + emit Set(msg.sender, v); + } + + event Tipped(address indexed who, uint256 amount); + + function tip() external payable { + emit Tipped(msg.sender, msg.value); + } + + function totalTipped() external view returns (uint256) { + return address(this).balance; + } +} diff --git a/tests/eth_rpc/web3js/package-lock.json b/tests/eth_rpc/web3js/package-lock.json new file mode 100644 index 0000000..87abb84 --- /dev/null +++ b/tests/eth_rpc/web3js/package-lock.json @@ -0,0 +1,2444 @@ +{ + "name": "interstellar-eth-rpc-web3js", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "interstellar-eth-rpc-web3js", + "version": "0.0.0", + "devDependencies": { + "@types/chai": "^4.3.16", + "@types/mocha": "^10.0.6", + "@types/node": "^20.12.7", + "chai": "^4.4.1", + "mocha": "^10.4.0", + "solc": "^0.8.26", + "ts-node": "^10.9.2", + "typescript": "^5.9.3", + "web3": "^4.16.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@ethereumjs/rlp": { + "version": "4.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@ethereumjs/rlp/-/rlp-4.0.1.tgz", + "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", + "dev": true, + "license": "MPL-2.0", + "bin": { + "rlp": "bin/rlp" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://mirrors.cloud.tencent.com/npm/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://mirrors.cloud.tencent.com/npm/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.3.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://mirrors.cloud.tencent.com/npm/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://mirrors.cloud.tencent.com/npm/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "4.3.20", + "resolved": "https://mirrors.cloud.tencent.com/npm/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://mirrors.cloud.tencent.com/npm/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://mirrors.cloud.tencent.com/npm/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/ws": { + "version": "8.5.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abitype": { + "version": "0.7.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/abitype/-/abitype-0.7.1.tgz", + "integrity": "sha512-VBkRHTDZf9Myaek/dO3yMmOzB/y2s3Zo6nVU7yaw1G+TvCHAjwaJzNGN9yo4K5D8bU/VZXKP1EJpRhFr862PlQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=4.9.4", + "zod": "^3 >=3.19.1" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://mirrors.cloud.tencent.com/npm/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://mirrors.cloud.tencent.com/npm/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/command-exists": { + "version": "1.2.9", + "resolved": "https://mirrors.cloud.tencent.com/npm/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-fetch": { + "version": "4.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/cross-fetch/-/cross-fetch-4.1.0.tgz", + "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ethereum-cryptography": { + "version": "2.2.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", + "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://mirrors.cloud.tencent.com/npm/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://mirrors.cloud.tencent.com/npm/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://mirrors.cloud.tencent.com/npm/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://mirrors.cloud.tencent.com/npm/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/solc": { + "version": "0.8.35", + "resolved": "https://mirrors.cloud.tencent.com/npm/solc/-/solc-0.8.35.tgz", + "integrity": "sha512-OaP/4zyoKRo2CjqZDxbtkeRlEo6MxP4FLCxntw1Agf9OSoecmwYKoFBSB34UcSKBFBucrTh3Mb0nRoJou62ibw==", + "dev": true, + "license": "MIT", + "dependencies": { + "command-exists": "^1.2.8", + "commander": "^8.1.0", + "follow-redirects": "^1.12.1", + "js-sha3": "0.8.0", + "memorystream": "^0.3.1", + "semver": "^5.5.0", + "tmp": "0.0.33" + }, + "bin": { + "solcjs": "solc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://mirrors.cloud.tencent.com/npm/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/web3": { + "version": "4.16.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/web3/-/web3-4.16.0.tgz", + "integrity": "sha512-SgoMSBo6EsJ5GFCGar2E/pR2lcR/xmUSuQ61iK6yDqzxmm42aPPxSqZfJz2z/UCR6pk03u77pU8TGV6lgMDdIQ==", + "dev": true, + "license": "LGPL-3.0", + "dependencies": { + "web3-core": "^4.7.1", + "web3-errors": "^1.3.1", + "web3-eth": "^4.11.1", + "web3-eth-abi": "^4.4.1", + "web3-eth-accounts": "^4.3.1", + "web3-eth-contract": "^4.7.2", + "web3-eth-ens": "^4.4.0", + "web3-eth-iban": "^4.0.7", + "web3-eth-personal": "^4.1.0", + "web3-net": "^4.1.0", + "web3-providers-http": "^4.2.0", + "web3-providers-ws": "^4.0.8", + "web3-rpc-methods": "^1.3.0", + "web3-rpc-providers": "^1.0.0-rc.4", + "web3-types": "^1.10.0", + "web3-utils": "^4.3.3", + "web3-validator": "^2.0.6" + }, + "engines": { + "node": ">=14.0.0", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-core": { + "version": "4.7.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/web3-core/-/web3-core-4.7.1.tgz", + "integrity": "sha512-9KSeASCb/y6BG7rwhgtYC4CvYY66JfkmGNEYb7q1xgjt9BWfkf09MJPaRyoyT5trdOxYDHkT9tDlypvQWaU8UQ==", + "dev": true, + "license": "LGPL-3.0", + "dependencies": { + "web3-errors": "^1.3.1", + "web3-eth-accounts": "^4.3.1", + "web3-eth-iban": "^4.0.7", + "web3-providers-http": "^4.2.0", + "web3-providers-ws": "^4.0.8", + "web3-types": "^1.10.0", + "web3-utils": "^4.3.3", + "web3-validator": "^2.0.6" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + }, + "optionalDependencies": { + "web3-providers-ipc": "^4.0.7" + } + }, + "node_modules/web3-errors": { + "version": "1.3.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/web3-errors/-/web3-errors-1.3.1.tgz", + "integrity": "sha512-w3NMJujH+ZSW4ltIZZKtdbkbyQEvBzyp3JRn59Ckli0Nz4VMsVq8aF1bLWM7A2kuQ+yVEm3ySeNU+7mSRwx7RQ==", + "dev": true, + "license": "LGPL-3.0", + "dependencies": { + "web3-types": "^1.10.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-eth": { + "version": "4.11.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/web3-eth/-/web3-eth-4.11.1.tgz", + "integrity": "sha512-q9zOkzHnbLv44mwgLjLXuyqszHuUgZWsQayD2i/rus2uk0G7hMn11bE2Q3hOVnJS4ws4VCtUznlMxwKQ+38V2w==", + "dev": true, + "license": "LGPL-3.0", + "dependencies": { + "setimmediate": "^1.0.5", + "web3-core": "^4.7.1", + "web3-errors": "^1.3.1", + "web3-eth-abi": "^4.4.1", + "web3-eth-accounts": "^4.3.1", + "web3-net": "^4.1.0", + "web3-providers-ws": "^4.0.8", + "web3-rpc-methods": "^1.3.0", + "web3-types": "^1.10.0", + "web3-utils": "^4.3.3", + "web3-validator": "^2.0.6" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-eth-abi": { + "version": "4.4.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/web3-eth-abi/-/web3-eth-abi-4.4.1.tgz", + "integrity": "sha512-60ecEkF6kQ9zAfbTY04Nc9q4eEYM0++BySpGi8wZ2PD1tw/c0SDvsKhV6IKURxLJhsDlb08dATc3iD6IbtWJmg==", + "dev": true, + "license": "LGPL-3.0", + "dependencies": { + "abitype": "0.7.1", + "web3-errors": "^1.3.1", + "web3-types": "^1.10.0", + "web3-utils": "^4.3.3", + "web3-validator": "^2.0.6" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-eth-accounts": { + "version": "4.3.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/web3-eth-accounts/-/web3-eth-accounts-4.3.1.tgz", + "integrity": "sha512-rTXf+H9OKze6lxi7WMMOF1/2cZvJb2AOnbNQxPhBDssKOllAMzLhg1FbZ4Mf3lWecWfN6luWgRhaeSqO1l+IBQ==", + "dev": true, + "license": "LGPL-3.0", + "dependencies": { + "@ethereumjs/rlp": "^4.0.1", + "crc-32": "^1.2.2", + "ethereum-cryptography": "^2.0.0", + "web3-errors": "^1.3.1", + "web3-types": "^1.10.0", + "web3-utils": "^4.3.3", + "web3-validator": "^2.0.6" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-eth-contract": { + "version": "4.7.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/web3-eth-contract/-/web3-eth-contract-4.7.2.tgz", + "integrity": "sha512-3ETqs2pMNPEAc7BVY/C3voOhTUeJdkf2aM3X1v+edbngJLHAxbvxKpOqrcO0cjXzC4uc2Q8Zpf8n8zT5r0eLnA==", + "dev": true, + "license": "LGPL-3.0", + "dependencies": { + "@ethereumjs/rlp": "^5.0.2", + "web3-core": "^4.7.1", + "web3-errors": "^1.3.1", + "web3-eth": "^4.11.1", + "web3-eth-abi": "^4.4.1", + "web3-types": "^1.10.0", + "web3-utils": "^4.3.3", + "web3-validator": "^2.0.6" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-eth-contract/node_modules/@ethereumjs/rlp": { + "version": "5.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/@ethereumjs/rlp/-/rlp-5.0.2.tgz", + "integrity": "sha512-DziebCdg4JpGlEqEdGgXmjqcFoJi+JGulUXwEjsZGAscAQ7MyD/7LE/GVCP29vEQxKc7AAwjT3A2ywHp2xfoCA==", + "dev": true, + "license": "MPL-2.0", + "bin": { + "rlp": "bin/rlp.cjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/web3-eth-ens": { + "version": "4.4.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/web3-eth-ens/-/web3-eth-ens-4.4.0.tgz", + "integrity": "sha512-DeyVIS060hNV9g8dnTx92syqvgbvPricE3MerCxe/DquNZT3tD8aVgFfq65GATtpCgDDJffO2bVeHp3XBemnSQ==", + "dev": true, + "license": "LGPL-3.0", + "dependencies": { + "@adraffy/ens-normalize": "^1.8.8", + "web3-core": "^4.5.0", + "web3-errors": "^1.2.0", + "web3-eth": "^4.8.0", + "web3-eth-contract": "^4.5.0", + "web3-net": "^4.1.0", + "web3-types": "^1.7.0", + "web3-utils": "^4.3.0", + "web3-validator": "^2.0.6" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-eth-iban": { + "version": "4.0.7", + "resolved": "https://mirrors.cloud.tencent.com/npm/web3-eth-iban/-/web3-eth-iban-4.0.7.tgz", + "integrity": "sha512-8weKLa9KuKRzibC87vNLdkinpUE30gn0IGY027F8doeJdcPUfsa4IlBgNC4k4HLBembBB2CTU0Kr/HAOqMeYVQ==", + "dev": true, + "license": "LGPL-3.0", + "dependencies": { + "web3-errors": "^1.1.3", + "web3-types": "^1.3.0", + "web3-utils": "^4.0.7", + "web3-validator": "^2.0.3" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-eth-personal": { + "version": "4.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/web3-eth-personal/-/web3-eth-personal-4.1.0.tgz", + "integrity": "sha512-RFN83uMuvA5cu1zIwwJh9A/bAj0OBxmGN3tgx19OD/9ygeUZbifOL06jgFzN0t+1ekHqm3DXYQM8UfHpXi7yDQ==", + "dev": true, + "license": "LGPL-3.0", + "dependencies": { + "web3-core": "^4.6.0", + "web3-eth": "^4.9.0", + "web3-rpc-methods": "^1.3.0", + "web3-types": "^1.8.0", + "web3-utils": "^4.3.1", + "web3-validator": "^2.0.6" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-net": { + "version": "4.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/web3-net/-/web3-net-4.1.0.tgz", + "integrity": "sha512-WWmfvHVIXWEoBDWdgKNYKN8rAy6SgluZ0abyRyXOL3ESr7ym7pKWbfP4fjApIHlYTh8tNqkrdPfM4Dyi6CA0SA==", + "dev": true, + "license": "LGPL-3.0", + "dependencies": { + "web3-core": "^4.4.0", + "web3-rpc-methods": "^1.3.0", + "web3-types": "^1.6.0", + "web3-utils": "^4.3.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-providers-http": { + "version": "4.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/web3-providers-http/-/web3-providers-http-4.2.0.tgz", + "integrity": "sha512-IPMnDtHB7dVwaB7/mMxAZzyq7d5ezfO1+Vw0bNfAeIi7gaDlJiggp85SdyAfOgov8AMUA/dyiY72kQ0KmjXKvQ==", + "dev": true, + "license": "LGPL-3.0", + "dependencies": { + "cross-fetch": "^4.0.0", + "web3-errors": "^1.3.0", + "web3-types": "^1.7.0", + "web3-utils": "^4.3.1" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-providers-ipc": { + "version": "4.0.7", + "resolved": "https://mirrors.cloud.tencent.com/npm/web3-providers-ipc/-/web3-providers-ipc-4.0.7.tgz", + "integrity": "sha512-YbNqY4zUvIaK2MHr1lQFE53/8t/ejHtJchrWn9zVbFMGXlTsOAbNoIoZWROrg1v+hCBvT2c9z8xt7e/+uz5p1g==", + "dev": true, + "license": "LGPL-3.0", + "optional": true, + "dependencies": { + "web3-errors": "^1.1.3", + "web3-types": "^1.3.0", + "web3-utils": "^4.0.7" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-providers-ws": { + "version": "4.0.8", + "resolved": "https://mirrors.cloud.tencent.com/npm/web3-providers-ws/-/web3-providers-ws-4.0.8.tgz", + "integrity": "sha512-goJdgata7v4pyzHRsg9fSegUG4gVnHZSHODhNnn6J93ykHkBI1nz4fjlGpcQLUMi4jAMz6SHl9Ibzs2jj9xqPw==", + "dev": true, + "license": "LGPL-3.0", + "dependencies": { + "@types/ws": "8.5.3", + "isomorphic-ws": "^5.0.0", + "web3-errors": "^1.2.0", + "web3-types": "^1.7.0", + "web3-utils": "^4.3.1", + "ws": "^8.17.1" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-rpc-methods": { + "version": "1.3.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/web3-rpc-methods/-/web3-rpc-methods-1.3.0.tgz", + "integrity": "sha512-/CHmzGN+IYgdBOme7PdqzF+FNeMleefzqs0LVOduncSaqsppeOEoskLXb2anSpzmQAP3xZJPaTrkQPWSJMORig==", + "dev": true, + "license": "LGPL-3.0", + "dependencies": { + "web3-core": "^4.4.0", + "web3-types": "^1.6.0", + "web3-validator": "^2.0.6" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-rpc-providers": { + "version": "1.0.0-rc.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/web3-rpc-providers/-/web3-rpc-providers-1.0.0-rc.4.tgz", + "integrity": "sha512-PXosCqHW0EADrYzgmueNHP3Y5jcSmSwH+Dkqvn7EYD0T2jcsdDAIHqk6szBiwIdhumM7gv9Raprsu/s/f7h1fw==", + "dev": true, + "license": "LGPL-3.0", + "dependencies": { + "web3-errors": "^1.3.1", + "web3-providers-http": "^4.2.0", + "web3-providers-ws": "^4.0.8", + "web3-types": "^1.10.0", + "web3-utils": "^4.3.3", + "web3-validator": "^2.0.6" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-types": { + "version": "1.10.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/web3-types/-/web3-types-1.10.0.tgz", + "integrity": "sha512-0IXoaAFtFc8Yin7cCdQfB9ZmjafrbP6BO0f0KT/khMhXKUpoJ6yShrVhiNpyRBo8QQjuOagsWzwSK2H49I7sbw==", + "dev": true, + "license": "LGPL-3.0", + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-utils": { + "version": "4.3.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/web3-utils/-/web3-utils-4.3.3.tgz", + "integrity": "sha512-kZUeCwaQm+RNc2Bf1V3BYbF29lQQKz28L0y+FA4G0lS8IxtJVGi5SeDTUkpwqqkdHHC7JcapPDnyyzJ1lfWlOw==", + "dev": true, + "license": "LGPL-3.0", + "dependencies": { + "ethereum-cryptography": "^2.0.0", + "eventemitter3": "^5.0.1", + "web3-errors": "^1.3.1", + "web3-types": "^1.10.0", + "web3-validator": "^2.0.6" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-validator": { + "version": "2.0.6", + "resolved": "https://mirrors.cloud.tencent.com/npm/web3-validator/-/web3-validator-2.0.6.tgz", + "integrity": "sha512-qn9id0/l1bWmvH4XfnG/JtGKKwut2Vokl6YXP5Kfg424npysmtRLe9DgiNBM9Op7QL/aSiaA0TVXibuIuWcizg==", + "dev": true, + "license": "LGPL-3.0", + "dependencies": { + "ethereum-cryptography": "^2.0.0", + "util": "^0.12.5", + "web3-errors": "^1.2.0", + "web3-types": "^1.6.0", + "zod": "^3.21.4" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://mirrors.cloud.tencent.com/npm/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://mirrors.cloud.tencent.com/npm/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://mirrors.cloud.tencent.com/npm/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://mirrors.cloud.tencent.com/npm/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/tests/eth_rpc/web3js/package.json b/tests/eth_rpc/web3js/package.json new file mode 100644 index 0000000..676b2da --- /dev/null +++ b/tests/eth_rpc/web3js/package.json @@ -0,0 +1,21 @@ +{ + "name": "interstellar-eth-rpc-web3js", + "private": true, + "version": "0.0.0", + "description": "web3.js v4 compatibility tests against Thor's Ethereum-compatible RPC.", + "scripts": { + "compile:contracts": "node scripts/compile.cjs", + "test": "mocha" + }, + "devDependencies": { + "@types/chai": "^4.3.16", + "@types/mocha": "^10.0.6", + "@types/node": "^20.12.7", + "chai": "^4.4.1", + "mocha": "^10.4.0", + "solc": "^0.8.26", + "ts-node": "^10.9.2", + "typescript": "^5.9.3", + "web3": "^4.16.0" + } +} diff --git a/tests/eth_rpc/web3js/scripts/compile.cjs b/tests/eth_rpc/web3js/scripts/compile.cjs new file mode 100644 index 0000000..69b7b8f --- /dev/null +++ b/tests/eth_rpc/web3js/scripts/compile.cjs @@ -0,0 +1,49 @@ +/* One-shot compile for every contracts/*.sol → contracts/.json. + * Run via `npm run compile:contracts`. The resulting JSON files are checked in, + * so the test suite has no solc dependency at runtime. */ +const fs = require('node:fs'); +const path = require('node:path'); +const solc = require('solc'); + +const root = path.join(__dirname, '..'); +const contractsDir = path.join(root, 'contracts'); + +const sources = {}; +for (const entry of fs.readdirSync(contractsDir)) { + if (entry.endsWith('.sol')) { + sources[entry] = { content: fs.readFileSync(path.join(contractsDir, entry), 'utf8') }; + } +} + +const input = { + language: 'Solidity', + sources, + settings: { + optimizer: { enabled: true, runs: 200 }, + evmVersion: 'paris', + outputSelection: { + '*': { '*': ['abi', 'evm.bytecode.object'] }, + }, + }, +}; + +const output = JSON.parse(solc.compile(JSON.stringify(input))); + +if (output.errors) { + const fatal = output.errors.filter((e) => e.severity === 'error'); + for (const e of output.errors) console.error(e.formattedMessage); + if (fatal.length > 0) process.exit(1); +} + +for (const [sourceFile, byName] of Object.entries(output.contracts)) { + for (const [contractName, contract] of Object.entries(byName)) { + const artifact = { + contractName, + abi: contract.abi, + bytecode: '0x' + contract.evm.bytecode.object, + }; + const outputPath = path.join(contractsDir, `${contractName}.json`); + fs.writeFileSync(outputPath, JSON.stringify(artifact, null, 2) + '\n'); + console.log(`wrote ${outputPath} (${artifact.bytecode.length / 2 - 1} bytes from ${sourceFile})`); + } +} diff --git a/tests/eth_rpc/web3js/src/fixtures.ts b/tests/eth_rpc/web3js/src/fixtures.ts new file mode 100644 index 0000000..b550bbd --- /dev/null +++ b/tests/eth_rpc/web3js/src/fixtures.ts @@ -0,0 +1,206 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { Web3 } from 'web3'; +import type { TransactionReceipt } from 'web3'; + +// NODE_URL is exported by the Go wrapper (tests/eth_rpc/web3js/web3js_test.go) +// which manages the network lifecycle via helper.RunTestMain. Running `npx mocha` +// or `npm test` directly requires the caller to export NODE_URL themselves. +export function getNodeUrl(): string { + const url = process.env.NODE_URL; + if (!url) { + throw new Error( + 'NODE_URL not set — run the suite via `go test` (which starts the network) or export NODE_URL manually', + ); + } + return url; +} + +// Pre-funded master accounts from LocalThreeNodesNetwork genesis. +// Mirrors tests/helper/client.go:16-21 and the ethersjs fixtures. +export const TEST_SENDER_KEY = + '0x01a4107bfb7d5141ec519e75788c34295741a1eefbfe460320efd2ada944071e'; +export const TEST_SENDER_ADDRESS = '0x61fF580B63D3845934610222245C116E013717ec'; + +export const NODE2_KEY = + '0x7072249b800ddac1d29a3cd06468cc1a917cbcd110dde358a905d03dad51748d'; +export const NODE2_ADDRESS = '0x327931085B4cCbCE0baABb5a5E1C678707C51d90'; + +export const NODE3_KEY = + '0xc55455943bf026dc44fcf189e8765eb0587c94e66029d580bae795386c0b737a'; +export const NODE3_ADDRESS = '0x084E48c8AE79656D7e27368AE5317b5c2D6a7497'; + +export function getHttpUrl(): string { + // Thor exposes the Ethereum-compatible JSON-RPC at /rpc — the bare + // URL returns 307. Mirrors tests/eth_rpc/eth_rpc_schema/rpc_test.go:71. + return getNodeUrl().replace(/\/$/, '') + '/rpc'; +} + +export function getWsUrl(): string { + // Thor accepts a WebSocket upgrade on the same /rpc path as HTTP POST + // (cmd/thor/httpserver/api_server.go — `router.PathPrefix("/rpc").Handler(rpcWs)`). + const base = getNodeUrl().replace(/\/$/, ''); + return base.replace(/^http/, 'ws') + '/rpc'; +} + +export function makeWeb3(): Web3 { + return new Web3(getHttpUrl()); +} + +export function makeWsWeb3(): Web3 { + // web3.js v4 auto-selects a WebSocketProvider when handed a ws:// URL. + return new Web3(getWsUrl()); +} + +// Raw JSON-RPC escape hatch — the web3.js analogue of ethers' provider.send(). +// Used for methods that lack a high-level wrapper (eth_getBlockReceipts, +// eth_uninstallFilter, EIP-1898 object forms, expected-rejection probes, ...). +export async function rpc(web3: Web3, method: string, params: unknown[] = []): Promise { + return web3.requestManager.send({ method, params }); +} + +export interface StorageArtifact { + contractName: string; + abi: import('web3').ContractAbi; + bytecode: string; +} + +export function loadStorageArtifact(): StorageArtifact { + return loadArtifact('Storage'); +} + +export function loadCreate2FactoryArtifact(): StorageArtifact { + return loadArtifact('Create2Factory'); +} + +function loadArtifact(name: string): StorageArtifact { + const artifactPath = path.join(__dirname, '..', 'contracts', `${name}.json`); + const raw = fs.readFileSync(artifactPath, 'utf8'); + return JSON.parse(raw) as StorageArtifact; +} + +export interface Eip1559Tx { + to?: string; + value?: bigint; + data?: string; + gas: bigint; +} + +// sendEip1559 offline-signs a type-2 transaction with `key` and submits it via +// eth_sendSignedTransaction (eth_sendRawTransaction), returning the mined +// receipt. Thor's Ethereum-compat RPC only accepts EIP-1559 envelopes, so every +// state-changing helper goes through this single deterministic path rather than +// web3.js's wallet auto-signing (which may emit a legacy type Thor would reject). +export async function sendEip1559( + web3: Web3, + key: string, + tx: Eip1559Tx, +): Promise { + const account = web3.eth.accounts.privateKeyToAccount(key); + const chainId = await web3.eth.getChainId(); + const nonce = await web3.eth.getTransactionCount(account.address, 'pending'); + const baseFee = await fetchBaseFee(web3); + const maxPriorityFeePerGas = 1n; + const maxFeePerGas = baseFee * 2n + maxPriorityFeePerGas; + + const signed = await account.signTransaction({ + to: tx.to, + value: tx.value ?? 0n, + data: tx.data, + gas: tx.gas, + nonce, + chainId, + maxFeePerGas, + maxPriorityFeePerGas, + type: 2, + }); + return web3.eth.sendSignedTransaction(signed.rawTransaction); +} + +// signEip1559Raw offline-signs a type-2 tx and returns the raw RLP hex without +// broadcasting — for the "broadcast an offline-signed tx" and parse-back cases. +export async function signEip1559Raw( + web3: Web3, + key: string, + tx: Eip1559Tx, +): Promise { + const account = web3.eth.accounts.privateKeyToAccount(key); + const chainId = await web3.eth.getChainId(); + const nonce = await web3.eth.getTransactionCount(account.address, 'pending'); + const baseFee = await fetchBaseFee(web3); + const maxPriorityFeePerGas = 1n; + const maxFeePerGas = baseFee * 2n + maxPriorityFeePerGas; + + const signed = await account.signTransaction({ + to: tx.to, + value: tx.value ?? 0n, + data: tx.data, + gas: tx.gas, + nonce, + chainId, + maxFeePerGas, + maxPriorityFeePerGas, + type: 2, + }); + return signed.rawTransaction; +} + +export async function fetchBaseFee(web3: Web3): Promise { + const block = await web3.eth.getBlock('latest'); + return block.baseFeePerGas ?? 0n; +} + +// deployContract deploys `artifact` from `key` via the EIP-1559 raw-tx path and +// returns { address, blockNumber } from the mined receipt. +export async function deployContract( + web3: Web3, + key: string, + artifact: StorageArtifact, +): Promise<{ address: string; blockNumber: bigint }> { + const account = web3.eth.accounts.privateKeyToAccount(key); + const deployTx = new web3.eth.Contract(artifact.abi).deploy({ data: artifact.bytecode }); + const data = deployTx.encodeABI(); + const gas = await deployTx.estimateGas({ from: account.address }); + const receipt = await sendEip1559(web3, key, { data, gas }); + return { + address: receipt.contractAddress as string, + blockNumber: receipt.blockNumber as bigint, + }; +} + +// contractSet submits a Storage.set(v) (or any single-uint256 setter) from `key` +// and waits for the receipt. Convenience for the events/websocket suites. +export async function contractSet( + web3: Web3, + key: string, + address: string, + abi: import('web3').ContractAbi, + value: bigint, +): Promise { + const account = web3.eth.accounts.privateKeyToAccount(key); + const contract = new web3.eth.Contract(abi, address); + // abi is the loose ContractAbi type, so method names aren't statically known. + const method = (contract.methods as Record { + estimateGas: (opts: { from: string }) => Promise; + encodeABI: () => string; + }>).set(value); + const gas = await method.estimateGas({ from: account.address }); + return sendEip1559(web3, key, { to: address, data: method.encodeABI(), gas }); +} + +// Collect every string-valued leaf of an unknown error/response — RPC error text +// can live at .message, .cause.message, .innerError, .data, etc. +export function collectStrings(obj: unknown, depth = 0): string[] { + const out: string[] = []; + if (depth > 5 || obj == null) return out; + if (typeof obj === 'string') { + out.push(obj); + return out; + } + if (typeof obj === 'object') { + for (const v of Object.values(obj as Record)) { + out.push(...collectStrings(v, depth + 1)); + } + } + return out; +} diff --git a/tests/eth_rpc/web3js/test/contract.test.ts b/tests/eth_rpc/web3js/test/contract.test.ts new file mode 100644 index 0000000..a17db3b --- /dev/null +++ b/tests/eth_rpc/web3js/test/contract.test.ts @@ -0,0 +1,210 @@ +import { expect } from 'chai'; +import { Web3 } from 'web3'; +import type { Contract } from 'web3-eth-contract'; +import { + collectStrings, + loadCreate2FactoryArtifact, + loadStorageArtifact, + makeWeb3, + sendEip1559, + StorageArtifact, + TEST_SENDER_ADDRESS, + TEST_SENDER_KEY, +} from '../src/fixtures'; + +describe('Contract — deploy & call via web3.eth.Contract', () => { + const artifact = loadStorageArtifact(); + let web3: Web3; + let contract: Contract; + let address: string; + + // Deploy `artifact` and return the new contract address (via the deterministic + // EIP-1559 raw-tx path; web3.js wallet auto-signing is avoided everywhere). + async function deploy(a: StorageArtifact): Promise { + const deployTx = new web3.eth.Contract(a.abi).deploy({ data: a.bytecode }); + const data = deployTx.encodeABI(); + const gas = await deployTx.estimateGas({ from: TEST_SENDER_ADDRESS }); + const receipt = await sendEip1559(web3, TEST_SENDER_KEY, { data, gas }); + expect(receipt.status, 'deploy receipt status').to.equal(1n); + return receipt.contractAddress as string; + } + + before(async () => { + web3 = makeWeb3(); + address = await deploy(artifact); + contract = new web3.eth.Contract(artifact.abi, address); + }); + + it('deploys to a non-empty contract address', async () => { + expect(address).to.match(/^0x[0-9a-fA-F]{40}$/); + const code = await web3.eth.getCode(address); + expect(code.length).to.be.greaterThan(2); + }); + + it('initial value() and get() both return 0n', async () => { + expect(await contract.methods.value().call()).to.equal(0n); + expect(await contract.methods.get().call()).to.equal(0n); + }); + + it('set(42) persists the value and the receipt status is 1', async () => { + const method = contract.methods.set(42n); + const gas = await method.estimateGas({ from: TEST_SENDER_ADDRESS }); + const receipt = await sendEip1559(web3, TEST_SENDER_KEY, { + to: address, + data: method.encodeABI(), + gas, + }); + expect(receipt.status).to.equal(1n); + expect(await contract.methods.get().call()).to.equal(42n); + expect(await contract.methods.value().call()).to.equal(42n); + }); + + it('setStrict(0) reverts with the declared reason (via eth_call)', async () => { + let caught: unknown; + try { + await contract.methods.setStrict(0n).call({ from: TEST_SENDER_ADDRESS }); + } catch (err) { + caught = err; + } + expect(caught, 'expected setStrict(0) to revert').to.not.be.undefined; + expect(collectStrings(caught).join(' || ')).to.match(/value must be non-zero|revert/i); + }); + + it('setStrictCustomError(0) reverts with a decoded custom error (via eth_call)', async () => { + let caught: unknown; + try { + await contract.methods.setStrictCustomError(0n).call({ from: TEST_SENDER_ADDRESS }); + } catch (err) { + caught = err; + } + expect(caught, 'expected custom-error revert').to.not.be.undefined; + const selector = web3.utils.keccak256('MustBeNonZero(uint256)').slice(0, 10); + const haystack = collectStrings(caught).join(' || '); + const found = haystack.includes('MustBeNonZero') || haystack.toLowerCase().includes(selector.slice(2).toLowerCase()); + expect(found, `no MustBeNonZero / ${selector} in error: ${haystack.slice(0, 200)}`).to.equal(true); + }); + + it('eth_call on a write function returns without sending a tx (no nonce change)', async () => { + const nonceBefore = await web3.eth.getTransactionCount(TEST_SENDER_ADDRESS); + await contract.methods.set(999n).call({ from: TEST_SENDER_ADDRESS }); + const nonceAfter = await web3.eth.getTransactionCount(TEST_SENDER_ADDRESS); + expect(nonceAfter).to.equal(nonceBefore); + // State unchanged — still 42 from the earlier set. + expect(await contract.methods.get().call()).to.equal(42n); + }); + + it('decodeLog decodes a Set event from a raw receipt log', async () => { + const method = contract.methods.set(99n); + const gas = await method.estimateGas({ from: TEST_SENDER_ADDRESS }); + const receipt = await sendEip1559(web3, TEST_SENDER_KEY, { + to: address, + data: method.encodeABI(), + gas, + }); + const logs = receipt.logs ?? []; + expect(logs.length).to.be.greaterThan(0); + + const setSig = web3.utils.keccak256('Set(address,uint256)'); + const setLog = logs.find((l) => (l.topics ?? [])[0] === setSig); + expect(setLog, 'Set log').to.not.be.undefined; + const decoded = web3.eth.abi.decodeLog( + [ + { indexed: true, name: 'who', type: 'address' }, + { indexed: false, name: 'value', type: 'uint256' }, + ], + setLog!.data as string, + (setLog!.topics as string[]).slice(1), + ); + expect(decoded.value).to.equal(99n); + expect((decoded.who as string).toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + }); + + it('contract..estimateGas returns a positive bigint via the method-level API', async () => { + const gas = await contract.methods.set(7n).estimateGas({ from: TEST_SENDER_ADDRESS }); + expect(gas).to.be.a('bigint'); + expect(gas > 0n, `gas was ${gas}`).to.equal(true); + }); + + it('contract..encodeABI returns ABI-encoded calldata with the right selector', async () => { + const data = contract.methods.set(99n).encodeABI(); + expect(data).to.match(/^0x[0-9a-fA-F]+$/); + // selector for set(uint256) = first 4 bytes of keccak256("set(uint256)") + expect(data.startsWith('0x60fe47b1'), 'selector mismatch').to.equal(true); + // selector (4B) + uint256 arg (32B) = 36B => 72 hex chars after 0x + expect(data.length).to.equal(2 + 4 * 2 + 32 * 2); + expect(data.toLowerCase()).to.match(/0+63$/, 'uint256 99 encoding'); + }); + + it('a fresh Contract handle bound to the same address reads current state', async () => { + const reattached = new web3.eth.Contract(artifact.abi, address); + expect(await reattached.methods.value().call()).to.equal(99n); + }); + + it('CREATE2 parity — locally computed address matches the on-chain deployed address', async function () { + this.timeout(90_000); + const factoryArtifact = loadCreate2FactoryArtifact(); + const factoryAddress = await deploy(factoryArtifact); + const factory = new web3.eth.Contract(factoryArtifact.abi, factoryAddress); + + const salt = web3.utils.randomHex(32); + const initCode = artifact.bytecode; // reuse Storage's deploy bytecode + const initCodeHash = web3.utils.keccak256(initCode); + const expected = create2Address(web3, factoryAddress, salt, initCodeHash); + + // Set gas explicitly: estimateGas undercounts CREATE2 + the inner Storage + // constructor in Thor's compat layer, leaving the inner create2 OOG. + const method = factory.methods.deploy(salt, initCode); + const receipt = await sendEip1559(web3, TEST_SENDER_KEY, { + to: factoryAddress, + data: method.encodeABI(), + gas: 3_000_000n, + }); + expect(receipt.status).to.equal(1n); + + const deployedSig = web3.utils.keccak256('Deployed(address)'); + const log = (receipt.logs ?? []).find((l) => (l.topics ?? [])[0] === deployedSig); + expect(log, 'Deployed event').to.not.be.undefined; + const onChainAddr = web3.eth.abi.decodeParameter('address', log!.data as string) as string; + + expect(onChainAddr.toLowerCase()).to.equal(expected.toLowerCase()); + const code = await web3.eth.getCode(onChainAddr); + expect(code.length).to.be.greaterThan(2); + }); + + it('payable tip() accepts value, increments contract balance, emits Tipped', async () => { + const balanceBefore = await web3.eth.getBalance(address); + + const method = contract.methods.tip(); + const gas = await method.estimateGas({ from: TEST_SENDER_ADDRESS, value: 1234n }); + const receipt = await sendEip1559(web3, TEST_SENDER_KEY, { + to: address, + data: method.encodeABI(), + value: 1234n, + gas, + }); + expect(receipt.status).to.equal(1n); + + const balanceAfter = await web3.eth.getBalance(address); + expect(balanceAfter - balanceBefore).to.equal(1234n); + expect(await contract.methods.totalTipped().call()).to.equal(balanceAfter); + + const tippedSig = web3.utils.keccak256('Tipped(address,uint256)'); + const log = (receipt.logs ?? []).find((l) => (l.topics ?? [])[0] === tippedSig); + expect(log, 'Tipped event').to.not.be.undefined; + const who = web3.eth.abi.decodeParameter('address', (log!.topics as string[])[1]) as string; + const amount = web3.eth.abi.decodeParameter('uint256', log!.data as string) as bigint; + expect(who.toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + expect(amount).to.equal(1234n); + }); +}); + +// CREATE2: address = keccak256(0xff ++ deployer ++ salt ++ keccak256(initCode))[12:] +function create2Address(web3: Web3, deployer: string, salt: string, initCodeHash: string): string { + const payload = '0xff' + strip0x(deployer) + strip0x(salt) + strip0x(initCodeHash); + const hash = web3.utils.keccak256(payload); + return web3.utils.toChecksumAddress('0x' + hash.slice(-40)); +} + +function strip0x(hex: string): string { + return hex.startsWith('0x') ? hex.slice(2) : hex; +} diff --git a/tests/eth_rpc/web3js/test/events.test.ts b/tests/eth_rpc/web3js/test/events.test.ts new file mode 100644 index 0000000..8742f6f --- /dev/null +++ b/tests/eth_rpc/web3js/test/events.test.ts @@ -0,0 +1,189 @@ +import { expect } from 'chai'; +import { Web3 } from 'web3'; +import { + contractSet, + deployContract, + loadStorageArtifact, + makeWeb3, + rpc, + sendEip1559, + NODE2_ADDRESS, + NODE2_KEY, + TEST_SENDER_ADDRESS, + TEST_SENDER_KEY, +} from '../src/fixtures'; + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + +describe('Events — historical filters & HTTP polling filters', () => { + const artifact = loadStorageArtifact(); + let web3: Web3; + let address: string; + let deployBlock: bigint; + let setTopic: string; + let tippedTopic: string; + + before(async () => { + web3 = makeWeb3(); + setTopic = web3.utils.keccak256('Set(address,uint256)'); + tippedTopic = web3.utils.keccak256('Tipped(address,uint256)'); + const d = await deployContract(web3, TEST_SENDER_KEY, artifact); + address = d.address; + deployBlock = d.blockNumber; + }); + + it('getPastEvents returns historical Set logs since deploy', async () => { + await contractSet(web3, TEST_SENDER_KEY, address, artifact.abi, 123n); + + const contract = new web3.eth.Contract(artifact.abi, address); + const events = await contract.getPastEvents('Set', { + fromBlock: deployBlock, + toBlock: 'latest', + }); + expect(events.length).to.be.greaterThan(0); + const last = events[events.length - 1] as { returnValues: Record }; + expect(last.returnValues.value).to.equal(123n); + expect((last.returnValues.who as string).toLowerCase()).to.equal( + TEST_SENDER_ADDRESS.toLowerCase(), + ); + }); + + it('HTTP filter trio (eth_newFilter / eth_getFilterChanges / eth_uninstallFilter)', async function () { + this.timeout(60_000); + + const fromBlock = '0x' + (await web3.eth.getBlockNumber()).toString(16); + const filterId = (await rpc(web3, 'eth_newFilter', [ + { fromBlock, toBlock: 'latest', address, topics: [setTopic] }, + ])) as string; + expect(filterId).to.match(/^0x[0-9a-fA-F]+$/); + + try { + await contractSet(web3, TEST_SENDER_KEY, address, artifact.abi, 8675309n); + + let changes: Array<{ topics: string[]; address: string }> = []; + const deadline = Date.now() + 30_000; + while (Date.now() < deadline) { + changes = (await rpc(web3, 'eth_getFilterChanges', [filterId])) as typeof changes; + if (changes.length > 0) break; + await new Promise((r) => setTimeout(r, 500)); + } + expect(changes.length, 'eth_getFilterChanges').to.be.greaterThan(0); + expect(changes[0].address.toLowerCase()).to.equal(address.toLowerCase()); + expect(changes[0].topics[0]).to.equal(setTopic); + } finally { + const removed = (await rpc(web3, 'eth_uninstallFilter', [filterId])) as boolean; + expect(removed, 'eth_uninstallFilter').to.equal(true); + } + }); + + it('eth_getFilterLogs returns the full matching log set for a log filter', async function () { + this.timeout(60_000); + // eth_getFilterLogs returns every log matching the filter (unlike + // eth_getFilterChanges, which is incremental since the last poll). + const fromBlock = '0x' + (await web3.eth.getBlockNumber()).toString(16); + const filterId = (await rpc(web3, 'eth_newFilter', [ + { fromBlock, toBlock: 'latest', address, topics: [setTopic] }, + ])) as string; + try { + await contractSet(web3, TEST_SENDER_KEY, address, artifact.abi, 13579n); + let logs: Array<{ topics: string[]; address: string }> = []; + const deadline = Date.now() + 30_000; + while (Date.now() < deadline) { + logs = (await rpc(web3, 'eth_getFilterLogs', [filterId])) as typeof logs; + if (logs.length > 0) break; + await new Promise((r) => setTimeout(r, 500)); + } + expect(logs.length, 'eth_getFilterLogs').to.be.greaterThan(0); + expect(logs[0].topics[0]).to.equal(setTopic); + expect(logs[0].address.toLowerCase()).to.equal(address.toLowerCase()); + } finally { + await rpc(web3, 'eth_uninstallFilter', [filterId]); + } + }); + + it('getLogs accepts address[] and OR-of-topic / null-slot filter shapes', async function () { + this.timeout(90_000); + // Emit one of each event so the OR-filter has matches on both signatures. + await contractSet(web3, TEST_SENDER_KEY, address, artifact.abi, 2024n); + const contract = new web3.eth.Contract(artifact.abi, address); + const tip = contract.methods.tip(); + const tipGas = await tip.estimateGas({ from: TEST_SENDER_ADDRESS, value: 5n }); + await sendEip1559(web3, TEST_SENDER_KEY, { + to: address, + data: tip.encodeABI(), + value: 5n, + gas: tipGas, + }); + + // address[]: real contract + zero address. Every returned log must still + // belong to the real contract. + const multiAddr = await web3.eth.getPastLogs({ + fromBlock: deployBlock, + toBlock: 'latest', + address: [address, ZERO_ADDRESS], + }); + expect(multiAddr, 'multi-address result').to.be.an('array').and.length.greaterThan(0); + for (const l of multiAddr) { + expect((l as { address: string }).address.toLowerCase()).to.equal(address.toLowerCase()); + } + + // topics: [[setTopic, tippedTopic]] — OR at position 0. + const orTopic = await web3.eth.getPastLogs({ + fromBlock: deployBlock, + toBlock: 'latest', + address, + topics: [[setTopic, tippedTopic]], + }); + const sigs = new Set((orTopic as Array<{ topics: string[] }>).map((l) => l.topics[0])); + expect(sigs.has(setTopic), 'OR-of-topic must include Set').to.equal(true); + expect(sigs.has(tippedTopic), 'OR-of-topic must include Tipped').to.equal(true); + + // topics: [setTopic, null] — wildcard on the indexed-sender slot. + const nullSlot = await web3.eth.getPastLogs({ + fromBlock: deployBlock, + toBlock: 'latest', + address, + topics: [setTopic, null], + }); + expect((nullSlot as unknown[]).length, 'null-slot must match Set logs').to.be.greaterThan(0); + for (const l of nullSlot as Array<{ topics: string[] }>) { + expect(l.topics[0]).to.equal(setTopic); + } + }); + + it('getPastEvents with an indexed-arg filter matches only that address', async function () { + this.timeout(120_000); + // Set from TEST_SENDER and from NODE2; filter who=TEST_SENDER must see only + // its own emission. + const senderReceipt = await contractSet(web3, TEST_SENDER_KEY, address, artifact.abi, 1001n); + const node2Receipt = await contractSet(web3, NODE2_KEY, address, artifact.abi, 1002n); + const fromBlock = + (senderReceipt.blockNumber as bigint) < (node2Receipt.blockNumber as bigint) + ? (senderReceipt.blockNumber as bigint) + : (node2Receipt.blockNumber as bigint); + + const contract = new web3.eth.Contract(artifact.abi, address); + const senderOnly = await contract.getPastEvents('Set', { + fromBlock, + toBlock: 'latest', + filter: { who: TEST_SENDER_ADDRESS }, + }); + const node2Only = await contract.getPastEvents('Set', { + fromBlock, + toBlock: 'latest', + filter: { who: NODE2_ADDRESS }, + }); + + const senderValues = (senderOnly as Array<{ returnValues: Record }>).map( + (e) => e.returnValues.value, + ); + const node2Values = (node2Only as Array<{ returnValues: Record }>).map( + (e) => e.returnValues.value, + ); + + expect(senderValues, 'sender-only').to.include(1001n); + expect(senderValues, 'sender-only').to.not.include(1002n); + expect(node2Values, 'node2-only').to.include(1002n); + expect(node2Values, 'node2-only').to.not.include(1001n); + }); +}); diff --git a/tests/eth_rpc/web3js/test/provider.test.ts b/tests/eth_rpc/web3js/test/provider.test.ts new file mode 100644 index 0000000..a8063d3 --- /dev/null +++ b/tests/eth_rpc/web3js/test/provider.test.ts @@ -0,0 +1,357 @@ +import { expect } from 'chai'; +import { Web3 } from 'web3'; +import { + collectStrings, + getHttpUrl, + makeWeb3, + rpc, + sendEip1559, + NODE2_ADDRESS, + TEST_SENDER_ADDRESS, + TEST_SENDER_KEY, +} from '../src/fixtures'; + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + +describe('web3.eth read-only RPC', () => { + let web3: Web3; + before(() => { + web3 = makeWeb3(); + }); + + it('getBlockNumber returns a positive bigint', async () => { + const n = await web3.eth.getBlockNumber(); + expect(n).to.be.a('bigint'); + expect(n > 0n, `blockNumber was ${n}`).to.equal(true); + }); + + it('getBlock("latest") returns a block with expected fields', async () => { + const block = await web3.eth.getBlock('latest'); + expect(block, 'latest block').to.not.be.undefined; + expect(block.number > 0n).to.equal(true); + expect(block.hash).to.match(/^0x[0-9a-fA-F]{64}$/); + expect(block.parentHash).to.match(/^0x[0-9a-fA-F]{64}$/); + expect(block.timestamp > 0n).to.equal(true); + }); + + it('getBalance returns a positive bigint for the funded sender', async () => { + const bal = await web3.eth.getBalance(TEST_SENDER_ADDRESS); + expect(bal).to.be.a('bigint'); + expect(bal > 0n, `balance was ${bal}`).to.equal(true); + }); + + it('getTransactionCount returns a non-negative bigint', async () => { + const n = await web3.eth.getTransactionCount(TEST_SENDER_ADDRESS); + expect(n).to.be.a('bigint'); + expect(n >= 0n).to.equal(true); + }); + + it('getCode for a non-contract address returns 0x', async () => { + const code = await web3.eth.getCode(NODE2_ADDRESS); + expect(code).to.equal('0x'); + }); + + it('call returns 0x for a no-op call to an EOA', async () => { + const result = await web3.eth.call({ to: ZERO_ADDRESS, data: '0x' }); + expect(result).to.equal('0x'); + }); + + it('estimateGas returns a positive bigint for a plain value transfer', async () => { + const gas = await web3.eth.estimateGas({ + from: TEST_SENDER_ADDRESS, + to: NODE2_ADDRESS, + value: 1n, + }); + expect(gas).to.be.a('bigint'); + expect(gas > 0n, `gas was ${gas}`).to.equal(true); + }); + + it('getChainId matches a direct eth_chainId call', async () => { + const fromApi = await web3.eth.getChainId(); + const direct = (await rpc(web3, 'eth_chainId')) as string; + expect(fromApi).to.equal(BigInt(direct)); + }); + + it('getGasPrice returns a positive bigint', async () => { + const gp = await web3.eth.getGasPrice(); + expect(gp).to.be.a('bigint'); + expect(gp > 0n, `gasPrice was ${gp}`).to.equal(true); + }); + + it('getMaxPriorityFeePerGas returns a bigint', async () => { + const tip = await web3.eth.getMaxPriorityFeePerGas(); + expect(tip).to.be.a('bigint'); + expect(tip >= 0n).to.equal(true); + }); + + it('eth_feeHistory returns baseFee and gasUsedRatio (no percentiles)', async () => { + // Thor accepts eth_feeHistory but rejects rewardPercentiles — call with []. + const raw = (await rpc(web3, 'eth_feeHistory', ['0x4', 'latest', []])) as { + oldestBlock: string; + baseFeePerGas: string[]; + gasUsedRatio: number[]; + }; + expect(raw, 'eth_feeHistory result').to.be.an('object'); + expect(raw.oldestBlock).to.match(/^0x[0-9a-fA-F]+$/); + expect(raw.baseFeePerGas).to.be.an('array').and.to.have.length.greaterThan(0); + expect(raw.gasUsedRatio).to.be.an('array').and.to.have.length.greaterThan(0); + }); + + it('eth_feeHistory with rewardPercentiles is rejected by Thor', async () => { + let caught: unknown; + try { + await rpc(web3, 'eth_feeHistory', ['0x4', 'latest', [25, 50, 75]]); + } catch (err) { + caught = err; + } + expect(caught, 'expected percentile request to be rejected').to.not.be.undefined; + const blob = collectStrings(caught).join(' || '); + expect(blob).to.match(/percentile|not yet supported|coalesce/i); + }); + + it('eth_getBlockReceipts returns the receipt array for the latest block', async () => { + const receipts = (await rpc(web3, 'eth_getBlockReceipts', ['latest'])) as Array< + Record + >; + expect(receipts).to.be.an('array'); + for (const r of receipts) { + expect(r.blockHash, 'receipt.blockHash').to.match(/^0x[0-9a-fA-F]{64}$/); + expect(r.transactionHash, 'receipt.transactionHash').to.match(/^0x[0-9a-fA-F]{64}$/); + expect(r.status, 'receipt.status').to.match(/^0x[01]$/); + } + }); + + describe('tx & log lookups (after a real send)', () => { + let txHash: string; + let blockNumber: bigint; + + before(async () => { + const receipt = await sendEip1559(web3, TEST_SENDER_KEY, { + to: NODE2_ADDRESS, + value: 1n, + gas: 21_000n, + }); + txHash = receipt.transactionHash as string; + blockNumber = receipt.blockNumber as bigint; + }); + + it('getTransaction by hash returns the sent tx', async () => { + const t = await web3.eth.getTransaction(txHash); + expect(t, 'tx lookup').to.not.be.undefined; + expect((t.hash as string).toLowerCase()).to.equal(txHash.toLowerCase()); + expect((t.from as string).toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + expect((t.to as string).toLowerCase()).to.equal(NODE2_ADDRESS.toLowerCase()); + expect(t.value).to.equal(1n); + }); + + it('getTransactionReceipt by hash returns a status-1 receipt', async () => { + const r = await web3.eth.getTransactionReceipt(txHash); + expect(r, 'receipt lookup').to.not.be.undefined; + expect(r.status).to.equal(1n); + expect(r.blockNumber).to.equal(blockNumber); + }); + + it('getLogs returns an array for a known block range', async () => { + // Plain value transfer emits no logs, but the call must succeed and + // return an array — non-empty cases are covered by the events suite. + const logs = await web3.eth.getPastLogs({ + fromBlock: blockNumber, + toBlock: blockNumber, + }); + expect(logs).to.be.an('array'); + }); + + it('getStorageAt returns 0x00..00 for an empty EOA slot 0', async () => { + const slot = await web3.eth.getStorageAt(NODE2_ADDRESS, 0); + expect(slot).to.match(/^0x0+$/); + }); + }); + + describe('getBlock variants', () => { + it('getBlock() round-trips with the latest block', async () => { + const latest = await web3.eth.getBlock('latest'); + const byHash = await web3.eth.getBlock(latest.hash as string); + expect(byHash.number).to.equal(latest.number); + expect(byHash.hash).to.equal(latest.hash); + }); + + it('getBlock() matches getBlock("latest") at the same height', async () => { + const latest = await web3.eth.getBlock('latest'); + const byNumber = await web3.eth.getBlock(latest.number); + expect(byNumber.hash).to.equal(latest.hash); + }); + + it('getBlock(, true) hydrates transaction objects when present', async () => { + const receipt = await sendEip1559(web3, TEST_SENDER_KEY, { + to: NODE2_ADDRESS, + value: 1n, + gas: 21_000n, + }); + const block = await web3.eth.getBlock(receipt.blockNumber, true); + expect(block.transactions, 'hydrated transactions').to.be.an('array').and.length.greaterThan(0); + const found = (block.transactions as Array>).find( + (tx) => (tx.hash as string).toLowerCase() === (receipt.transactionHash as string).toLowerCase(), + ); + expect(found, `hydrated tx ${receipt.transactionHash}`).to.exist; + expect((found!.from as string).toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + }); + }); + + describe('block tag handling', () => { + it('getBlock("earliest") returns the genesis block at #0', async () => { + const block = await web3.eth.getBlock('earliest'); + expect(block.number).to.equal(0n); + }); + + it('getBlock("finalized") returns a block at or below latest', async () => { + const finalized = await web3.eth.getBlock('finalized'); + const latest = await web3.eth.getBlock('latest'); + expect(finalized.number <= latest.number).to.equal(true); + }); + + it('getBlock("safe") returns a block at or below latest', async () => { + const safe = await web3.eth.getBlock('safe'); + const latest = await web3.eth.getBlock('latest'); + expect(safe.number <= latest.number).to.equal(true); + }); + + it('getBlock("pending") returns a block (Thor mirrors latest — no separate mempool)', async () => { + const pending = await web3.eth.getBlock('pending'); + expect(pending, 'pending block').to.not.be.undefined; + expect(pending.number >= 0n).to.equal(true); + }); + }); + + describe('node-side keystore path (expected absent on Thor)', () => { + it('getAccounts / eth_accounts returns an empty array (no unlocked keys)', async () => { + const accounts = await web3.eth.getAccounts(); + expect(accounts).to.be.an('array'); + expect(accounts.length).to.equal(0); + }); + + it('eth_sendTransaction is rejected — no node-side signer to deliver to', async () => { + let caught: unknown; + try { + await rpc(web3, 'eth_sendTransaction', [ + { from: TEST_SENDER_ADDRESS, to: NODE2_ADDRESS, value: '0x1' }, + ]); + } catch (err) { + caught = err; + } + expect(caught, 'expected eth_sendTransaction to be rejected').to.not.be.undefined; + expect(collectStrings(caught).join(' || ').length).to.be.greaterThan(0); + }); + + it('personal_sign is rejected — no node-side keys to sign with', async () => { + let caught: unknown; + try { + await rpc(web3, 'personal_sign', ['0x68656c6c6f', TEST_SENDER_ADDRESS]); + } catch (err) { + caught = err; + } + expect(caught, 'expected personal_sign to be rejected').to.not.be.undefined; + }); + }); + + describe('EIP-4844 / blob fees (expected unsupported on Thor)', () => { + it('eth_blobBaseFee is rejected — Thor has not implemented EIP-4844', async () => { + let caught: unknown; + try { + await rpc(web3, 'eth_blobBaseFee', []); + } catch (err) { + caught = err; + } + expect(caught, 'expected eth_blobBaseFee to be rejected').to.not.be.undefined; + expect(collectStrings(caught).join(' || ').length).to.be.greaterThan(0); + }); + }); + + describe('JSON-RPC batching (HTTP)', () => { + it('web3.BatchRequest carries 3 concurrent reads in a single batched POST', async () => { + const batch = new web3.BatchRequest(); + const pBlock = batch.add({ method: 'eth_blockNumber', params: [] }); + const pChain = batch.add({ method: 'eth_chainId', params: [] }); + const pGas = batch.add({ method: 'eth_gasPrice', params: [] }); + await batch.execute(); + const [bn, chainId, gp] = await Promise.all([pBlock, pChain, pGas]); + expect(bn as string).to.match(/^0x[0-9a-fA-F]+$/); + expect(chainId as string).to.match(/^0x[0-9a-fA-F]+$/); + expect(gp as string).to.match(/^0x[0-9a-fA-F]+$/); + }); + + it('raw 11-request batch exceeds Thor maxBatchRequests=10 and the whole batch is rejected', async () => { + // Thor's HTTP jsonrpc dispatcher caps batches at 10 requests. An 11-request + // batch posted raw must NOT come back as 11 successful results. + const batch = Array.from({ length: 11 }, (_, i) => ({ + jsonrpc: '2.0', + id: i, + method: 'eth_blockNumber', + params: [], + })); + const resp = await fetch(getHttpUrl(), { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(batch), + }); + const text = await resp.text(); + let parsed: unknown; + try { + parsed = JSON.parse(text); + } catch { + parsed = null; + } + + const okHttp = resp.ok; + const isErrorEnvelope = + parsed != null && + typeof parsed === 'object' && + !Array.isArray(parsed) && + 'error' in (parsed as Record); + const isAllSuccess = + Array.isArray(parsed) && + (parsed as unknown[]).length === 11 && + (parsed as Array>).every((r) => 'result' in r && !('error' in r)); + + expect( + !okHttp || isErrorEnvelope || !isAllSuccess, + `expected Thor to reject an 11-request batch, got ok=${okHttp} body=${text.slice(0, 200)}`, + ).to.equal(true); + }); + }); + + describe('EIP-1898 block reference forms', () => { + it('eth_getBalance accepts {blockNumber: tag} object form', async () => { + const bal = (await rpc(web3, 'eth_getBalance', [ + TEST_SENDER_ADDRESS, + { blockNumber: 'latest' }, + ])) as string; + expect(bal).to.match(/^0x[0-9a-fA-F]+$/); + }); + + it('eth_getBalance accepts {blockHash: ...} object form', async () => { + const latest = await web3.eth.getBlock('latest'); + const bal = (await rpc(web3, 'eth_getBalance', [ + TEST_SENDER_ADDRESS, + { blockHash: latest.hash }, + ])) as string; + expect(bal).to.match(/^0x[0-9a-fA-F]+$/); + }); + + it('eth_call accepts {blockNumber: tag} object form', async () => { + const result = (await rpc(web3, 'eth_call', [ + { to: ZERO_ADDRESS, data: '0x' }, + { blockNumber: 'latest' }, + ])) as string; + expect(result).to.equal('0x'); + }); + + it('eth_getTransactionCount accepts {blockNumber: 0x} numeric object form', async () => { + const latestBn = await web3.eth.getBlockNumber(); + const n = (await rpc(web3, 'eth_getTransactionCount', [ + TEST_SENDER_ADDRESS, + { blockNumber: '0x' + latestBn.toString(16) }, + ])) as string; + expect(n).to.match(/^0x[0-9a-fA-F]+$/); + }); + }); +}); diff --git a/tests/eth_rpc/web3js/test/rpc-extra.test.ts b/tests/eth_rpc/web3js/test/rpc-extra.test.ts new file mode 100644 index 0000000..14a56e7 --- /dev/null +++ b/tests/eth_rpc/web3js/test/rpc-extra.test.ts @@ -0,0 +1,244 @@ +import { expect } from 'chai'; +import { Web3 } from 'web3'; +import { + collectStrings, + makeWeb3, + rpc, + sendEip1559, + NODE2_ADDRESS, + TEST_SENDER_ADDRESS, + TEST_SENDER_KEY, +} from '../src/fixtures'; + +describe('Network & node info (supported on Thor)', () => { + let web3: Web3; + before(() => { + web3 = makeWeb3(); + }); + + it('net.getId returns a network id', async () => { + const id = await web3.eth.net.getId(); + expect(id).to.be.a('bigint'); + expect(id >= 0n).to.equal(true); + }); + + it('net.isListening returns true', async () => { + const listening = await web3.eth.net.isListening(); + expect(listening).to.equal(true); + }); + + it('net.getPeerCount returns a non-negative count', async () => { + const peers = await web3.eth.net.getPeerCount(); + expect(peers >= 0n).to.equal(true); + }); + + it('getNodeInfo (web3_clientVersion) returns a non-empty string', async () => { + const info = await web3.eth.getNodeInfo(); + expect(info).to.be.a('string').and.length.greaterThan(0); + }); + + it('isSyncing (eth_syncing) returns a boolean or a syncing object', async () => { + const syncing = await web3.eth.isSyncing(); + expect(['boolean', 'object']).to.include(typeof syncing); + }); +}); + +describe('Misc eth_* methods supported on Thor', () => { + let web3: Web3; + before(() => { + web3 = makeWeb3(); + }); + + it('eth_coinbase returns an address (zero on this PoA network)', async () => { + const cb = (await rpc(web3, 'eth_coinbase')) as string; + expect(cb).to.match(/^0x[0-9a-fA-F]{40}$/); + }); + + it('eth_mining returns a boolean', async () => { + const mining = await rpc(web3, 'eth_mining'); + expect(mining).to.be.a('boolean'); + }); + + it('eth_hashrate returns a hex quantity', async () => { + const hr = (await rpc(web3, 'eth_hashrate')) as string; + expect(hr).to.match(/^0x[0-9a-fA-F]+$/); + }); + + it('eth_getUncleCountByBlockNumber returns 0x0 (Thor has no uncles)', async () => { + const count = (await rpc(web3, 'eth_getUncleCountByBlockNumber', ['latest'])) as string; + expect(count).to.match(/^0x0+$/); + }); + + it('eth_getUncleCountByBlockHash returns 0x0 (Thor has no uncles)', async () => { + const latest = await web3.eth.getBlock('latest'); + const count = (await rpc(web3, 'eth_getUncleCountByBlockHash', [latest.hash])) as string; + expect(count).to.match(/^0x0+$/); + }); + + it('eth_getUncleByBlockNumberAndIndex returns null', async () => { + const uncle = await rpc(web3, 'eth_getUncleByBlockNumberAndIndex', ['latest', '0x0']); + expect(uncle).to.equal(null); + }); + + it('eth_getUncleByBlockHashAndIndex returns null', async () => { + const latest = await web3.eth.getBlock('latest'); + const uncle = await rpc(web3, 'eth_getUncleByBlockHashAndIndex', [latest.hash, '0x0']); + expect(uncle).to.equal(null); + }); + + it('eth_newBlockFilter returns a filter id', async () => { + const id = (await rpc(web3, 'eth_newBlockFilter')) as string; + expect(id).to.match(/^0x[0-9a-fA-F]+$/); + await rpc(web3, 'eth_uninstallFilter', [id]); + }); + + it('eth_newPendingTransactionFilter returns a filter id', async () => { + const id = (await rpc(web3, 'eth_newPendingTransactionFilter')) as string; + expect(id).to.match(/^0x[0-9a-fA-F]+$/); + await rpc(web3, 'eth_uninstallFilter', [id]); + }); +}); + +describe('Block & transaction index methods (implemented on Thor)', () => { + let web3: Web3; + let txHash: string; + let blockNumber: bigint; + let blockHash: string; + let txIndex: bigint; + + before(async () => { + web3 = makeWeb3(); + const receipt = await sendEip1559(web3, TEST_SENDER_KEY, { + to: NODE2_ADDRESS, + value: 1n, + gas: 21_000n, + }); + txHash = receipt.transactionHash as string; + blockNumber = receipt.blockNumber as bigint; + blockHash = receipt.blockHash as string; + txIndex = receipt.transactionIndex as bigint; + }); + + it('eth_getBlockTransactionCountByNumber matches the block tx array length', async () => { + const block = await web3.eth.getBlock(blockNumber, false); + const count = (await rpc(web3, 'eth_getBlockTransactionCountByNumber', [ + '0x' + blockNumber.toString(16), + ])) as string; + expect(count).to.match(/^0x[0-9a-fA-F]+$/); + expect(BigInt(count)).to.equal(BigInt(block.transactions.length)); + }); + + it('eth_getBlockTransactionCountByHash matches the block tx array length', async () => { + const block = await web3.eth.getBlock(blockNumber, false); + const count = (await rpc(web3, 'eth_getBlockTransactionCountByHash', [blockHash])) as string; + expect(count).to.match(/^0x[0-9a-fA-F]+$/); + expect(BigInt(count)).to.equal(BigInt(block.transactions.length)); + }); + + it('eth_getTransactionByBlockNumberAndIndex returns the sent tx at its index', async () => { + const t = (await rpc(web3, 'eth_getTransactionByBlockNumberAndIndex', [ + '0x' + blockNumber.toString(16), + '0x' + txIndex.toString(16), + ])) as { hash: string; blockHash: string } | null; + expect(t, 'tx by (number,index)').to.not.be.null; + expect(t!.hash.toLowerCase()).to.equal(txHash.toLowerCase()); + expect(t!.blockHash.toLowerCase()).to.equal(blockHash.toLowerCase()); + }); + + it('eth_getTransactionByBlockHashAndIndex returns the sent tx at its index', async () => { + const t = (await rpc(web3, 'eth_getTransactionByBlockHashAndIndex', [ + blockHash, + '0x' + txIndex.toString(16), + ])) as { hash: string } | null; + expect(t, 'tx by (hash,index)').to.not.be.null; + expect(t!.hash.toLowerCase()).to.equal(txHash.toLowerCase()); + }); + + it('eth_getTransactionByBlockNumberAndIndex returns null for an out-of-range index', async () => { + const t = await rpc(web3, 'eth_getTransactionByBlockNumberAndIndex', [ + '0x' + blockNumber.toString(16), + '0xffff', + ]); + expect(t).to.equal(null); + }); +}); + +describe('eth_* methods NOT implemented by Thor (skipped until shipped)', () => { + let web3: Web3; + before(() => { + web3 = makeWeb3(); + }); + + const notFound = (err: unknown): boolean => + /not found|not supported|unsupported|does not exist|not available/i.test( + collectStrings(err).join(' | '), + ); + + // Standard Ethereum methods thor's pedro/eth_eq_json_rpc dispatcher does NOT + // register. Each attempts the call and skips while it 404s at the method + // level; if Thor ever registers one, the success path keeps it honest, and a + // non-"not found" error fails loudly. + const unimplemented: Array<{ name: string; params: unknown[] }> = [ + { name: 'eth_getProof', params: [TEST_SENDER_ADDRESS, [], 'latest'] }, + { name: 'eth_createAccessList', params: [{ from: TEST_SENDER_ADDRESS, to: NODE2_ADDRESS }, 'latest'] }, + { name: 'eth_protocolVersion', params: [] }, + { name: 'eth_pendingTransactions', params: [] }, + { name: 'eth_sign', params: [TEST_SENDER_ADDRESS, '0x68656c6c6f'] }, + { name: 'eth_signTransaction', params: [{ from: TEST_SENDER_ADDRESS, to: NODE2_ADDRESS, value: '0x1' }] }, + { name: 'eth_getRawTransactionByHash', params: ['0x' + '00'.repeat(32)] }, + { name: 'debug_traceTransaction', params: ['0x' + '00'.repeat(32)] }, + ]; + + for (const c of unimplemented) { + it(`${c.name} — skipped while unimplemented`, async function () { + let result: unknown; + let caught: unknown; + try { + result = await rpc(web3, c.name, c.params); + } catch (err) { + caught = err; + } + if (caught !== undefined && notFound(caught)) { + this.skip(); + } + expect( + caught, + `${c.name} errored for a non-"not found" reason: ${collectStrings(caught).join(' | ')}`, + ).to.be.undefined; + expect(result, `${c.name} unexpectedly returned undefined without an error`).to.not.be.undefined; + }); + } +}); + +describe('Category-3 divergences from Ethereum (skipped until Thor aligns)', () => { + let web3: Web3; + before(() => { + web3 = makeWeb3(); + }); + + // geth's eth_feeHistory returns a per-block × per-percentile `reward` matrix + // when called with rewardPercentiles. Thor (rpc/fees/handler.go) currently + // rejects the percentile form — "reward percentiles are not yet supported" — + // so a fee estimator that requests percentiles can't use it. We SKIP on that + // documented gap; if Thor ever ships it, the call succeeds and the + // reward-matrix assertion keeps it honest. The sibling "rejected by Thor" test + // in provider.test.ts covers the current behavior. + it('eth_feeHistory with rewardPercentiles returns a reward matrix (geth parity)', async function () { + let raw: { reward?: string[][] }; + try { + raw = (await rpc(web3, 'eth_feeHistory', ['0x4', 'latest', [25, 50, 75]])) as { + reward?: string[][]; + }; + } catch (err) { + if (/percentile|not yet supported/i.test(collectStrings(err).join(' | '))) { + this.skip(); + } + throw err; + } + expect(raw, 'feeHistory result').to.be.an('object'); + expect(raw.reward, 'reward matrix').to.be.an('array').and.length.greaterThan(0); + for (const row of raw.reward ?? []) { + expect(row, 'per-block reward row').to.be.an('array').and.length(3); + } + }); +}); diff --git a/tests/eth_rpc/web3js/test/wallet.test.ts b/tests/eth_rpc/web3js/test/wallet.test.ts new file mode 100644 index 0000000..d920c83 --- /dev/null +++ b/tests/eth_rpc/web3js/test/wallet.test.ts @@ -0,0 +1,171 @@ +import { expect } from 'chai'; +import { Web3 } from 'web3'; +import { + collectStrings, + fetchBaseFee, + makeWeb3, + rpc, + sendEip1559, + signEip1559Raw, + NODE2_ADDRESS, + TEST_SENDER_ADDRESS, + TEST_SENDER_KEY, +} from '../src/fixtures'; + +describe('Accounts — sign & send EIP-1559 tx', () => { + let web3: Web3; + before(() => { + web3 = makeWeb3(); + }); + + it('sends 1 wei and receives a successful receipt with correct balance delta', async () => { + const before = await web3.eth.getBalance(NODE2_ADDRESS); + + const receipt = await sendEip1559(web3, TEST_SENDER_KEY, { + to: NODE2_ADDRESS, + value: 1n, + gas: 21_000n, + }); + + expect(receipt.status).to.equal(1n); + expect((receipt.from as string).toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + expect((receipt.to as string).toLowerCase()).to.equal(NODE2_ADDRESS.toLowerCase()); + expect(receipt.transactionHash as string).to.match(/^0x[0-9a-fA-F]{64}$/); + + const after = await web3.eth.getBalance(NODE2_ADDRESS); + expect(after - before).to.equal(1n); + }); + + it('rejects an unfunded address with an insufficient-funds error', async () => { + const unfundedKey = '0x' + '11'.repeat(32); + let threw = false; + try { + await sendEip1559(web3, unfundedKey, { to: NODE2_ADDRESS, value: 1n, gas: 21_000n }); + } catch { + threw = true; + } + expect(threw, 'expected unfunded send to throw').to.equal(true); + }); + + it('EIP-2930 (type 1) access-list transactions are rejected by Thor — only EIP-1559 is accepted', async () => { + const account = web3.eth.accounts.privateKeyToAccount(TEST_SENDER_KEY); + const chainId = await web3.eth.getChainId(); + const nonce = await web3.eth.getTransactionCount(account.address, 'pending'); + const gasPrice = await web3.eth.getGasPrice(); + const signed = await account.signTransaction({ + to: NODE2_ADDRESS, + value: 1n, + // Access lists raise the intrinsic gas floor (2400/address), so web3.js's + // offline signer rejects 21000 before we ever reach Thor — give headroom + // so the rejection we assert is Thor's, on the EIP-2930 envelope itself. + gas: 50_000n, + gasPrice, + nonce, + chainId, + type: 1, + accessList: [{ address: NODE2_ADDRESS, storageKeys: [] }], + }); + + let caught: unknown; + try { + await web3.eth.sendSignedTransaction(signed.rawTransaction); + } catch (err) { + caught = err; + } + expect(caught, 'expected EIP-2930 tx to be rejected').to.not.be.undefined; + expect(collectStrings(caught).join(' || ')).to.match(/rlp|access|unsupported|expected List|coalesce|type/i); + }); + + it('Legacy (type 0) transactions are rejected by Thor — only EIP-1559 is accepted', async () => { + const account = web3.eth.accounts.privateKeyToAccount(TEST_SENDER_KEY); + const chainId = await web3.eth.getChainId(); + const nonce = await web3.eth.getTransactionCount(account.address, 'pending'); + const gasPrice = await web3.eth.getGasPrice(); + const signed = await account.signTransaction({ + to: NODE2_ADDRESS, + value: 1n, + gas: 21_000n, + gasPrice, + nonce, + chainId, + type: 0, + }); + + let caught: unknown; + try { + await web3.eth.sendSignedTransaction(signed.rawTransaction); + } catch (err) { + caught = err; + } + expect(caught, 'expected legacy tx to be rejected').to.not.be.undefined; + expect(collectStrings(caught).join(' || ')).to.match(/rlp|legacy|unsupported|expected List|coalesce/i); + }); + + it('accounts.sign produces a signature that accounts.recover recovers', async () => { + const account = web3.eth.accounts.privateKeyToAccount(TEST_SENDER_KEY); + const message = 'hello thor'; + + const signed = account.sign(message); + expect(signed.signature).to.match(/^0x[0-9a-fA-F]{130}$/); + + const recovered = web3.eth.accounts.recover(message, signed.signature); + expect(recovered.toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + }); + + it('signTransaction produces a raw RLP that recoverTransaction maps back to the signer', async () => { + const account = web3.eth.accounts.privateKeyToAccount(TEST_SENDER_KEY); + const chainId = await web3.eth.getChainId(); + const nonce = await web3.eth.getTransactionCount(account.address, 'pending'); + const baseFee = await fetchBaseFee(web3); + const signed = await account.signTransaction({ + to: NODE2_ADDRESS, + value: 2n, + gas: 21_000n, + nonce, + chainId, + maxPriorityFeePerGas: 1n, + maxFeePerGas: baseFee * 2n + 1n, + type: 2, + }); + expect(signed.rawTransaction).to.match(/^0x[0-9a-fA-F]+$/); + + const recovered = web3.eth.accounts.recoverTransaction(signed.rawTransaction); + expect(recovered.toLowerCase()).to.equal(TEST_SENDER_ADDRESS.toLowerCase()); + }); + + it('sendSignedTransaction accepts an offline-signed raw tx and confirms it', async () => { + const before = await web3.eth.getBalance(NODE2_ADDRESS); + + const raw = await signEip1559Raw(web3, TEST_SENDER_KEY, { + to: NODE2_ADDRESS, + value: 3n, + gas: 21_000n, + }); + const receipt = await web3.eth.sendSignedTransaction(raw); + expect(receipt.status).to.equal(1n); + expect(receipt.transactionHash as string).to.match(/^0x[0-9a-fA-F]{64}$/); + + const after = await web3.eth.getBalance(NODE2_ADDRESS); + expect(after - before).to.equal(3n); + }); + + it('eth_signTypedData_v4 is rejected — no node-side keys to sign with', async () => { + // web3.js has no offline EIP-712 signer; the only typed-data path goes to the + // node, which Thor rejects (no keystore). Documenting the gap keeps it pinned. + let caught: unknown; + try { + await rpc(web3, 'eth_signTypedData_v4', [ + TEST_SENDER_ADDRESS, + JSON.stringify({ + domain: { name: 'InterstellarTest', version: '1', chainId: 1, verifyingContract: NODE2_ADDRESS }, + types: { EIP712Domain: [], Mail: [{ name: 'contents', type: 'string' }] }, + primaryType: 'Mail', + message: { contents: 'hi' }, + }), + ]); + } catch (err) { + caught = err; + } + expect(caught, 'expected eth_signTypedData_v4 to be rejected').to.not.be.undefined; + }); +}); diff --git a/tests/eth_rpc/web3js/test/websocket.test.ts b/tests/eth_rpc/web3js/test/websocket.test.ts new file mode 100644 index 0000000..bcb7cc7 --- /dev/null +++ b/tests/eth_rpc/web3js/test/websocket.test.ts @@ -0,0 +1,134 @@ +import { expect } from 'chai'; +import { Web3 } from 'web3'; +import { + contractSet, + deployContract, + loadStorageArtifact, + makeWeb3, + makeWsWeb3, + sendEip1559, + NODE2_ADDRESS, + TEST_SENDER_KEY, +} from '../src/fixtures'; + +// web3.js v4 WebSocketProvider exposes disconnect()/getStatus() on currentProvider. +interface SocketProviderLike { + disconnect: (code?: number, reason?: string) => void; + getStatus: () => 'connecting' | 'connected' | 'disconnected'; +} +function socket(web3: Web3): SocketProviderLike { + return web3.currentProvider as unknown as SocketProviderLike; +} + +describe('WebSocketProvider — eth_subscribe (newHeads / logs / pending)', () => { + it('getChainId works over a WebSocket transport', async () => { + const ws = makeWsWeb3(); + try { + const httpChainId = await makeWeb3().eth.getChainId(); + const wsChainId = await ws.eth.getChainId(); + expect(wsChainId).to.equal(httpChainId); + } finally { + socket(ws).disconnect(); + } + }); + + it('subscribe("newBlockHeaders") receives a notification over eth_subscribe', async function () { + this.timeout(60_000); + const ws = makeWsWeb3(); + try { + const sub = await ws.eth.subscribe('newBlockHeaders'); + const header = await new Promise<{ number: bigint }>((resolve, reject) => { + sub.on('data', (h) => resolve(h as unknown as { number: bigint })); + sub.on('error', reject); + }); + expect(header.number > 0n, `header.number was ${header.number}`).to.equal(true); + await sub.unsubscribe(); + } finally { + socket(ws).disconnect(); + } + }); + + it('subscribe("logs") receives a Set log over eth_subscribe(logs)', async function () { + this.timeout(60_000); + // Deploy + emit via HTTP so the WS path only carries subscription traffic. + const http = makeWeb3(); + const setTopic = http.utils.keccak256('Set(address,uint256)'); + const artifact = loadStorageArtifact(); + const { address } = await deployContract(http, TEST_SENDER_KEY, artifact); + + const ws = makeWsWeb3(); + try { + const sub = await ws.eth.subscribe('logs', { address, topics: [setTopic] }); + const seen = new Promise<{ topics: string[]; transactionHash: string }>((resolve, reject) => { + sub.on('data', (l) => resolve(l as unknown as { topics: string[]; transactionHash: string })); + sub.on('error', reject); + }); + + const receipt = await contractSet(http, TEST_SENDER_KEY, address, artifact.abi, 4242n); + const log = await seen; + expect(log.topics[0]).to.equal(setTopic); + expect(log.transactionHash.toLowerCase()).to.equal( + (receipt.transactionHash as string).toLowerCase(), + ); + await sub.unsubscribe(); + } finally { + socket(ws).disconnect(); + } + }); + + it('subscribe("pendingTransactions") receives a tx-hash notification', async function () { + this.timeout(60_000); + const http = makeWeb3(); + const ws = makeWsWeb3(); + try { + const sub = await ws.eth.subscribe('pendingTransactions'); + const seen = new Promise((resolve, reject) => { + sub.on('data', (h) => resolve(h as unknown as string)); + sub.on('error', reject); + }); + + const receipt = await sendEip1559(http, TEST_SENDER_KEY, { + to: NODE2_ADDRESS, + value: 1n, + gas: 21_000n, + }); + + const observed = await seen; + expect(observed).to.match(/^0x[0-9a-fA-F]{64}$/); + expect(observed.toLowerCase()).to.equal((receipt.transactionHash as string).toLowerCase()); + await sub.unsubscribe(); + } finally { + socket(ws).disconnect(); + } + }); + + it('subscribe("syncing") is accepted by Thor (returns a subscription id)', async function () { + this.timeout(30_000); + // Thor's eth_eq_json_rpc branch implements the 'syncing' subtype: eth_subscribe + // returns a subscription id and immediately pushes the status (false when in + // sync). The earlier "syncing is rejected" expectation no longer holds — the + // Go-side rejection test was removed alongside this change. We assert on the + // subscription id (proof Thor accepted the eth_subscribe) rather than the + // data frame, since web3.js's SyncingSubscription does not surface a `false` + // (not-syncing) payload as a 'data' event. + const ws = makeWsWeb3(); + try { + const sub = await ws.eth.subscribe('syncing'); + expect(sub.id, 'syncing subscription id').to.match(/^0x[0-9a-fA-F]+$/); + await sub.unsubscribe(); + } finally { + socket(ws).disconnect(); + } + }); + + it('provider.disconnect() closes the websocket cleanly', async () => { + const ws = makeWsWeb3(); + const bn = await ws.eth.getBlockNumber(); + expect(bn > 0n).to.equal(true); + expect(socket(ws).getStatus()).to.equal('connected'); + + socket(ws).disconnect(); + await new Promise((r) => setTimeout(r, 200)); + expect(socket(ws).getStatus()).to.equal('disconnected'); + }); +}); diff --git a/tests/eth_rpc/web3js/tsconfig.json b/tests/eth_rpc/web3js/tsconfig.json new file mode 100644 index 0000000..7dce8c3 --- /dev/null +++ b/tests/eth_rpc/web3js/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "node", + "ignoreDeprecations": "5.0", + "esModuleInterop": true, + "resolveJsonModule": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": false, + "sourceMap": true, + "outDir": "dist", + "types": ["node", "mocha"] + }, + "ts-node": { + "transpileOnly": true + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/tests/eth_rpc/web3js/web3js_test.go b/tests/eth_rpc/web3js/web3js_test.go new file mode 100644 index 0000000..5488292 --- /dev/null +++ b/tests/eth_rpc/web3js/web3js_test.go @@ -0,0 +1,38 @@ +// Thin Go wrapper that launches the mocha + web3.js v4 suite as part of +// `go test ./...`. The wrapper shares the network lifecycle with every other +// Go test package via helper.RunTestMain — when NODE_URL is exported (e.g. by +// `make test`), the existing network is reused; otherwise RunTestMain starts a +// fresh one. +// +// We invoke `npx mocha` directly (not `npm test`) to skip any pretest hook, +// mirroring the ethersjs wrapper. + +package web3js + +import ( + "os" + "os/exec" + "testing" + + "github.com/vechain/interstellar-e2e/tests/helper" +) + +var nodeURL string + +func TestMain(m *testing.M) { + os.Exit(helper.RunTestMain(m, &nodeURL, nil)) +} + +func TestWeb3JS(t *testing.T) { + if _, err := os.Stat("node_modules"); os.IsNotExist(err) { + t.Fatal("node_modules missing — run `make test` (auto-installs) or `npm ci` in tests/eth_rpc/web3js/") + } + + cmd := exec.CommandContext(t.Context(), "npx", "mocha") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = append(os.Environ(), "NODE_URL="+nodeURL) + if err := cmd.Run(); err != nil { + t.Fatalf("web3js mocha suite failed: %v", err) + } +} From 9e06d4a900d149156c411ab8c7f8ad4de1ba6f54 Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Mon, 29 Jun 2026 17:27:22 +0800 Subject: [PATCH 10/14] Change test timeout setting --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 109999b..c015e85 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ test: build-network ethersjs-deps web3js-deps viem-deps @/tmp/interstellar-network start & \ NODE_URL=$$(/tmp/interstellar-network node-url) && \ NODE_P2P_PORT=$$(/tmp/interstellar-network node-p2p-port) && \ - cd tests && NODE_URL=$$NODE_URL NODE_P2P_PORT=$$NODE_P2P_PORT go test -v -count=1 -p 1 -timeout 20m ./... ; \ + cd tests && NODE_URL=$$NODE_URL NODE_P2P_PORT=$$NODE_P2P_PORT go test -v -count=1 -p 1 -timeout 30m ./... ; \ CODE=$$? ; \ /tmp/interstellar-network stop 2>/dev/null || true ; \ exit $$CODE From df631e0c45ea26d6b88d88d3da1bcf6f59456010 Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Mon, 29 Jun 2026 19:21:23 +0800 Subject: [PATCH 11/14] Optimize test --- network/setup/network.go | 7 +++++++ tests/eth_rpc/ethersjs/src/fixtures.ts | 6 +++++- tests/eth_rpc/viem/src/fixtures.ts | 6 ++++-- tests/helper/client.go | 4 +++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/network/setup/network.go b/network/setup/network.go index f1194f2..4452b0a 100644 --- a/network/setup/network.go +++ b/network/setup/network.go @@ -51,6 +51,13 @@ func BuildNetwork() *network.Network { // Raise block gas limit above MaxTxGasLimit (1<<24) so EIP-7825 boundary // tests can verify at-limit transactions are both accepted and includable. gen.GasLimit = 40_000_000 + // Shorten the block interval from the preset default (10s) to the Thor + // minimum (2s — customnet rejects 0 or 1). Every test that submits a real + // transaction blocks on the next packed block, so this cuts confirmation + // latency ~5x and dominates total `make test` wall-clock. + if gen.Config != nil { + gen.Config.BlockInterval = 2 + } // Activate the INTERSTELLAR fork from block 1, leaving block 0 as a // pre-fork state that tests can simulate against via InspectClauses Revision("0"). gen.ForkConfig.AddField("INTERSTELLAR", 1) //nolint:errcheck diff --git a/tests/eth_rpc/ethersjs/src/fixtures.ts b/tests/eth_rpc/ethersjs/src/fixtures.ts index a69f273..b903820 100644 --- a/tests/eth_rpc/ethersjs/src/fixtures.ts +++ b/tests/eth_rpc/ethersjs/src/fixtures.ts @@ -36,7 +36,11 @@ export function getHttpUrl(): string { } export function makeProvider(): JsonRpcProvider { - return new JsonRpcProvider(getHttpUrl()); + const provider = new JsonRpcProvider(getHttpUrl()); + // ethers v6 defaults to 4s polling; tighten so tx confirmations are observed + // promptly under the 2s block interval instead of lagging a full poll period. + provider.pollingInterval = 250; + return provider; } export function getWsUrl(): string { diff --git a/tests/eth_rpc/viem/src/fixtures.ts b/tests/eth_rpc/viem/src/fixtures.ts index 83de9ad..a8fd486 100644 --- a/tests/eth_rpc/viem/src/fixtures.ts +++ b/tests/eth_rpc/viem/src/fixtures.ts @@ -53,7 +53,9 @@ export function getWsUrl(): string { // makePublicClient builds a read-only client over the HTTP transport. export function makePublicClient() { - return createPublicClient({ transport: http(getHttpUrl()) }); + // viem defaults to 4s polling; tighten so waitForTransactionReceipt picks up a + // freshly-packed receipt promptly under the 2s block interval. + return createPublicClient({ transport: http(getHttpUrl()), pollingInterval: 250 }); } // makeWsClient builds a client over the WebSocket transport — viem routes @@ -84,7 +86,7 @@ export async function getThorChain(): Promise { export async function makeWalletClient(key: `0x${string}`) { const account = privateKeyToAccount(key); const chain = await getThorChain(); - return createWalletClient({ account, chain, transport: http(getHttpUrl()) }); + return createWalletClient({ account, chain, transport: http(getHttpUrl()), pollingInterval: 250 }); } // rpc is the raw JSON-RPC escape hatch — the viem analogue of ethers' diff --git a/tests/helper/client.go b/tests/helper/client.go index 55d3a67..da560b8 100644 --- a/tests/helper/client.go +++ b/tests/helper/client.go @@ -73,7 +73,9 @@ func WaitForReceipt(t *testing.T, client *thorclient.Client, txID *thor.Bytes32, if err == nil && receipt != nil { return receipt } - time.Sleep(2 * time.Second) + // Poll well under the 2s block interval so a freshly-packed receipt is + // observed promptly rather than adding up to a full poll period of latency. + time.Sleep(250 * time.Millisecond) } t.Fatalf("timed out waiting for receipt: %s", txID) return nil From 86c15961bfb1d3a7bb5914274c5bce0dcbeed92a Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Wed, 1 Jul 2026 20:13:28 +0800 Subject: [PATCH 12/14] test(eth_rpc): assert feeHistory rewardPercentiles now that Thor supports it --- .../eth_rpc_schema_extra_test.go | 38 ++++++------ tests/eth_rpc/ethersjs/test/provider.test.ts | 60 ++++--------------- tests/eth_rpc/viem/test/provider.test.ts | 11 ++++ tests/eth_rpc/viem/test/rpc-extra.test.ts | 31 ---------- tests/eth_rpc/web3js/test/provider.test.ts | 20 ++++--- tests/eth_rpc/web3js/test/rpc-extra.test.ts | 33 ---------- 6 files changed, 51 insertions(+), 142 deletions(-) diff --git a/tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_extra_test.go b/tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_extra_test.go index e47100f..e6b8fb4 100644 --- a/tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_extra_test.go +++ b/tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_extra_test.go @@ -269,36 +269,34 @@ func TestUnimplementedMethods(t *testing.T) { } // ----------------------------------------------------------------------------- -// Cat-3 — divergences from go-ethereum (skipped until Thor aligns) +// eth_feeHistory — rewardPercentiles form (geth parity) // ----------------------------------------------------------------------------- // TestEthFeeHistory_RewardPercentiles probes the rewardPercentiles form of // eth_feeHistory. // // geth returns a per-block × per-percentile `reward` matrix when called with -// rewardPercentiles. Thor (rpc/fees/handler.go) currently rejects the percentile -// form — "reward percentiles are not yet supported" (code -32000) — so a fee -// estimator that requests percentiles can't use it. We SKIP on that documented -// gap; if Thor ever ships it, the call succeeds and the reward-matrix assertion -// keeps it honest. TestEthFeeHistory (no percentiles) covers the supported path. +// rewardPercentiles. Thor now implements this form (rpc/fees/handler.go), so the +// call succeeds and returns a reward matrix with one entry per requested +// percentile per block. TestEthFeeHistory (no percentiles) covers the base path. func TestEthFeeHistory_RewardPercentiles(t *testing.T) { result, err := rpcCall(t, "eth_feeHistory", "0x4", "latest", []float64{25, 50, 75}) - if isRewardPercentilesUnsupported(err) { - t.Skipf("eth_feeHistory rewardPercentiles not supported by Thor: %v", err) - } require.NoError(t, err, "eth_feeHistory with rewardPercentiles") - var fh map[string]any + // Validate the whole result — including the `reward` matrix — against the + // eth_feeHistory JSON schema. This is the only path that exercises the + // schema's `reward` array-of-array-of-quantity block, since the base + // TestEthFeeHistory sends empty percentiles and gets no reward field. + validateResult(t, "eth_feeHistory", result) + var fh struct { + Reward [][]string `json:"reward"` + } require.NoError(t, json.Unmarshal(result, &fh), "unmarshal feeHistory") - require.Contains(t, fh, "reward", + require.NotEmpty(t, fh.Reward, "feeHistory must include a per-block reward matrix when rewardPercentiles is requested") -} - -// isRewardPercentilesUnsupported reports whether err is Thor's documented -// rejection of the eth_feeHistory rewardPercentiles parameter. -func isRewardPercentilesUnsupported(err error) bool { - if err == nil { - return false + for _, row := range fh.Reward { + require.Len(t, row, 3, "each reward row must have one value per requested percentile") + for _, r := range row { + require.Regexp(t, "^0x[0-9a-fA-F]+$", r, "reward value must be a QUANTITY") + } } - msg := strings.ToLower(err.Error()) - return strings.Contains(msg, "percentile") || strings.Contains(msg, "not yet supported") } diff --git a/tests/eth_rpc/ethersjs/test/provider.test.ts b/tests/eth_rpc/ethersjs/test/provider.test.ts index ac2383a..a7f7b49 100644 --- a/tests/eth_rpc/ethersjs/test/provider.test.ts +++ b/tests/eth_rpc/ethersjs/test/provider.test.ts @@ -158,23 +158,18 @@ describe('Provider read-only RPC', () => { expect(raw.gasUsedRatio).to.be.an('array').and.to.have.length.greaterThan(0); }); - it('eth_feeHistory with rewardPercentiles is rejected by Thor', async () => { - let caught: unknown; - try { - await provider.send('eth_feeHistory', ['0x4', 'latest', [25, 50, 75]]); - } catch (err) { - caught = err; - } - expect(caught, 'expected percentile request to be rejected').to.not.be.undefined; - const haystack: string[] = []; - const walk = (obj: unknown, depth: number) => { - if (depth > 3 || obj == null) return; - if (typeof obj === 'string') haystack.push(obj); - else if (typeof obj === 'object') - for (const v of Object.values(obj as Record)) walk(v, depth + 1); + it('eth_feeHistory with rewardPercentiles returns a reward matrix (geth parity)', async () => { + // Thor now implements the rewardPercentiles form (rpc/fees/handler.go), + // returning a per-block × per-percentile `reward` matrix like geth. + const raw = (await provider.send('eth_feeHistory', ['0x4', 'latest', [25, 50, 75]])) as { + reward?: string[][]; }; - walk(caught, 0); - expect(haystack.join(' || ')).to.match(/percentile|not yet supported|coalesce/i); + expect(raw, 'eth_feeHistory result').to.be.an('object'); + expect(raw.reward, 'reward matrix').to.be.an('array').and.length.greaterThan(0); + for (const row of raw.reward!) { + expect(row, 'per-block reward row').to.be.an('array').and.length(3); + for (const r of row) expect(r, 'reward value').to.match(/^0x[0-9a-fA-F]+$/); + } }); it('eth_getBlockReceipts returns the receipt array for the latest block', async () => { @@ -711,36 +706,3 @@ describe('Unimplemented standard eth_* methods (skipped until Thor ships them)', }); } }); - -describe('Category-3 divergences from Ethereum (skipped until Thor aligns)', () => { - let provider: JsonRpcProvider; - before(() => { - provider = makeProvider(); - }); - - // geth's eth_feeHistory returns a per-block × per-percentile `reward` matrix - // when called with rewardPercentiles. Thor (rpc/fees/handler.go) currently - // rejects the percentile form — "reward percentiles are not yet supported" — - // so a fee estimator that requests percentiles can't use it. We SKIP on that - // documented gap; if Thor ever ships it, the call succeeds and the - // reward-matrix assertion keeps it honest. The sibling "is rejected by Thor" - // test above covers the current behavior. - it('eth_feeHistory with rewardPercentiles returns a reward matrix (geth parity)', async function () { - let raw: { reward?: string[][] }; - try { - raw = (await provider.send('eth_feeHistory', ['0x4', 'latest', [25, 50, 75]])) as { - reward?: string[][]; - }; - } catch (err) { - if (/percentile|not yet supported/i.test(collectStrings(err).join(' || '))) { - this.skip(); - } - throw err; - } - expect(raw, 'feeHistory result').to.be.an('object'); - expect(raw.reward, 'reward matrix').to.be.an('array').and.length.greaterThan(0); - for (const row of raw.reward!) { - expect(row, 'per-block reward row').to.be.an('array').and.length(3); - } - }); -}); diff --git a/tests/eth_rpc/viem/test/provider.test.ts b/tests/eth_rpc/viem/test/provider.test.ts index 2794354..f258fe1 100644 --- a/tests/eth_rpc/viem/test/provider.test.ts +++ b/tests/eth_rpc/viem/test/provider.test.ts @@ -90,6 +90,17 @@ describe('Public client read-only RPC', () => { expect(fh.gasUsedRatio).to.be.an('array').and.length.greaterThan(0); }); + it('getFeeHistory with rewardPercentiles returns a reward matrix (geth parity)', async () => { + // Thor now implements the rewardPercentiles form (rpc/fees/handler.go), + // returning a per-block × per-percentile `reward` matrix like geth. + const fh = await client.getFeeHistory({ blockCount: 4, rewardPercentiles: [25, 50, 75] }); + expect(fh.reward, 'reward matrix').to.be.an('array').and.length.greaterThan(0); + for (const row of fh.reward!) { + expect(row, 'per-block reward row').to.be.an('array').and.length(3); + for (const r of row) expect(r, 'reward value').to.be.a('bigint'); + } + }); + it('eth_getBlockReceipts returns the receipt array for the latest block', async () => { const receipts = (await rpc(client, 'eth_getBlockReceipts', ['latest'])) as Array< Record diff --git a/tests/eth_rpc/viem/test/rpc-extra.test.ts b/tests/eth_rpc/viem/test/rpc-extra.test.ts index 401b5a1..e298beb 100644 --- a/tests/eth_rpc/viem/test/rpc-extra.test.ts +++ b/tests/eth_rpc/viem/test/rpc-extra.test.ts @@ -215,34 +215,3 @@ describe('eth_* methods NOT implemented by Thor (skipped until shipped)', () => } }); }); - -describe('Category-3 divergences from Ethereum (skipped until Thor aligns)', () => { - let client: PublicClient; - before(() => { - client = makePublicClient(); - }); - - // geth's eth_feeHistory returns a per-block × per-percentile reward matrix when - // called with rewardPercentiles. Thor (rpc/fees/handler.go) currently rejects - // the percentile form — "reward percentiles are not yet supported" — so a fee - // estimator that requests percentiles can't use it. We SKIP on that documented - // gap; if Thor ever ships it, the reward-matrix assertion keeps it honest. - it('getFeeHistory with rewardPercentiles returns a reward matrix (geth parity)', async function () { - let fh: { reward?: bigint[][] }; - try { - fh = await client.getFeeHistory({ - blockCount: 4, - rewardPercentiles: [25, 50, 75], - }); - } catch (err) { - if (/percentile|not yet supported/i.test(collectStrings(err).join(' | '))) { - this.skip(); - } - throw err; - } - expect(fh.reward, 'reward matrix').to.be.an('array').and.length.greaterThan(0); - for (const row of fh.reward!) { - expect(row, 'per-block reward row').to.be.an('array').and.length(3); - } - }); -}); diff --git a/tests/eth_rpc/web3js/test/provider.test.ts b/tests/eth_rpc/web3js/test/provider.test.ts index a8063d3..b8bc6d0 100644 --- a/tests/eth_rpc/web3js/test/provider.test.ts +++ b/tests/eth_rpc/web3js/test/provider.test.ts @@ -97,16 +97,18 @@ describe('web3.eth read-only RPC', () => { expect(raw.gasUsedRatio).to.be.an('array').and.to.have.length.greaterThan(0); }); - it('eth_feeHistory with rewardPercentiles is rejected by Thor', async () => { - let caught: unknown; - try { - await rpc(web3, 'eth_feeHistory', ['0x4', 'latest', [25, 50, 75]]); - } catch (err) { - caught = err; + it('eth_feeHistory with rewardPercentiles returns a reward matrix (geth parity)', async () => { + // Thor now implements the rewardPercentiles form (rpc/fees/handler.go), + // returning a per-block × per-percentile `reward` matrix like geth. + const raw = (await rpc(web3, 'eth_feeHistory', ['0x4', 'latest', [25, 50, 75]])) as { + reward?: string[][]; + }; + expect(raw, 'eth_feeHistory result').to.be.an('object'); + expect(raw.reward, 'reward matrix').to.be.an('array').and.length.greaterThan(0); + for (const row of raw.reward ?? []) { + expect(row, 'per-block reward row').to.be.an('array').and.length(3); + for (const r of row) expect(r, 'reward value').to.match(/^0x[0-9a-fA-F]+$/); } - expect(caught, 'expected percentile request to be rejected').to.not.be.undefined; - const blob = collectStrings(caught).join(' || '); - expect(blob).to.match(/percentile|not yet supported|coalesce/i); }); it('eth_getBlockReceipts returns the receipt array for the latest block', async () => { diff --git a/tests/eth_rpc/web3js/test/rpc-extra.test.ts b/tests/eth_rpc/web3js/test/rpc-extra.test.ts index 14a56e7..7cee04a 100644 --- a/tests/eth_rpc/web3js/test/rpc-extra.test.ts +++ b/tests/eth_rpc/web3js/test/rpc-extra.test.ts @@ -209,36 +209,3 @@ describe('eth_* methods NOT implemented by Thor (skipped until shipped)', () => }); } }); - -describe('Category-3 divergences from Ethereum (skipped until Thor aligns)', () => { - let web3: Web3; - before(() => { - web3 = makeWeb3(); - }); - - // geth's eth_feeHistory returns a per-block × per-percentile `reward` matrix - // when called with rewardPercentiles. Thor (rpc/fees/handler.go) currently - // rejects the percentile form — "reward percentiles are not yet supported" — - // so a fee estimator that requests percentiles can't use it. We SKIP on that - // documented gap; if Thor ever ships it, the call succeeds and the - // reward-matrix assertion keeps it honest. The sibling "rejected by Thor" test - // in provider.test.ts covers the current behavior. - it('eth_feeHistory with rewardPercentiles returns a reward matrix (geth parity)', async function () { - let raw: { reward?: string[][] }; - try { - raw = (await rpc(web3, 'eth_feeHistory', ['0x4', 'latest', [25, 50, 75]])) as { - reward?: string[][]; - }; - } catch (err) { - if (/percentile|not yet supported/i.test(collectStrings(err).join(' | '))) { - this.skip(); - } - throw err; - } - expect(raw, 'feeHistory result').to.be.an('object'); - expect(raw.reward, 'reward matrix').to.be.an('array').and.length.greaterThan(0); - for (const row of raw.reward ?? []) { - expect(row, 'per-block reward row').to.be.an('array').and.length(3); - } - }); -}); From 0fd6b01dc1ba1b7dc16bded788cbe7739264a8e9 Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Thu, 2 Jul 2026 16:57:21 +0800 Subject: [PATCH 13/14] Update README --- .github/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/README.md b/.github/README.md index c35e568..af8843a 100644 --- a/.github/README.md +++ b/.github/README.md @@ -12,6 +12,7 @@ End-to-end tests for the VeChain **INTERSTELLAR** fork, which activates at block | `tests/eip7934` | [EIP-7934](https://eips.ethereum.org/EIPS/eip-7934) | Max RLP-encoded block size (`MaxRLPBlockSize = 8_388_608`); packer-level split test + P2P consensus-level rejection of oversized blocks | | `tests/eip7883` | [EIP-7883](https://eips.ethereum.org/EIPS/eip-7883) | ModExp precompile repricing | | `tests/eip7939` | [EIP-7939](https://eips.ethereum.org/EIPS/eip-7939) | `CLZ` opcode (0x1e) — count leading zeros | +| `tests/eip6780` | [EIP-7939](https://eips.ethereum.org/EIPS/eip-6780) | `SELFDESTRUCT` only in same transaction | ## Repository layout @@ -30,6 +31,8 @@ interstellar-e2e/ ├── eip7934/ ├── eip7883/ └── eip7939/ + └── eip6780/ + └── eth_rpc/ # test eth rpc endpoint ``` ## Prerequisites From 5e95b17ba9f935b15c9ed8fb4dd1a4e57feab9cd Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Wed, 29 Jul 2026 00:03:43 +0800 Subject: [PATCH 14/14] fix: serialize Go e2e test packages with go test -p 1 - Prevents concurrent eth_rpc suites from reusing the same funded account's nonce, which caused pending transactions to never be mined and suites to fail with receipt/block timeouts --- Makefile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index fc9cadc..fdd3e02 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,12 @@ web3js-deps: viem-deps: @[ -d tests/eth_rpc/viem/node_modules ] || (cd tests/eth_rpc/viem && npm ci) +# -p 1 is required, not a perf knob: every eth_rpc suite signs its transactions +# with the same funded account (node1Key in tests/helper/client.go, mirrored in +# each JS suite's src/fixtures.ts). Letting Go run those packages concurrently +# makes them clobber each other's pending nonce, so transactions are never mined +# and the suites fail with receipt/block timeouts. Serialising is also faster +# here, since the parallel runs spent most of their time waiting on those. test: build-network ethersjs-deps web3js-deps viem-deps @/tmp/interstellar-network start & \ START_PID=$$! ; \ @@ -25,7 +31,7 @@ test: build-network ethersjs-deps web3js-deps viem-deps if [ -z "$$NODE_URL" ] || [ -z "$$NODE_P2P_PORT" ]; then \ echo "ERROR: empty node connection details — refusing to run e2e tests against no network"; exit 1; \ fi ; \ - cd tests && NODE_URL=$$NODE_URL NODE_P2P_PORT=$$NODE_P2P_PORT go test -v -count=1 -timeout 20m ./... + cd tests && NODE_URL=$$NODE_URL NODE_P2P_PORT=$$NODE_P2P_PORT go test -v -count=1 -timeout 20m -p 1 ./... stop: /tmp/interstellar-network stop 2>/dev/null || true