Skip to content
This repository was archived by the owner on Oct 20, 2024. It is now read-only.

Commit 26551c2

Browse files
authored
Add RPC method for eth_getUserOperationByHash (#83)
1 parent d3a9216 commit 26551c2

File tree

9 files changed

+239
-37
lines changed

9 files changed

+239
-37
lines changed

internal/start/private.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ func PrivateMode() {
9191
c.SetGetUserOpReceiptFunc(client.GetUserOpReceiptWithEthClient(eth))
9292
c.SetGetSimulateValidationFunc(client.GetSimulateValidationWithRpcClient(rpc))
9393
c.SetGetCallGasEstimateFunc(client.GetCallGasEstimateWithEthClient(eth))
94+
c.SetGetUserOpByHashFunc(client.GetUserOpByHashWithEthClient(eth))
9495
c.UseLogger(logr)
9596
c.UseModules(
9697
check.ValidateOpValues(),

pkg/client/client.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type Client struct {
2828
getUserOpReceipt GetUserOpReceiptFunc
2929
getSimulateValidation GetSimulateValidationFunc
3030
getCallGasEstimate GetCallGasEstimateFunc
31+
getUserOpByHash GetUserOpByHashFunc
3132
}
3233

3334
// New initializes a new ERC-4337 client which can be extended with modules for validating UserOperations
@@ -46,6 +47,7 @@ func New(
4647
getUserOpReceipt: getUserOpReceiptNoop(),
4748
getSimulateValidation: getSimulateValidationNoop(),
4849
getCallGasEstimate: getCallGasEstimateNoop(),
50+
getUserOpByHash: getUserOpByHashNoop(),
4951
}
5052
}
5153

@@ -87,6 +89,12 @@ func (i *Client) SetGetCallGasEstimateFunc(fn GetCallGasEstimateFunc) {
8789
i.getCallGasEstimate = fn
8890
}
8991

92+
// SetGetUserOpByHashFunc defines a general function for fetching a userOp given a userOpHash, EntryPoint
93+
// address, and chain ID. This function is called in *Client.GetUserOperationByHash.
94+
func (i *Client) SetGetUserOpByHashFunc(fn GetUserOpByHashFunc) {
95+
i.getUserOpByHash = fn
96+
}
97+
9098
// SendUserOperation implements the method call for eth_sendUserOperation.
9199
// It returns true if userOp was accepted otherwise returns an error.
92100
func (i *Client) SendUserOperation(op map[string]any, ep string) (string, error) {
@@ -199,6 +207,21 @@ func (i *Client) GetUserOperationReceipt(
199207
return ev, nil
200208
}
201209

210+
// GetUserOperationByHash returns a UserOperation based on a given userOpHash returned by
211+
// *Client.SendUserOperation.
212+
func (i *Client) GetUserOperationByHash(hash string) (*entrypoint.HashLookupResult, error) {
213+
// Init logger
214+
l := i.logger.WithName("eth_getUserOperationByHash").WithValues("userop_hash", hash)
215+
216+
res, err := i.getUserOpByHash(hash, i.supportedEntryPoints[0], i.chainID)
217+
if err != nil {
218+
l.Error(err, "eth_getUserOperationByHash error")
219+
return nil, err
220+
}
221+
222+
return res, nil
223+
}
224+
202225
// SupportedEntryPoints implements the method call for eth_supportedEntryPoints. It returns the array of
203226
// EntryPoint addresses that is supported by the client. The first address in the array is the preferred
204227
// EntryPoint.

pkg/client/rpc.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ func (r *RpcAdapter) Eth_getUserOperationReceipt(
3535
return r.client.GetUserOperationReceipt(userOpHash)
3636
}
3737

38+
// Eth_getUserOperationByHash routes method calls to *Client.GetUserOperationByHash.
39+
func (r *RpcAdapter) Eth_getUserOperationByHash(
40+
userOpHash string,
41+
) (*entrypoint.HashLookupResult, error) {
42+
return r.client.GetUserOperationByHash(userOpHash)
43+
}
44+
3845
// Eth_supportedEntryPoints routes method calls to *Client.SupportedEntryPoints.
3946
func (r *RpcAdapter) Eth_supportedEntryPoints() ([]string, error) {
4047
return r.client.SupportedEntryPoints()

pkg/client/utils.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package client
22

33
import (
44
"errors"
5+
"math/big"
56

67
"github.com/ethereum/go-ethereum/common"
78
"github.com/ethereum/go-ethereum/ethclient"
@@ -67,3 +68,22 @@ func GetCallGasEstimateWithEthClient(eth *ethclient.Client) GetCallGasEstimateFu
6768
return gas.CallGasEstimate(eth, ep, op)
6869
}
6970
}
71+
72+
// GetUserOpByHashFunc is a general interface for fetching a UserOperation given a userOpHash, EntryPoint
73+
// address, and chain ID.
74+
type GetUserOpByHashFunc func(hash string, ep common.Address, chain *big.Int) (*entrypoint.HashLookupResult, error)
75+
76+
func getUserOpByHashNoop() GetUserOpByHashFunc {
77+
return func(hash string, ep common.Address, chain *big.Int) (*entrypoint.HashLookupResult, error) {
78+
//lint:ignore ST1005 This needs to match the bundler test spec.
79+
return nil, errors.New("Missing/invalid userOpHash")
80+
}
81+
}
82+
83+
// GetUserOpByHashWithEthClient returns an implementation of GetUserOpByHashFunc that relies on an eth client
84+
// to fetch a UserOperation.
85+
func GetUserOpByHashWithEthClient(eth *ethclient.Client) GetUserOpByHashFunc {
86+
return func(hash string, ep common.Address, chain *big.Int) (*entrypoint.HashLookupResult, error) {
87+
return entrypoint.GetUserOperationByHash(eth, hash, ep, chain)
88+
}
89+
}

pkg/entrypoint/filter.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package entrypoint
2+
3+
import (
4+
"context"
5+
"math/big"
6+
7+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
8+
"github.com/ethereum/go-ethereum/common"
9+
"github.com/ethereum/go-ethereum/ethclient"
10+
)
11+
12+
func filterUserOperationEvent(
13+
eth *ethclient.Client,
14+
userOpHash string,
15+
entryPoint common.Address,
16+
) (*EntrypointUserOperationEventIterator, error) {
17+
ep, err := NewEntrypoint(entryPoint, eth)
18+
if err != nil {
19+
return nil, err
20+
}
21+
bn, err := eth.BlockNumber(context.Background())
22+
if err != nil {
23+
return nil, err
24+
}
25+
toBlk := big.NewInt(0).SetUint64(bn)
26+
startBlk := big.NewInt(0)
27+
sub10kBlk := big.NewInt(0).Sub(toBlk, big.NewInt(10000))
28+
if sub10kBlk.Cmp(startBlk) > 0 {
29+
startBlk = sub10kBlk
30+
}
31+
32+
return ep.FilterUserOperationEvent(
33+
&bind.FilterOpts{Start: startBlk.Uint64()},
34+
[][32]byte{common.HexToHash(userOpHash)},
35+
[]common.Address{},
36+
[]common.Address{},
37+
)
38+
}

pkg/entrypoint/methods.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import (
1010
)
1111

1212
var (
13-
bytes32, _ = abi.NewType("bytes32", "", nil)
14-
uint256, _ = abi.NewType("uint256", "", nil)
15-
bytes, _ = abi.NewType("bytes", "", nil)
13+
bytes32, _ = abi.NewType("bytes32", "", nil)
14+
uint256, _ = abi.NewType("uint256", "", nil)
15+
bytes, _ = abi.NewType("bytes", "", nil)
16+
address, _ = abi.NewType("address", "", nil)
17+
1618
validatePaymasterUserOpMethod = abi.NewMethod(
1719
"validatePaymasterUserOp",
1820
"validatePaymasterUserOp",
@@ -31,6 +33,21 @@ var (
3133
},
3234
)
3335
validatePaymasterUserOpSelector = hexutil.Encode(validatePaymasterUserOpMethod.ID)
36+
37+
handleOpsMethod = abi.NewMethod(
38+
"handleOps",
39+
"handleOps",
40+
abi.Function,
41+
"",
42+
false,
43+
false,
44+
abi.Arguments{
45+
{Name: "ops", Type: userop.UserOpArr},
46+
{Name: "beneficiary", Type: address},
47+
},
48+
nil,
49+
)
50+
handleOpsSelector = hexutil.Encode(handleOpsMethod.ID)
3451
)
3552

3653
type validatePaymasterUserOpOutput struct {

pkg/entrypoint/opbyhash.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package entrypoint
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"math/big"
9+
"strings"
10+
11+
"github.com/ethereum/go-ethereum/common"
12+
"github.com/ethereum/go-ethereum/common/hexutil"
13+
"github.com/ethereum/go-ethereum/ethclient"
14+
"github.com/stackup-wallet/stackup-bundler/pkg/userop"
15+
)
16+
17+
type HashLookupResult struct {
18+
UserOperation *userop.UserOperation `json:"userOperation"`
19+
EntryPoint string `json:"entryPoint"`
20+
BlockNumber *big.Int `json:"blockNumber"`
21+
BlockHash common.Hash `json:"blockHash"`
22+
TransactionHash common.Hash `json:"transactionHash"`
23+
}
24+
25+
// GetUserOperationByHash filters the EntryPoint contract for UserOperationEvents and returns the
26+
// corresponding UserOp from a given userOpHash.
27+
func GetUserOperationByHash(
28+
eth *ethclient.Client,
29+
userOpHash string,
30+
entryPoint common.Address,
31+
chainID *big.Int,
32+
) (*HashLookupResult, error) {
33+
it, err := filterUserOperationEvent(eth, userOpHash, entryPoint)
34+
if err != nil {
35+
return nil, err
36+
}
37+
38+
if it.Next() {
39+
receipt, err := eth.TransactionReceipt(context.Background(), it.Event.Raw.TxHash)
40+
if err != nil {
41+
return nil, err
42+
}
43+
tx, isPending, err := eth.TransactionByHash(context.Background(), it.Event.Raw.TxHash)
44+
if err != nil {
45+
return nil, err
46+
} else if isPending {
47+
//lint:ignore ST1005 This needs to match the bundler test spec.
48+
return nil, errors.New("Missing/invalid userOpHash")
49+
}
50+
51+
hex := hexutil.Encode(tx.Data())
52+
if strings.HasPrefix(hex, handleOpsSelector) {
53+
data := common.Hex2Bytes(hex[len(handleOpsSelector):])
54+
args, err := handleOpsMethod.Inputs.Unpack(data)
55+
if err != nil {
56+
return nil, err
57+
}
58+
if len(args) != 2 {
59+
return nil, fmt.Errorf(
60+
"handleOps: invalid input length: expected 2, got %d",
61+
len(args),
62+
)
63+
}
64+
65+
// TODO: Find better way to convert this
66+
ops, ok := args[0].([]struct {
67+
Sender common.Address `json:"sender"`
68+
Nonce *big.Int `json:"nonce"`
69+
InitCode []uint8 `json:"initCode"`
70+
CallData []uint8 `json:"callData"`
71+
CallGasLimit *big.Int `json:"callGasLimit"`
72+
VerificationGasLimit *big.Int `json:"verificationGasLimit"`
73+
PreVerificationGas *big.Int `json:"preVerificationGas"`
74+
MaxFeePerGas *big.Int `json:"maxFeePerGas"`
75+
MaxPriorityFeePerGas *big.Int `json:"maxPriorityFeePerGas"`
76+
PaymasterAndData []uint8 `json:"paymasterAndData"`
77+
Signature []uint8 `json:"signature"`
78+
})
79+
if !ok {
80+
return nil, errors.New("handleOps: cannot assert type: ops is not of type []struct{...}")
81+
}
82+
83+
for _, abiOp := range ops {
84+
data, err := json.Marshal(abiOp)
85+
if err != nil {
86+
return nil, err
87+
}
88+
89+
var op userop.UserOperation
90+
if err = json.Unmarshal(data, &op); err != nil {
91+
return nil, err
92+
}
93+
94+
if op.GetUserOpHash(entryPoint, chainID).String() == userOpHash {
95+
return &HashLookupResult{
96+
UserOperation: &op,
97+
EntryPoint: entryPoint.String(),
98+
BlockNumber: receipt.BlockNumber,
99+
BlockHash: receipt.BlockHash,
100+
TransactionHash: it.Event.Raw.TxHash,
101+
}, nil
102+
}
103+
}
104+
}
105+
106+
}
107+
108+
//lint:ignore ST1005 This needs to match the bundler test spec.
109+
return nil, errors.New("Missing/invalid userOpHash")
110+
}

pkg/entrypoint/receipt.go

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"errors"
66
"math/big"
77

8-
"github.com/ethereum/go-ethereum/accounts/abi/bind"
98
"github.com/ethereum/go-ethereum/common"
109
"github.com/ethereum/go-ethereum/common/hexutil"
1110
"github.com/ethereum/go-ethereum/core/types"
@@ -45,30 +44,11 @@ func GetUserOperationReceipt(
4544
userOpHash string,
4645
entryPoint common.Address,
4746
) (*UserOperationReceipt, error) {
48-
ep, err := NewEntrypoint(entryPoint, eth)
47+
it, err := filterUserOperationEvent(eth, userOpHash, entryPoint)
4948
if err != nil {
5049
return nil, err
5150
}
52-
bn, err := eth.BlockNumber(context.Background())
53-
if err != nil {
54-
return nil, err
55-
}
56-
toBlk := big.NewInt(0).SetUint64(bn)
57-
startBlk := big.NewInt(0)
58-
sub10kBlk := big.NewInt(0).Sub(toBlk, big.NewInt(10000))
59-
if sub10kBlk.Cmp(startBlk) > 0 {
60-
startBlk = sub10kBlk
61-
}
6251

63-
it, err := ep.FilterUserOperationEvent(
64-
&bind.FilterOpts{Start: startBlk.Uint64()},
65-
[][32]byte{common.HexToHash(userOpHash)},
66-
[]common.Address{},
67-
[]common.Address{},
68-
)
69-
if err != nil {
70-
return nil, err
71-
}
7252
if it.Next() {
7353
receipt, err := eth.TransactionReceipt(context.Background(), it.Event.Raw.TxHash)
7454
if err != nil {

pkg/userop/object.go

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,26 @@ import (
1212
)
1313

1414
var (
15+
// UserOpPrimitives is the primitive ABI types for each UserOperation field.
16+
UserOpPrimitives = []abi.ArgumentMarshaling{
17+
{Name: "sender", InternalType: "Sender", Type: "address"},
18+
{Name: "nonce", InternalType: "Nonce", Type: "uint256"},
19+
{Name: "initCode", InternalType: "InitCode", Type: "bytes"},
20+
{Name: "callData", InternalType: "CallData", Type: "bytes"},
21+
{Name: "callGasLimit", InternalType: "CallGasLimit", Type: "uint256"},
22+
{Name: "verificationGasLimit", InternalType: "VerificationGasLimit", Type: "uint256"},
23+
{Name: "preVerificationGas", InternalType: "PreVerificationGas", Type: "uint256"},
24+
{Name: "maxFeePerGas", InternalType: "MaxFeePerGas", Type: "uint256"},
25+
{Name: "maxPriorityFeePerGas", InternalType: "MaxPriorityFeePerGas", Type: "uint256"},
26+
{Name: "paymasterAndData", InternalType: "PaymasterAndData", Type: "bytes"},
27+
{Name: "signature", InternalType: "Signature", Type: "bytes"},
28+
}
29+
1530
// UserOpType is the ABI type of a UserOperation.
16-
UserOpType, _ = abi.NewType("tuple", "userOp", []abi.ArgumentMarshaling{
17-
{Name: "Sender", Type: "address"},
18-
{Name: "Nonce", Type: "uint256"},
19-
{Name: "InitCode", Type: "bytes"},
20-
{Name: "CallData", Type: "bytes"},
21-
{Name: "CallGasLimit", Type: "uint256"},
22-
{Name: "VerificationGasLimit", Type: "uint256"},
23-
{Name: "PreVerificationGas", Type: "uint256"},
24-
{Name: "MaxFeePerGas", Type: "uint256"},
25-
{Name: "MaxPriorityFeePerGas", Type: "uint256"},
26-
{Name: "PaymasterAndData", Type: "bytes"},
27-
{Name: "Signature", Type: "bytes"},
28-
})
31+
UserOpType, _ = abi.NewType("tuple", "op", UserOpPrimitives)
32+
33+
// UserOpArr is the ABI type for an array of UserOperations.
34+
UserOpArr, _ = abi.NewType("tuple[]", "ops", UserOpPrimitives)
2935
)
3036

3137
func getAbiArgs() abi.Arguments {

0 commit comments

Comments
 (0)