From 2fc319d9e74710faa8631ee6f1310c701f75d882 Mon Sep 17 00:00:00 2001 From: Roger <50648015+RogerLamTd@users.noreply.github.com> Date: Tue, 15 Apr 2025 08:49:07 -0700 Subject: [PATCH] docs(taiko): autogen json-rpc docs (#417) * init script * . * . * . * . * rename * rename * lint * apply changes --- .github/workflows/taiko-geth--pages.yml | 42 +++ README.md | 4 + scripts/taiko_generate_jsonrpc.go | 323 ++++++++++++++++++++++++ 3 files changed, 369 insertions(+) create mode 100644 .github/workflows/taiko-geth--pages.yml create mode 100644 scripts/taiko_generate_jsonrpc.go diff --git a/.github/workflows/taiko-geth--pages.yml b/.github/workflows/taiko-geth--pages.yml new file mode 100644 index 0000000000..8a053e84b8 --- /dev/null +++ b/.github/workflows/taiko-geth--pages.yml @@ -0,0 +1,42 @@ +name: Taiko Geth JSON-RPC Autogen Docs + +on: + push: + branches: [taiko] + +jobs: + generate-and-deploy: + runs-on: [arc-runner-set] + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.23" + + - name: Set up node + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install jrgen + run: | + npm install -g jrgen + + - name: Run script + run: | + go run ./scripts/generate-jsonrpc.go + jrgen docs-html jrgen.json + mv taiko-json-rpc-api-reference.html index.html + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./ # Set this to where your `index.html` is located + publish_branch: gh-pages + destination_dir: taiko-geth-json-rpc \ No newline at end of file diff --git a/README.md b/README.md index 3aaf603603..a9fddf21c0 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,10 @@ The codebase is based on [go-ethereum v1.15.5](https://github.com/ethereum/go-et 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`). +## JSON-RPC API + +JSON-RPC documentation for taiko-specific JSON-RPC calls can be found [here](https://taikoxyz.github.io/taiko-geth/taiko-geth-json-rpc/). + ## Go Ethereum Golang execution layer implementation of the Ethereum protocol. diff --git a/scripts/taiko_generate_jsonrpc.go b/scripts/taiko_generate_jsonrpc.go new file mode 100644 index 0000000000..d82959e093 --- /dev/null +++ b/scripts/taiko_generate_jsonrpc.go @@ -0,0 +1,323 @@ +package main + +import ( + "encoding/json" + "fmt" + "go/ast" + "go/parser" + "go/token" + "log" + "os" + "reflect" + "strings" + + "github.com/ethereum/go-ethereum/eth" +) + +type JRGenSpec struct { + Schema string `json:"$schema"` + JRGen string `json:"jrgen"` + JSONRPC string `json:"jsonrpc"` + Info Info `json:"info"` + Definitions map[string]interface{} `json:"definitions"` + Methods map[string]Method `json:"methods"` +} + +type Info struct { + Title string `json:"title"` + Description []string `json:"description"` + Version string `json:"version"` + Servers []Server `json:"servers,omitempty"` +} + +type Server struct { + URL string `json:"url"` + Description string `json:"description"` +} + +type Method struct { + Summary string `json:"summary"` + Description string `json:"description"` + Tags []string `json:"tags,omitempty"` + Params map[string]interface{} `json:"params,omitempty"` + Result map[string]interface{} `json:"result,omitempty"` +} + +func parseParamNamesFromFile(filename string) (map[string][]string, map[string]string, error) { + fset := token.NewFileSet() + fileAST, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) + if err != nil { + return nil, nil, err + } + + methodParams := make(map[string][]string) + methodDocs := make(map[string]string) + + for _, decl := range fileAST.Decls { + if funcDecl, ok := decl.(*ast.FuncDecl); ok && funcDecl.Recv != nil { + recvField := funcDecl.Recv.List[0] + var recvTypeStr string + + switch t := recvField.Type.(type) { + case *ast.StarExpr: + if ident, ok := t.X.(*ast.Ident); ok { + recvTypeStr = "*" + ident.Name + } + case *ast.Ident: + recvTypeStr = t.Name + } + + if recvTypeStr == "*TaikoAPIBackend" || recvTypeStr == "*TaikoAuthAPIBackend" { + var params []string + if funcDecl.Type.Params != nil { + for _, field := range funcDecl.Type.Params.List { + if len(field.Names) > 0 { + for _, name := range field.Names { + params = append(params, name.Name) + } + } else { + params = append(params, exprToString(field.Type)) + } + } + } + key := recvTypeStr + "." + funcDecl.Name.Name + methodParams[key] = params + + if funcDecl.Doc != nil { + methodDocs[key] = strings.TrimSpace(funcDecl.Doc.Text()) + } + } + } + } + + return methodParams, methodDocs, nil +} + +func exprToString(expr ast.Expr) string { + switch t := expr.(type) { + case *ast.Ident: + return t.Name + case *ast.StarExpr: + return "*" + exprToString(t.X) + case *ast.ArrayType: + return "[]" + exprToString(t.Elt) + case *ast.SelectorExpr: + return exprToString(t.X) + "." + t.Sel.Name + default: + return fmt.Sprintf("%T", expr) + } +} + +func generateReturnSchema(goType reflect.Type) map[string]interface{} { + switch goType.Kind() { + case reflect.Ptr: + return map[string]interface{}{ + "$ref": "#/definitions/" + goType.String(), + } + case reflect.Slice: + elem := goType.Elem() + if elem.Kind() == reflect.Ptr { + return map[string]interface{}{ + "type": "array", + "items": map[string]interface{}{ + "$ref": "#/definitions/" + elem.String(), + }, + } + } + return map[string]interface{}{ + "type": "array", + "items": generateTypeRef(elem), + } + default: + return generateTypeRef(goType) + } +} + +func generateTypeRef(goType reflect.Type) map[string]interface{} { + switch goType.Kind() { + case reflect.Ptr: + switch goType.String() { + case "*miner.PreBuiltTxList": + return map[string]interface{}{ + "$ref": "#/definitions/*miner.PreBuiltTxList", + } + case "*big.Int": + return map[string]interface{}{ + "$ref": "#/definitions/*big.Int", + } + case "*rawdb.L1Origin": + return map[string]interface{}{ + "$ref": "#/definitions/*rawdb.L1Origin", + } + case "*math.HexOrDecimal256": + return map[string]interface{}{ + "$ref": "#/definitions/*math.HexOrDecimal256", + } + } + case reflect.Slice: + elem := goType.Elem() + if elem.Kind() == reflect.Ptr { + return map[string]interface{}{ + "type": "array", + "items": map[string]interface{}{ + "$ref": "#/definitions/" + elem.String(), + }, + } + } + return map[string]interface{}{ + "type": "array", + "items": generateTypeRef(elem), + } + case reflect.String: + return map[string]interface{}{ + "type": "string", + "examples": []string{"0x123456"}, + } + case reflect.Int, reflect.Int64, reflect.Uint, reflect.Uint64: + return map[string]interface{}{ + "type": "integer", + "examples": []int{10000}, + } + case reflect.Bool: + return map[string]interface{}{ + "type": "boolean", + "examples": []bool{true}, + } + } + + return map[string]interface{}{ + "type": "string", + "examples": []string{"0x123456"}, + } +} + +func extractMethods(obj interface{}, prefix string, paramMapping map[string][]string, docMapping map[string]string) map[string]Method { + t := reflect.TypeOf(obj) + methods := make(map[string]Method) + + var recName string + if t.Kind() == reflect.Ptr { + recName = "*" + t.Elem().Name() + } else { + recName = t.Name() + } + + for i := 0; i < t.NumMethod(); i++ { + m := t.Method(i) + key := recName + "." + m.Name + mappedNames := paramMapping[key] + + props := map[string]interface{}{} + required := []string{} + + for j := 1; j < m.Type.NumIn(); j++ { + paramType := m.Type.In(j) + paramName := fmt.Sprintf("param%d", j-1) + if j-1 < len(mappedNames) { + paramName = mappedNames[j-1] + } + props[paramName] = generateTypeRef(paramType) + required = append(required, paramName) + } + + var paramsObj map[string]interface{} + if len(props) > 0 { + paramsObj = map[string]interface{}{ + "type": "object", + "properties": props, + "required": required, + } + } + + var resultType map[string]interface{} + if m.Type.NumOut() > 0 { + out := m.Type.Out(0) + resultType = generateReturnSchema(out) + } + + description := fmt.Sprintf("Invokes the %s method on %s", m.Name, recName) + if doc, ok := docMapping[key]; ok { + description = doc + } + + method := Method{ + Summary: fmt.Sprintf("RPC method %s", m.Name), + Description: description, + Tags: []string{prefix}, + Params: paramsObj, + Result: resultType, + } + + methods[prefix+m.Name] = method + } + return methods +} + +func main() { + paramMapping, docMapping, err := parseParamNamesFromFile("eth/taiko_api_backend.go") + if err != nil { + log.Fatalf("Failed to parse params: %v", err) + } + + spec := JRGenSpec{ + Schema: "https://rawgit.com/mzernetsch/jrgen/master/jrgen-spec.schema.json", + JRGen: "1.2", + JSONRPC: "2.0", + Info: Info{ + Title: "Taiko JSON-RPC API", + Version: "1.0", + Description: []string{"Auto-generated JSON-RPC API for Taiko backend."}, + }, + Definitions: map[string]interface{}{ + "*miner.PreBuiltTxList": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "TxList": map[string]interface{}{"type": "array"}, + "EstimatedGasUsed": map[string]interface{}{"type": "integer", "examples": []int{10000}}, + "BytesLength": map[string]interface{}{"type": "integer", "examples": []int{10000}}, + }, + "required": []string{"TxList", "EstimatedGasUsed", "BytesLength"}, + }, + "*big.Int": map[string]interface{}{ + "type": "integer", + "examples": []int{10000}, + }, + "*rawdb.L1Origin": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "blockID": map[string]interface{}{"$ref": "#/definitions/*big.Int"}, + "l2BlockHash": map[string]interface{}{"type": "string", "examples": []string{"0x123456"}}, + "l1BlockHeight": map[string]interface{}{"$ref": "#/definitions/*big.Int"}, + "l1BlockHash": map[string]interface{}{"type": "string", "examples": []string{"0x123456"}}, + }, + "required": []string{"blockID", "l2BlockHash", "l1BlockHeight", "l1BlockHash"}, + }, + "*math.HexOrDecimal256": map[string]interface{}{ + "$ref": "#/definitions/*big.Int", + "description": "Hexadecimal or decimal representation of a number.", + }, + }, + Methods: map[string]Method{}, + } + + taiko := ð.TaikoAPIBackend{} + auth := ð.TaikoAuthAPIBackend{} + + for k, v := range extractMethods(taiko, "taiko_", paramMapping, docMapping) { + spec.Methods[k] = v + } + for k, v := range extractMethods(auth, "taikoAuth_", paramMapping, docMapping) { + spec.Methods[k] = v + } + + jsonData, err := json.MarshalIndent(spec, "", " ") + if err != nil { + log.Fatalf("Failed to marshal JSON: %v", err) + } + err = os.WriteFile("jrgen.json", jsonData, 0644) + if err != nil { + log.Fatalf("Failed to write file: %v", err) + } + + fmt.Println("jrgen.json generated successfully.") +}