Skip to content

Commit 78846cf

Browse files
committed
Bundler Calls
1 parent 3c1f688 commit 78846cf

12 files changed

Lines changed: 307 additions & 68 deletions

File tree

rpccalls/alchemy.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package rpccalls
2+
3+
// USerOpV6 - standard
4+
5+
type AlchemyV7UserOp struct {
6+
Sender string `json:"sender"`
7+
Nonce string `json:"nonce"`
8+
Factory string `json:"factory"`
9+
FactoryData string `json:"factoryData"`
10+
CallData string `json:"callData"`
11+
CallGasLimit string `json:"callGasLimit"`
12+
VerificationGasLimit string `json:"verificationGasLimit"`
13+
PreVerificationGas string `json:"preVerificationGas"`
14+
MaxFeePerGas string `json:"maxFeePerGas"`
15+
MaxPriorityFeePerGas string `json:"maxPriorityFeePerGas"`
16+
PaymasterVerificationGasLimit string `json:"paymasterVerificationGasLimit"`
17+
PaymasterPostOpGasLimit string `json:"paymasterPostOpGasLimit"`
18+
Signature string `json:"signature"`
19+
Paymaster string `json:"paymaster"`
20+
PaymasterData string `json:"paymasterData"`
21+
}
22+
23+
type AlchemyEstimateGasCostResponse struct {
24+
PreVerificationGas string `json:"preVerificationGas"`
25+
CallGasLimit string `json:"callGasLimit"`
26+
VerificationGasLimit string `json:"verificationGasLimit"`
27+
}
28+
29+
/*
30+
31+
eth_sendUserOperation
32+
Submits a user operation to a Bundler. If the request is successful, the endpoint will return a user operation hash that the caller can use to look up the status of the user operation. If it fails, or another error occurs, an error code and description will be returned.
33+
eth_estimateUserOperationGas
34+
Estimates the gas values for a user operation. It returns the preVerificationGas, verificationGasLimit, and callGasLimit values associated with the provided user operation.
35+
eth_getUserOperationByHash
36+
Returns a user operation based on the given user operation hash. It returns the user operation along with extra information including what block/transaction it was included in. If the operation has not yet been included, it will return null.
37+
eth_getUserOperationReceipt
38+
Returns a user operation receipt ( metadata associated with the given user operation ) based on the given user operation hash. It returns null if the user operation has not yet been included.
39+
rundler_maxPriorityFeePerGas
40+
Returns a fee per gas that is an estimate of how much users should set as a priority fee in UOs for Rundler endpoints.
41+
eth_supportedEntryPoints
42+
Returns a list of Entrypoint contract addresses supported by the bunder endpoints.
43+
44+
*/

rpccalls/apicalls.go

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,90 @@
11
package rpccalls
22

33
import (
4+
"bytes"
45
"fmt"
56
"io"
67
"net/http"
7-
"strings"
88

99
"encoding/json"
10-
11-
"github.com/san-lab/go4337/userop"
1210
)
1311

14-
const StdBundlerCallTemplate = `curl -X GET %s/%s --header 'accept: application/json' --header 'content-type: application/json' -d '
15-
{
16-
"jsonrpc": "2.0",
17-
"id": 1,
18-
"method": "eth_supportedEntryPoints",
19-
"params": []
20-
}
21-
'
22-
`
23-
24-
func ApiCall(url, key string, methodTemplate string, usop *userop.UserOperation) ([]byte, error) {
12+
func POSTCall(url, key string, data []byte) (result []byte, aerr *APIError, err error) {
2513
client := http.Client{}
2614
url = url + "/" + key
27-
ausop := usop.ToUserOpForApi()
28-
data, err := json.Marshal(ausop)
29-
if err != nil {
30-
return nil, fmt.Errorf("could not marshal userop: %v", err)
31-
}
32-
33-
methodCall := fmt.Sprintf(methodTemplate, string(data))
34-
//fmt.Println("methodCall:", methodCall)
35-
buf := strings.NewReader(methodCall)
15+
buf := bytes.NewBuffer(data)
16+
//fmt.Println("POSTing", url, "with", string(data))
3617
req, err := http.NewRequest("POST", url, nil)
3718
if err != nil {
38-
//fmt.Println(err)
39-
return nil, err
19+
return nil, nil, err
4020
}
4121
req.Header.Set("accept", "application/json")
4222
req.Header.Set("content-type", "application/json")
4323
req.Body = io.NopCloser(buf)
4424
resp, err := client.Do(req)
4525
if err != nil {
46-
return nil, fmt.Errorf("could not do request: %v", err)
26+
return nil, nil, fmt.Errorf("could not do request: %v", err)
27+
}
28+
defer resp.Body.Close()
29+
resbts, err := io.ReadAll(resp.Body)
30+
if err != nil {
31+
return nil, nil, fmt.Errorf("could not read response: %v", err)
32+
}
33+
aresp := &APIRPCResponse{}
34+
err = json.Unmarshal(resbts, aresp)
35+
if err != nil {
36+
return nil, nil, fmt.Errorf("could not unmarshal response: %v", err, string(resbts))
37+
}
38+
if aresp.Error.Code != 0 {
39+
return nil, &aresp.Error, nil
40+
}
41+
return aresp.Result, nil, nil
42+
}
43+
44+
func ApiFreeHandCall(url, key, methodTemplate string, params ...interface{}) (result []byte, aerr *APIError, err error) {
45+
sparams := make([]any, len(params))
46+
for i, p := range params {
47+
sparams[i], err = json.Marshal(p)
48+
if err != nil {
49+
return nil, nil, fmt.Errorf("could not marshal param: %v", err)
50+
}
51+
}
52+
calldat := fmt.Sprintf(methodTemplate, sparams...)
53+
return POSTCall(url, key, []byte(calldat))
54+
55+
}
56+
57+
// Returns unparsed "Result" if successful, otherwise nil and either *APIError or error
58+
func ApiCall(url, key string, ar *APIRequest) ([]byte, *APIError, error) {
59+
data, err := ar.ToJSON()
60+
if err != nil {
61+
return nil, nil, fmt.Errorf("could not marshal APIRequest: %v", err, ar)
4762
}
48-
return io.ReadAll(resp.Body)
4963

64+
return POSTCall(url, key, data)
65+
66+
}
67+
68+
type APIRequest struct {
69+
ID int `json:"id"`
70+
Jsonrpc string `json:"jsonrpc"`
71+
Method string `json:"method"`
72+
Params []interface{} `json:"params"`
73+
}
74+
75+
func (ar *APIRequest) ToJSON() ([]byte, error) {
76+
return json.Marshal(ar)
77+
}
78+
79+
type APIRPCResponse struct {
80+
ID int `json:"id"`
81+
Jsonrpc string `json:"jsonrpc"`
82+
Result json.RawMessage `json:"result,omitempty"`
83+
Error APIError `json:"error,omitempty"`
84+
}
85+
86+
type APIError struct {
87+
Code int `json:"code"`
88+
Data json.RawMessage `json:"data"`
89+
Message string `json:"message"`
5090
}

rpccalls/call.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ func CreateSignedTransaction(rpc *state.RPCEndpoint, from, to common.Address, va
2828
if err != nil {
2929
return nil, fmt.Errorf("could not get gas price: %v", err)
3030
}
31+
gasPrice = gasPrice.Add(
32+
gasPrice,
33+
new(big.Int).Div(gasPrice, big.NewInt(5))) // 20% more than suggested
3134

3235
nonce, err := client.PendingNonceAt(context.Background(), from)
3336
if err != nil {

rpccalls/servicecalls.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func GetWalletNonce(rpc *state.RPCEndpoint, addr common.Address) (uint64, error)
3232

3333
}
3434

35-
func GetStdNonce(rpc *state.RPCEndpoint, addr common.Address) (uint64, error) {
35+
func GetPendingNonce(rpc *state.RPCEndpoint, addr common.Address) (uint64, error) {
3636
client, err := ethclient.Dial(rpc.URL)
3737
if err != nil {
3838
return 0, fmt.Errorf("could not connect to rpc: %v", err)
@@ -44,6 +44,18 @@ func GetStdNonce(rpc *state.RPCEndpoint, addr common.Address) (uint64, error) {
4444
return nonce, nil
4545
}
4646

47+
func GetNonce(rpc *state.RPCEndpoint, addr common.Address) (uint64, error) {
48+
client, err := ethclient.Dial(rpc.URL)
49+
if err != nil {
50+
return 0, fmt.Errorf("could not connect to rpc: %v", err)
51+
}
52+
nonce, err := client.NonceAt(context.Background(), addr, nil)
53+
if err != nil {
54+
return 0, fmt.Errorf("could not get nonce: %v", err)
55+
}
56+
return nonce, nil
57+
}
58+
4759
func GetBalance(rpc *state.RPCEndpoint, addr common.Address) (*big.Int, error) {
4860
client, err := ethclient.Dial(rpc.URL)
4961
if err != nil {

rpccalls/stackup.go

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,57 @@
11
package rpccalls
22

3+
import (
4+
"encoding/hex"
5+
"encoding/json"
6+
"fmt"
7+
"strconv"
8+
9+
"github.com/ethereum/go-ethereum/common"
10+
"github.com/san-lab/go4337/userop"
11+
)
12+
313
const StackupAPIDefURL = "https://api.stackup.sh/v1/paymaster/"
414

5-
/*
6-
*/
15+
const StackUpPMPayTemplate = "{\"jsonrpc\": \"2.0\",\"id\": 1,\"method\": \"pm_sponsorUserOperation\",\"params\": [%s,\"0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789\",{\"type\": \"payg\"}]}"
16+
const StackUpSendOpTemplate = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_sendUserOperation\",\"params\":[%s,\"0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789\" ]}"
17+
18+
type StackUpPMPayGResult struct {
19+
PaymasterAndData string `json:"paymasterAndData"`
20+
PreVerificationGas string `json:"preVerificationGas"`
21+
VerificationGasLimit string `json:"verificationGasLimit"`
22+
CallGasLimit string `json:"callGasLimit"`
23+
}
24+
25+
func StackUpPMPayCall(url, key string, usop *userop.UserOpForApiV6) (*StackUpPMPayGResult, *APIError, error) {
26+
ar := &APIRequest{
27+
ID: 1,
28+
Jsonrpc: "2.0",
29+
Method: "pm_sponsorUserOperation",
30+
Params: []interface{}{usop, "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", map[string]string{"type": "payg"}},
31+
}
32+
bt, aerr, err := ApiCall(url, key, ar)
33+
if err != nil {
34+
return nil, nil, fmt.Errorf("API Call error: %v", err)
35+
}
36+
if aerr != nil {
37+
return nil, aerr, nil
38+
}
39+
res := &StackUpPMPayGResult{}
40+
err = json.Unmarshal(bt, res)
41+
if err != nil {
42+
return nil, nil, fmt.Errorf("could not unmarshal response: %v", err)
43+
}
44+
return res, nil, nil
45+
46+
}
47+
48+
func IncorporateStackUpPMResToUserOp(usop *userop.UserOperation, res *StackUpPMPayGResult) error {
49+
pma := common.HexToAddress(res.PaymasterAndData[:42])
50+
usop.Paymaster = &pma
51+
usop.PaymasterData, _ = hex.DecodeString(res.PaymasterAndData[42:])
52+
53+
usop.PreVerificationGas, _ = strconv.ParseUint(res.PreVerificationGas[2:], 16, 64)
54+
usop.VerificationGasLimit, _ = strconv.ParseUint(res.VerificationGasLimit[2:], 16, 64)
55+
usop.CallGasLimit, _ = strconv.ParseUint(res.CallGasLimit[2:], 16, 64)
56+
return nil
57+
}

ui/apisui.go

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,17 @@ import (
1212
var ApiCallsItem = &Item{Label: "API Calls", Details: "Call APIs"}
1313
var ApiKeyItem = &Item{Label: "API Key"}
1414
var ApiURLItem = &Item{Label: "API URL"}
15-
var ApiMethodItem = &Item{Label: "API Method"}
15+
var StackUpPMApiItem = &Item{Label: "StackUp Paymaster API"}
16+
var StackUpBNApiItem = &Item{Label: "StackUp Bundler API"}
17+
var ApiMethodItem = &Item{Label: "API Method (free-hand)"}
1618

1719
var ApiUserOpItem = &Item{Label: "User Operation"}
1820
var ApiCallItem = &Item{Label: "Call API"}
1921

2022
func ApiKeysUI(usop *userop.UserOperation) {
2123
var key, url, mtemplate string
2224
var ok, allOk bool
25+
var keyAndUrl bool
2326
if usop != nil {
2427
ApiUserOpItem.Value = usop
2528
}
@@ -31,6 +34,7 @@ func ApiKeysUI(usop *userop.UserOperation) {
3134
if ApiURLItem.Value != nil {
3235
url, ok = ApiURLItem.Value.(string)
3336
allOk = allOk && ok
37+
keyAndUrl = allOk
3438
}
3539
if ApiMethodItem.Value != nil {
3640
mtemplate, ok = ApiMethodItem.Value.(string)
@@ -39,8 +43,12 @@ func ApiKeysUI(usop *userop.UserOperation) {
3943
if ApiUserOpItem.Value != nil {
4044
usop, ok = ApiUserOpItem.Value.(*userop.UserOperation)
4145
allOk = allOk && ok
46+
keyAndUrl = keyAndUrl && ok
47+
}
48+
items := []*Item{ApiKeyItem, ApiURLItem, ApiUserOpItem}
49+
if keyAndUrl {
50+
items = append(items, StackUpPMApiItem, StackUpBNApiItem, ApiMethodItem)
4251
}
43-
items := []*Item{ApiKeyItem, ApiURLItem, ApiMethodItem, ApiUserOpItem}
4452
if allOk {
4553
items = append(items, ApiCallItem)
4654
}
@@ -55,6 +63,25 @@ func ApiKeysUI(usop *userop.UserOperation) {
5563
switch sel {
5664
case Back.Label:
5765
return
66+
case StackUpPMApiItem.Label:
67+
res, aerr, err := rpccalls.StackUpPMPayCall(url, key, usop.ToUserOpForApiV6())
68+
if err != nil {
69+
fmt.Println("Error making API call:", err)
70+
} else if aerr != nil {
71+
fmt.Printf("Error from API call: %v, %s, %s\n", aerr.Code, aerr.Message, string(aerr.Data))
72+
} else {
73+
74+
fmt.Printf("API call result:\n CallGasLimit: %s (0x%x)\n PreVerificationGas: %s (0x%x)\n VerificationGasLimit: %s (0x%x)\n PaymasterAndData: %s\n",
75+
res.CallGasLimit, usop.CallGasLimit,
76+
res.PreVerificationGas, usop.PreVerificationGas,
77+
res.VerificationGasLimit, usop.VerificationGasLimit,
78+
res.PaymasterAndData)
79+
if YesNoPromptUI("Incorporate to the UserOp?") {
80+
rpccalls.IncorporateStackUpPMResToUserOp(usop, res)
81+
state.Save()
82+
}
83+
}
84+
5885
case ApiKeyItem.Label:
5986
_, name, key, good := StringFromDictionaryUI(state.ApiKeysLabel)
6087
if good {
@@ -76,11 +103,15 @@ func ApiKeysUI(usop *userop.UserOperation) {
76103
case ApiUserOpItem.Label:
77104
SelectUserOpUI(ApiUserOpItem)
78105
case ApiCallItem.Label:
79-
ret, err := rpccalls.ApiCall(url, key, mtemplate, usop)
106+
ret, aerr, err := rpccalls.ApiFreeHandCall(url, key, mtemplate, usop.ToUserOpForApiV6())
80107
if err != nil {
81-
fmt.Println("Error in API call:", err)
108+
fmt.Println("Error making API call:", err)
109+
} else if aerr != nil {
110+
fmt.Printf("Error from API call: %v, %s, %s\n", aerr.Code, aerr.Message, string(aerr.Data))
111+
82112
} else {
83113
fmt.Println("API call result:", string(ret))
114+
84115
}
85116
default:
86117
fmt.Println("Not implemented yet:", sel)

ui/chaincall.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,8 @@ func GenericRPCCallUI() {
202202
}
203203
switch sel {
204204
case TargetContractItem.Label:
205-
_, addressOk = TargetContractItem.Value.(*common.Address)
205+
_, TargetContractItem.Value, addressOk = AddressFromBookUI("Target Contract")
206+
206207
case Back.Label:
207208
return
208209

@@ -222,9 +223,10 @@ func GenericRPCCallUI() {
222223
}
223224

224225
func UtilCallUI(endpoint *state.RPCEndpoint, account common.Address) {
226+
PendingNonceItem := &Item{Label: "Get Pending Nonce", Details: "Get pending nonce at address"}
225227
NonceItem := &Item{Label: "Get Nonce", Details: "Get nonce at address"}
226228
BalanceItem := &Item{Label: "Get Balance", Details: "Get balance at address"}
227-
Items := []*Item{NonceItem, BalanceItem, Back}
229+
Items := []*Item{PendingNonceItem, NonceItem, BalanceItem, Back}
228230
prompt := promptui.Select{
229231
Label: "Select action",
230232
Items: Items,
@@ -238,8 +240,15 @@ func UtilCallUI(endpoint *state.RPCEndpoint, account common.Address) {
238240
return
239241
}
240242
switch sel {
243+
case PendingNonceItem.Label:
244+
nonce, err := rpccalls.GetPendingNonce(endpoint, account)
245+
if err != nil {
246+
fmt.Println(err)
247+
} else {
248+
fmt.Printf("Pending Nonce at %s: %v\n", account, nonce)
249+
}
241250
case NonceItem.Label:
242-
nonce, err := rpccalls.GetStdNonce(endpoint, account)
251+
nonce, err := rpccalls.GetNonce(endpoint, account)
243252
if err != nil {
244253
fmt.Println(err)
245254
} else {

ui/commoninputs.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,12 @@ func MultiLineInput(label string) (string, error) {
178178
return multiLineInput, nil
179179
}
180180

181+
func YesNoPromptUI(label string) bool {
182+
prpt := promptui.Prompt{Label: label + "(yes/no)", Default: "no"}
183+
y, err := prpt.Run()
184+
return err == nil && y == "yes"
185+
}
186+
181187
func InputNewAddressUI(label string) (string, *common.Address, error) {
182188
prompt := promptui.Prompt{
183189
Label: "Name for " + label,

0 commit comments

Comments
 (0)