Skip to content

Commit 2fc319d

Browse files
authored
docs(taiko): autogen json-rpc docs (#417)
* init script * . * . * . * . * rename * rename * lint * apply changes
1 parent 2f9a84e commit 2fc319d

File tree

3 files changed

+369
-0
lines changed

3 files changed

+369
-0
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Taiko Geth JSON-RPC Autogen Docs
2+
3+
on:
4+
push:
5+
branches: [taiko]
6+
7+
jobs:
8+
generate-and-deploy:
9+
runs-on: [arc-runner-set]
10+
permissions:
11+
contents: write
12+
steps:
13+
- name: Checkout repository
14+
uses: actions/checkout@v4
15+
16+
- name: Set up Go
17+
uses: actions/setup-go@v5
18+
with:
19+
go-version: "1.23"
20+
21+
- name: Set up node
22+
uses: actions/setup-node@v4
23+
with:
24+
node-version: "20"
25+
26+
- name: Install jrgen
27+
run: |
28+
npm install -g jrgen
29+
30+
- name: Run script
31+
run: |
32+
go run ./scripts/generate-jsonrpc.go
33+
jrgen docs-html jrgen.json
34+
mv taiko-json-rpc-api-reference.html index.html
35+
36+
- name: Deploy to GitHub Pages
37+
uses: peaceiris/actions-gh-pages@v4
38+
with:
39+
github_token: ${{ secrets.GITHUB_TOKEN }}
40+
publish_dir: ./ # Set this to where your `index.html` is located
41+
publish_branch: gh-pages
42+
destination_dir: taiko-geth-json-rpc

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ The codebase is based on [go-ethereum v1.15.5](https://github.com/ethereum/go-et
88

99
All source code changes by [taiko](https://taiko.xyz) team are commented with `"CHANGE(taiko): ...."`, and the filenames of all additional files have the prefix `"taiko_"` (`taiko_*.go`).
1010

11+
## JSON-RPC API
12+
13+
JSON-RPC documentation for taiko-specific JSON-RPC calls can be found [here](https://taikoxyz.github.io/taiko-geth/taiko-geth-json-rpc/).
14+
1115
## Go Ethereum
1216

1317
Golang execution layer implementation of the Ethereum protocol.

scripts/taiko_generate_jsonrpc.go

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"go/ast"
7+
"go/parser"
8+
"go/token"
9+
"log"
10+
"os"
11+
"reflect"
12+
"strings"
13+
14+
"github.com/ethereum/go-ethereum/eth"
15+
)
16+
17+
type JRGenSpec struct {
18+
Schema string `json:"$schema"`
19+
JRGen string `json:"jrgen"`
20+
JSONRPC string `json:"jsonrpc"`
21+
Info Info `json:"info"`
22+
Definitions map[string]interface{} `json:"definitions"`
23+
Methods map[string]Method `json:"methods"`
24+
}
25+
26+
type Info struct {
27+
Title string `json:"title"`
28+
Description []string `json:"description"`
29+
Version string `json:"version"`
30+
Servers []Server `json:"servers,omitempty"`
31+
}
32+
33+
type Server struct {
34+
URL string `json:"url"`
35+
Description string `json:"description"`
36+
}
37+
38+
type Method struct {
39+
Summary string `json:"summary"`
40+
Description string `json:"description"`
41+
Tags []string `json:"tags,omitempty"`
42+
Params map[string]interface{} `json:"params,omitempty"`
43+
Result map[string]interface{} `json:"result,omitempty"`
44+
}
45+
46+
func parseParamNamesFromFile(filename string) (map[string][]string, map[string]string, error) {
47+
fset := token.NewFileSet()
48+
fileAST, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
49+
if err != nil {
50+
return nil, nil, err
51+
}
52+
53+
methodParams := make(map[string][]string)
54+
methodDocs := make(map[string]string)
55+
56+
for _, decl := range fileAST.Decls {
57+
if funcDecl, ok := decl.(*ast.FuncDecl); ok && funcDecl.Recv != nil {
58+
recvField := funcDecl.Recv.List[0]
59+
var recvTypeStr string
60+
61+
switch t := recvField.Type.(type) {
62+
case *ast.StarExpr:
63+
if ident, ok := t.X.(*ast.Ident); ok {
64+
recvTypeStr = "*" + ident.Name
65+
}
66+
case *ast.Ident:
67+
recvTypeStr = t.Name
68+
}
69+
70+
if recvTypeStr == "*TaikoAPIBackend" || recvTypeStr == "*TaikoAuthAPIBackend" {
71+
var params []string
72+
if funcDecl.Type.Params != nil {
73+
for _, field := range funcDecl.Type.Params.List {
74+
if len(field.Names) > 0 {
75+
for _, name := range field.Names {
76+
params = append(params, name.Name)
77+
}
78+
} else {
79+
params = append(params, exprToString(field.Type))
80+
}
81+
}
82+
}
83+
key := recvTypeStr + "." + funcDecl.Name.Name
84+
methodParams[key] = params
85+
86+
if funcDecl.Doc != nil {
87+
methodDocs[key] = strings.TrimSpace(funcDecl.Doc.Text())
88+
}
89+
}
90+
}
91+
}
92+
93+
return methodParams, methodDocs, nil
94+
}
95+
96+
func exprToString(expr ast.Expr) string {
97+
switch t := expr.(type) {
98+
case *ast.Ident:
99+
return t.Name
100+
case *ast.StarExpr:
101+
return "*" + exprToString(t.X)
102+
case *ast.ArrayType:
103+
return "[]" + exprToString(t.Elt)
104+
case *ast.SelectorExpr:
105+
return exprToString(t.X) + "." + t.Sel.Name
106+
default:
107+
return fmt.Sprintf("%T", expr)
108+
}
109+
}
110+
111+
func generateReturnSchema(goType reflect.Type) map[string]interface{} {
112+
switch goType.Kind() {
113+
case reflect.Ptr:
114+
return map[string]interface{}{
115+
"$ref": "#/definitions/" + goType.String(),
116+
}
117+
case reflect.Slice:
118+
elem := goType.Elem()
119+
if elem.Kind() == reflect.Ptr {
120+
return map[string]interface{}{
121+
"type": "array",
122+
"items": map[string]interface{}{
123+
"$ref": "#/definitions/" + elem.String(),
124+
},
125+
}
126+
}
127+
return map[string]interface{}{
128+
"type": "array",
129+
"items": generateTypeRef(elem),
130+
}
131+
default:
132+
return generateTypeRef(goType)
133+
}
134+
}
135+
136+
func generateTypeRef(goType reflect.Type) map[string]interface{} {
137+
switch goType.Kind() {
138+
case reflect.Ptr:
139+
switch goType.String() {
140+
case "*miner.PreBuiltTxList":
141+
return map[string]interface{}{
142+
"$ref": "#/definitions/*miner.PreBuiltTxList",
143+
}
144+
case "*big.Int":
145+
return map[string]interface{}{
146+
"$ref": "#/definitions/*big.Int",
147+
}
148+
case "*rawdb.L1Origin":
149+
return map[string]interface{}{
150+
"$ref": "#/definitions/*rawdb.L1Origin",
151+
}
152+
case "*math.HexOrDecimal256":
153+
return map[string]interface{}{
154+
"$ref": "#/definitions/*math.HexOrDecimal256",
155+
}
156+
}
157+
case reflect.Slice:
158+
elem := goType.Elem()
159+
if elem.Kind() == reflect.Ptr {
160+
return map[string]interface{}{
161+
"type": "array",
162+
"items": map[string]interface{}{
163+
"$ref": "#/definitions/" + elem.String(),
164+
},
165+
}
166+
}
167+
return map[string]interface{}{
168+
"type": "array",
169+
"items": generateTypeRef(elem),
170+
}
171+
case reflect.String:
172+
return map[string]interface{}{
173+
"type": "string",
174+
"examples": []string{"0x123456"},
175+
}
176+
case reflect.Int, reflect.Int64, reflect.Uint, reflect.Uint64:
177+
return map[string]interface{}{
178+
"type": "integer",
179+
"examples": []int{10000},
180+
}
181+
case reflect.Bool:
182+
return map[string]interface{}{
183+
"type": "boolean",
184+
"examples": []bool{true},
185+
}
186+
}
187+
188+
return map[string]interface{}{
189+
"type": "string",
190+
"examples": []string{"0x123456"},
191+
}
192+
}
193+
194+
func extractMethods(obj interface{}, prefix string, paramMapping map[string][]string, docMapping map[string]string) map[string]Method {
195+
t := reflect.TypeOf(obj)
196+
methods := make(map[string]Method)
197+
198+
var recName string
199+
if t.Kind() == reflect.Ptr {
200+
recName = "*" + t.Elem().Name()
201+
} else {
202+
recName = t.Name()
203+
}
204+
205+
for i := 0; i < t.NumMethod(); i++ {
206+
m := t.Method(i)
207+
key := recName + "." + m.Name
208+
mappedNames := paramMapping[key]
209+
210+
props := map[string]interface{}{}
211+
required := []string{}
212+
213+
for j := 1; j < m.Type.NumIn(); j++ {
214+
paramType := m.Type.In(j)
215+
paramName := fmt.Sprintf("param%d", j-1)
216+
if j-1 < len(mappedNames) {
217+
paramName = mappedNames[j-1]
218+
}
219+
props[paramName] = generateTypeRef(paramType)
220+
required = append(required, paramName)
221+
}
222+
223+
var paramsObj map[string]interface{}
224+
if len(props) > 0 {
225+
paramsObj = map[string]interface{}{
226+
"type": "object",
227+
"properties": props,
228+
"required": required,
229+
}
230+
}
231+
232+
var resultType map[string]interface{}
233+
if m.Type.NumOut() > 0 {
234+
out := m.Type.Out(0)
235+
resultType = generateReturnSchema(out)
236+
}
237+
238+
description := fmt.Sprintf("Invokes the %s method on %s", m.Name, recName)
239+
if doc, ok := docMapping[key]; ok {
240+
description = doc
241+
}
242+
243+
method := Method{
244+
Summary: fmt.Sprintf("RPC method %s", m.Name),
245+
Description: description,
246+
Tags: []string{prefix},
247+
Params: paramsObj,
248+
Result: resultType,
249+
}
250+
251+
methods[prefix+m.Name] = method
252+
}
253+
return methods
254+
}
255+
256+
func main() {
257+
paramMapping, docMapping, err := parseParamNamesFromFile("eth/taiko_api_backend.go")
258+
if err != nil {
259+
log.Fatalf("Failed to parse params: %v", err)
260+
}
261+
262+
spec := JRGenSpec{
263+
Schema: "https://rawgit.com/mzernetsch/jrgen/master/jrgen-spec.schema.json",
264+
JRGen: "1.2",
265+
JSONRPC: "2.0",
266+
Info: Info{
267+
Title: "Taiko JSON-RPC API",
268+
Version: "1.0",
269+
Description: []string{"Auto-generated JSON-RPC API for Taiko backend."},
270+
},
271+
Definitions: map[string]interface{}{
272+
"*miner.PreBuiltTxList": map[string]interface{}{
273+
"type": "object",
274+
"properties": map[string]interface{}{
275+
"TxList": map[string]interface{}{"type": "array"},
276+
"EstimatedGasUsed": map[string]interface{}{"type": "integer", "examples": []int{10000}},
277+
"BytesLength": map[string]interface{}{"type": "integer", "examples": []int{10000}},
278+
},
279+
"required": []string{"TxList", "EstimatedGasUsed", "BytesLength"},
280+
},
281+
"*big.Int": map[string]interface{}{
282+
"type": "integer",
283+
"examples": []int{10000},
284+
},
285+
"*rawdb.L1Origin": map[string]interface{}{
286+
"type": "object",
287+
"properties": map[string]interface{}{
288+
"blockID": map[string]interface{}{"$ref": "#/definitions/*big.Int"},
289+
"l2BlockHash": map[string]interface{}{"type": "string", "examples": []string{"0x123456"}},
290+
"l1BlockHeight": map[string]interface{}{"$ref": "#/definitions/*big.Int"},
291+
"l1BlockHash": map[string]interface{}{"type": "string", "examples": []string{"0x123456"}},
292+
},
293+
"required": []string{"blockID", "l2BlockHash", "l1BlockHeight", "l1BlockHash"},
294+
},
295+
"*math.HexOrDecimal256": map[string]interface{}{
296+
"$ref": "#/definitions/*big.Int",
297+
"description": "Hexadecimal or decimal representation of a number.",
298+
},
299+
},
300+
Methods: map[string]Method{},
301+
}
302+
303+
taiko := &eth.TaikoAPIBackend{}
304+
auth := &eth.TaikoAuthAPIBackend{}
305+
306+
for k, v := range extractMethods(taiko, "taiko_", paramMapping, docMapping) {
307+
spec.Methods[k] = v
308+
}
309+
for k, v := range extractMethods(auth, "taikoAuth_", paramMapping, docMapping) {
310+
spec.Methods[k] = v
311+
}
312+
313+
jsonData, err := json.MarshalIndent(spec, "", " ")
314+
if err != nil {
315+
log.Fatalf("Failed to marshal JSON: %v", err)
316+
}
317+
err = os.WriteFile("jrgen.json", jsonData, 0644)
318+
if err != nil {
319+
log.Fatalf("Failed to write file: %v", err)
320+
}
321+
322+
fmt.Println("jrgen.json generated successfully.")
323+
}

0 commit comments

Comments
 (0)