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 != "" {