diff --git a/README.md b/README.md index 7a7f8372..d72efe00 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,14 @@ List of supported RPC methods. | `debug_traceCall` | `debug.TraceCall(msg *w3types.Message, blockNumber *big.Int, config *debug.TraceConfig).Returns(trace *debug.Trace)`
`debug.CallTraceCall(msg *w3types.Message, blockNumber *big.Int, overrides w3types.State).Returns(trace *debug.CallTrace)` | `debug_traceTransaction` | `debug.TraceTx(txHash common.Hash, config *debug.TraceConfig).Returns(trace *debug.Trace)`
`debug.CallTraceTx(txHash common.Hash, overrides w3types.State).Returns(trace *debug.CallTrace)` +### [`txpool`](https://pkg.go.dev/github.com/lmittmann/w3/module/txpool) + +| Method | Go Code +| :--------------------| :------- +| `txpool_content` | `txpool.Content().Returns(resp *txpool.ContentResponse)` +| `txpool_contentFrom` | `txpool.ContentFrom(addr common.Address).Returns(resp *txpool.ContentFromResponse)` +| `txpool_status` | `txpool.Status().Returns(resp *txpool.StatusResponse)` + ### [`web3`](https://pkg.go.dev/github.com/lmittmann/w3/module/web3) | Method | Go Code diff --git a/docs/pages/index.mdx b/docs/pages/index.mdx index ce047260..a9b6708d 100644 --- a/docs/pages/index.mdx +++ b/docs/pages/index.mdx @@ -239,6 +239,14 @@ List of supported RPC methods. | `debug_traceCall` | `debug.TraceCall(msg *w3types.Message, blockNumber *big.Int, config *debug.TraceConfig).Returns(trace *debug.Trace)`
`debug.CallTraceCall(msg *w3types.Message, blockNumber *big.Int, overrides w3types.State).Returns(trace *debug.CallTrace)` | `debug_traceTransaction` | `debug.TraceTx(txHash common.Hash, config *debug.TraceConfig).Returns(trace *debug.Trace)`
`debug.CallTraceTx(txHash common.Hash, overrides w3types.State).Returns(trace *debug.CallTrace)` +### [`txpool`](https://pkg.go.dev/github.com/lmittmann/w3/module/txpool) + +| Method | Go Code +| :--------------------| :------- +| `txpool_content` | `txpool.Content().Returns(resp *txpool.ContentResponse)` +| `txpool_contentFrom` | `txpool.ContentFrom(addr common.Address).Returns(resp *txpool.ContentFromResponse)` +| `txpool_status` | `txpool.Status().Returns(resp *txpool.StatusResponse)` + ### [`web3`](https://pkg.go.dev/github.com/lmittmann/w3/module/web3) | Method | Go Code diff --git a/docs/public/robots.txt b/docs/public/robots.txt new file mode 100644 index 00000000..c2a49f4f --- /dev/null +++ b/docs/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / diff --git a/module/txpool/content.go b/module/txpool/content.go new file mode 100644 index 00000000..da0b3d16 --- /dev/null +++ b/module/txpool/content.go @@ -0,0 +1,100 @@ +package txpool + +import ( + "encoding/json" + "sort" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/lmittmann/w3/internal/module" + "github.com/lmittmann/w3/w3types" +) + +// Content requests the pending and queued transactions in the transaction pool. +func Content() w3types.CallerFactory[ContentResponse] { + return module.NewFactory[ContentResponse]( + "txpool_content", + nil, + ) +} + +// ContentFrom requests the pending and queued transactions in the transaction pool +// from the given address. +func ContentFrom(addr common.Address) w3types.CallerFactory[ContentFromResponse] { + return module.NewFactory[ContentFromResponse]( + "txpool_contentFrom", + []any{addr}, + ) +} + +type ContentResponse struct { + Pending map[common.Address][]*types.Transaction + Queued map[common.Address][]*types.Transaction +} + +func (c *ContentResponse) UnmarshalJSON(data []byte) error { + type contentResponse struct { + Pending map[common.Address]map[uint64]*types.Transaction `json:"pending"` + Queued map[common.Address]map[uint64]*types.Transaction `json:"queued"` + } + + var dec contentResponse + if err := json.Unmarshal(data, &dec); err != nil { + return err + } + + c.Pending = make(map[common.Address][]*types.Transaction, len(dec.Pending)) + for addr, nonceTx := range dec.Pending { + txs := make(types.TxByNonce, 0, len(nonceTx)) + for _, tx := range nonceTx { + txs = append(txs, tx) + } + sort.Sort(txs) + c.Pending[addr] = txs + } + + c.Queued = make(map[common.Address][]*types.Transaction, len(dec.Queued)) + for addr, nonceTx := range dec.Queued { + txs := make(types.TxByNonce, 0, len(nonceTx)) + for _, tx := range nonceTx { + txs = append(txs, tx) + } + sort.Sort(txs) + c.Queued[addr] = txs + } + + return nil +} + +type ContentFromResponse struct { + Pending []*types.Transaction + Queued []*types.Transaction +} + +func (cf *ContentFromResponse) UnmarshalJSON(data []byte) error { + type contentFromResponse struct { + Pending map[uint64]*types.Transaction `json:"pending"` + Queued map[uint64]*types.Transaction `json:"queued"` + } + + var dec contentFromResponse + if err := json.Unmarshal(data, &dec); err != nil { + return err + } + + txs := make(types.TxByNonce, 0, len(dec.Pending)) + for _, tx := range dec.Pending { + txs = append(txs, tx) + } + sort.Sort(txs) + cf.Pending = txs + + txs = make(types.TxByNonce, 0, len(dec.Queued)) + for _, tx := range dec.Queued { + txs = append(txs, tx) + } + sort.Sort(txs) + cf.Queued = txs + + return nil +} diff --git a/module/txpool/content_test.go b/module/txpool/content_test.go new file mode 100644 index 00000000..0c9e7e83 --- /dev/null +++ b/module/txpool/content_test.go @@ -0,0 +1,85 @@ +package txpool_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/lmittmann/w3/module/txpool" + "github.com/lmittmann/w3/rpctest" +) + +func TestContent(t *testing.T) { + tests := []rpctest.TestCase[txpool.ContentResponse]{ + { + Golden: "content", + Call: txpool.Content(), + WantRet: txpool.ContentResponse{ + Pending: map[common.Address][]*types.Transaction{ + common.HexToAddress("0x000454307bB96E303044046a6eB2736D2aD560B6"): { + types.NewTx(&types.DynamicFeeTx{ + ChainID: big.NewInt(1), + Nonce: 4652, + GasTipCap: big.NewInt(31407912032), + GasFeeCap: big.NewInt(202871575924), + Gas: 1100000, + To: ptr(common.HexToAddress("0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B")), + Value: big.NewInt(81000000000000000), + Data: common.FromHex("0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000001896e196d5300000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000011fc51222ce800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000004657febe8d8000000000000000000000000000000000000000000000000000011fc51222ce800000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000008d538a82c84d7003aa0e7f1098bd9dc5ea1777be000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000"), + R: new(big.Int).SetBytes(common.FromHex("0xf91729846ac8bb780a7239b7f0157d53330bba310a0811f4eec2eae25172c252")), + S: new(big.Int).SetBytes(common.FromHex("0x1bb9a1b9aea8b128e0e8dc42b17664be50c7b4073973c730b6c4cf2a3b3503cb")), + }), + }, + }, + Queued: map[common.Address][]*types.Transaction{ + common.HexToAddress("0x1BA4Ca9ac6ff4CF192C11E8C1624563f302cAA87"): { + types.NewTx(&types.DynamicFeeTx{ + ChainID: big.NewInt(1), + Nonce: 183, + GasTipCap: big.NewInt(110000000), + GasFeeCap: big.NewInt(20027736270), + Gas: 99226, + To: ptr(common.HexToAddress("0x1BA4Ca9ac6ff4CF192C11E8C1624563f302cAA87")), + Value: big.NewInt(0), + Data: []byte{}, + R: new(big.Int).SetBytes(common.FromHex("0xea35c7c0643b79664b0bbb7f42d64edd371508ae4c33c1f817a80a2655465935")), + S: new(big.Int).SetBytes(common.FromHex("0x76d39f794e9e1ee359d66b7d3b19b90aaf2051b2159c68f3bb8c954558989da8")), + }), + }, + }, + }, + }, + } + + rpctest.RunTestCases(t, tests) +} + +func TestContentFrom(t *testing.T) { + tests := []rpctest.TestCase[txpool.ContentFromResponse]{ + { + Golden: "contentFrom", + Call: txpool.ContentFrom(common.HexToAddress("0x1BA4Ca9ac6ff4CF192C11E8C1624563f302cAA87")), + WantRet: txpool.ContentFromResponse{ + Queued: []*types.Transaction{ + types.NewTx(&types.DynamicFeeTx{ + ChainID: big.NewInt(1), + Nonce: 183, + GasTipCap: big.NewInt(110000000), + GasFeeCap: big.NewInt(20027736270), + Gas: 99226, + To: ptr(common.HexToAddress("0x1BA4Ca9ac6ff4CF192C11E8C1624563f302cAA87")), + Value: big.NewInt(0), + Data: []byte{}, + R: new(big.Int).SetBytes(common.FromHex("0xea35c7c0643b79664b0bbb7f42d64edd371508ae4c33c1f817a80a2655465935")), + S: new(big.Int).SetBytes(common.FromHex("0x76d39f794e9e1ee359d66b7d3b19b90aaf2051b2159c68f3bb8c954558989da8")), + }), + }, + }, + }, + } + + rpctest.RunTestCases(t, tests) +} + +func ptr[T any](x T) *T { return &x } diff --git a/module/txpool/doc.go b/module/txpool/doc.go new file mode 100644 index 00000000..ed5b9b66 --- /dev/null +++ b/module/txpool/doc.go @@ -0,0 +1,4 @@ +/* +Package txpool implements RPC API bindings for methods in the "txpool" namespace. +*/ +package txpool diff --git a/module/txpool/status.go b/module/txpool/status.go new file mode 100644 index 00000000..3e3722ae --- /dev/null +++ b/module/txpool/status.go @@ -0,0 +1,37 @@ +package txpool + +import ( + "encoding/json" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/lmittmann/w3/internal/module" + "github.com/lmittmann/w3/w3types" +) + +// Status requests the number of pending and queued transactions in the transaction pool. +func Status() w3types.CallerFactory[StatusResponse] { + return module.NewFactory[StatusResponse]( + "txpool_status", + nil, + ) +} + +type StatusResponse struct { + Pending uint + Queued uint +} + +func (s *StatusResponse) UnmarshalJSON(data []byte) error { + type statusResponse struct { + Pending hexutil.Uint `json:"pending"` + Queued hexutil.Uint `json:"queued"` + } + + var dec statusResponse + if err := json.Unmarshal(data, &dec); err != nil { + return err + } + s.Pending = uint(dec.Pending) + s.Queued = uint(dec.Queued) + return nil +} diff --git a/module/txpool/status_test.go b/module/txpool/status_test.go new file mode 100644 index 00000000..55b187e0 --- /dev/null +++ b/module/txpool/status_test.go @@ -0,0 +1,20 @@ +package txpool_test + +import ( + "testing" + + "github.com/lmittmann/w3/module/txpool" + "github.com/lmittmann/w3/rpctest" +) + +func TestStatus(t *testing.T) { + tests := []rpctest.TestCase[txpool.StatusResponse]{ + { + Golden: "status", + Call: txpool.Status(), + WantRet: txpool.StatusResponse{Pending: 10, Queued: 7}, + }, + } + + rpctest.RunTestCases(t, tests) +} diff --git a/module/txpool/testdata/content.golden b/module/txpool/testdata/content.golden new file mode 100644 index 00000000..70709f03 --- /dev/null +++ b/module/txpool/testdata/content.golden @@ -0,0 +1,2 @@ +> {"jsonrpc":"2.0","id":1,"method":"txpool_content"} +< {"jsonrpc":"2.0","id":1,"result":{"pending":{"0x000454307bB96E303044046a6eB2736D2aD560B6":{"4652":{"blockHash":null,"blockNumber":null,"from":"0x000454307bb96e303044046a6eb2736d2ad560b6","gas":"0x10c8e0","gasPrice":"0x2f3c169574","maxFeePerGas":"0x2f3c169574","maxPriorityFeePerGas":"0x7500eb460","hash":"0xb4f36425ff7007c1fc6d29148f062f5ed3232ad99bc185f5ad26f071851409c7","input":"0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000001896e196d5300000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000011fc51222ce800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000004657febe8d8000000000000000000000000000000000000000000000000000011fc51222ce800000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000008d538a82c84d7003aa0e7f1098bd9dc5ea1777be000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000","nonce":"0x122c","to":"0xef1c6e67703c7bd7107eed8303fbe6ec2554bf6b","transactionIndex":null,"value":"0x11fc51222ce8000","type":"0x2","accessList":[],"chainId":"0x1","v":"0x0","r":"0xf91729846ac8bb780a7239b7f0157d53330bba310a0811f4eec2eae25172c252","s":"0x1bb9a1b9aea8b128e0e8dc42b17664be50c7b4073973c730b6c4cf2a3b3503cb"}}},"queued":{"0x1BA4Ca9ac6ff4CF192C11E8C1624563f302cAA87":{"183":{"blockHash":null,"blockNumber":null,"from":"0x1ba4ca9ac6ff4cf192c11e8c1624563f302caa87","gas":"0x1839a","gasPrice":"0x4a9bf00ce","maxFeePerGas":"0x4a9bf00ce","maxPriorityFeePerGas":"0x68e7780","hash":"0x222c11451c3bf3607a4ef18fa87f26575a0dca97e10ceb81b8ebebdd45d5bbfb","input":"0x","nonce":"0xb7","to":"0x1ba4ca9ac6ff4cf192c11e8c1624563f302caa87","transactionIndex":null,"value":"0x0","type":"0x2","accessList":[],"chainId":"0x1","v":"0x0","r":"0xea35c7c0643b79664b0bbb7f42d64edd371508ae4c33c1f817a80a2655465935","s":"0x76d39f794e9e1ee359d66b7d3b19b90aaf2051b2159c68f3bb8c954558989da8"}}}}} diff --git a/module/txpool/testdata/contentFrom.golden b/module/txpool/testdata/contentFrom.golden new file mode 100644 index 00000000..45ffa43e --- /dev/null +++ b/module/txpool/testdata/contentFrom.golden @@ -0,0 +1,2 @@ +> {"jsonrpc":"2.0","id":1,"method":"txpool_contentFrom","params":["0x1ba4ca9ac6ff4cf192c11e8c1624563f302caa87"]} +< {"jsonrpc":"2.0","id":1,"result":{"queued":{"183":{"blockHash":null,"blockNumber":null,"from":"0x1ba4ca9ac6ff4cf192c11e8c1624563f302caa87","gas":"0x1839a","gasPrice":"0x4a9bf00ce","maxFeePerGas":"0x4a9bf00ce","maxPriorityFeePerGas":"0x68e7780","hash":"0x222c11451c3bf3607a4ef18fa87f26575a0dca97e10ceb81b8ebebdd45d5bbfb","input":"0x","nonce":"0xb7","to":"0x1ba4ca9ac6ff4cf192c11e8c1624563f302caa87","transactionIndex":null,"value":"0x0","type":"0x2","accessList":[],"chainId":"0x1","v":"0x0","r":"0xea35c7c0643b79664b0bbb7f42d64edd371508ae4c33c1f817a80a2655465935","s":"0x76d39f794e9e1ee359d66b7d3b19b90aaf2051b2159c68f3bb8c954558989da8"}}}} diff --git a/module/txpool/testdata/status.golden b/module/txpool/testdata/status.golden new file mode 100644 index 00000000..7e419f31 --- /dev/null +++ b/module/txpool/testdata/status.golden @@ -0,0 +1,2 @@ +> {"jsonrpc":"2.0","id":1,"method":"txpool_status"} +< {"jsonrpc":"2.0","id":1,"result":{"pending":"0xa","queued":"0x7"}} diff --git a/rpctest/test_case.go b/rpctest/test_case.go index 5b1e52a2..c9f8be50 100644 --- a/rpctest/test_case.go +++ b/rpctest/test_case.go @@ -5,6 +5,7 @@ import ( "math/big" "testing" + "github.com/ethereum/go-ethereum/core/types" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/lmittmann/w3" @@ -51,7 +52,8 @@ func comp[T any](t *testing.T, wantVal, gotVal T, wantErr, gotErr error, opts .. // compare values opts = append(opts, - cmp.AllowUnexported(big.Int{}), + cmp.AllowUnexported(big.Int{}, types.Transaction{}), + cmpopts.IgnoreFields(types.Transaction{}, "time", "hash", "size", "from"), cmpopts.EquateEmpty(), ) if diff := cmp.Diff(wantVal, gotVal, opts...); diff != "" {