From 30761c7b8c04e7e68af562da1031e026097a7159 Mon Sep 17 00:00:00 2001 From: Renato Maia <1887792+renatomaia@users.noreply.github.com> Date: Mon, 27 Jul 2026 09:27:15 -0300 Subject: [PATCH 01/13] feat(jsonrpc): add support for JSON-RPC 2.0 batch requests --- internal/jsonrpc/batchcalls_test.go | 352 ++++++++++++ internal/jsonrpc/jsonrpc-discover.json | 2 +- internal/jsonrpc/jsonrpc.go | 733 +++++++++++++------------ internal/jsonrpc/limitedwriter.go | 58 ++ internal/jsonrpc/types.go | 20 +- 5 files changed, 801 insertions(+), 364 deletions(-) create mode 100644 internal/jsonrpc/batchcalls_test.go create mode 100644 internal/jsonrpc/limitedwriter.go diff --git a/internal/jsonrpc/batchcalls_test.go b/internal/jsonrpc/batchcalls_test.go new file mode 100644 index 000000000..d937c9581 --- /dev/null +++ b/internal/jsonrpc/batchcalls_test.go @@ -0,0 +1,352 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package jsonrpc + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "net/http/httptest" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/cartesi/rollups-node/pkg/service" + "github.com/stretchr/testify/require" +) + +const ( + testBatchSize = 100 + testBatchSuccessCount = 10 + testLargeResultSize = 1<<20 - 38 // 1 MB - `,{"jsonrpc":"2.0","result":"...","id":??}` + testResponseBudgetSlack = 1 << 20 +) + +func serveRPC(t *testing.T, s *Service, body []byte) *httptest.ResponseRecorder { + t.Helper() + req := httptest.NewRequest(http.MethodPost, "/rpc", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + rr := httptest.NewRecorder() + s.handleRPC(rr, req) + return rr +} + +func newBatchTestService() *Service { + return &Service{ + Service: service.Service{ + Logger: slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil)), + }, + } +} + +func decodeRPCResponse(t *testing.T, body []byte) RPCResponse { + t.Helper() + var response RPCResponse + require.NoError(t, json.Unmarshal(body, &response)) + return response +} + +func decodeRPCBatch(t *testing.T, body []byte) []RPCResponse { + t.Helper() + var responses []RPCResponse + require.NoError(t, json.Unmarshal(body, &responses)) + return responses +} + +func requireRPCError(t *testing.T, response RPCResponse, id any, code int) { + t.Helper() + require.Equal(t, "2.0", response.JSONRPC) + require.Equal(t, id, response.ID) + require.NotNil(t, response.Error) + require.Equal(t, code, response.Error.Code) +} + +func TestJSONRPCBatchRejectsEmptyBatchWithSingleObject(t *testing.T) { + s := newBatchTestService() + rr := serveRPC(t, s, []byte(`[]`)) + + require.Equal(t, http.StatusOK, rr.Code) + require.Equal(t, "application/json", rr.Header().Get("Content-Type")) + requireRPCError(t, decodeRPCResponse(t, rr.Body.Bytes()), nil, JSONRPC_INVALID_REQUEST) + + var array []RPCResponse + require.Error(t, json.Unmarshal(rr.Body.Bytes(), &array), + "an empty batch error must be one JSON-RPC object, not an array") +} + +func TestJSONRPCBatchRejectsMoreThanMaximumBeforeDispatch(t *testing.T) { + s := newBatchTestService() + var calls atomic.Int32 + const method = "test_batch_cap" + withTestRPCHandler(t, method, func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) { + calls.Add(1) + return true, nil + }) + + requests := make([]json.RawMessage, testBatchSize+1) + for i := range requests { + requests[i] = json.RawMessage(fmt.Sprintf( + `{"jsonrpc":"2.0","method":%q,"id":%d}`, method, i)) + } + body, err := json.Marshal(requests) + require.NoError(t, err) + rr := serveRPC(t, s, body) + + require.Equal(t, http.StatusOK, rr.Code) + requireRPCError(t, decodeRPCResponse(t, rr.Body.Bytes()), nil, JSONRPC_INVALID_REQUEST) + require.Zero(t, calls.Load(), "an oversized batch must be rejected before dispatch") +} + +func TestJSONRPCMalformedBatchReturnsParseErrorObject(t *testing.T) { + s := newBatchTestService() + rr := serveRPC(t, s, []byte(`[{"jsonrpc":"2.0","method":"rpc.discover","id":1},`)) + + require.Equal(t, http.StatusOK, rr.Code) + require.Equal(t, "application/json", rr.Header().Get("Content-Type")) + requireRPCError(t, decodeRPCResponse(t, rr.Body.Bytes()), nil, JSONRPC_PARSE_ERROR) +} + +func TestJSONRPCBatchMalformedElementDoesNotPoisonValidSiblings(t *testing.T) { + s := newBatchTestService() + body := []byte(`[ + {"jsonrpc":"2.0","method":"cartesi_getNodeVersion","id":1}, + 17, + {"jsonrpc":"2.0","method":"cartesi_getNodeVersion","id":3} + ]`) + rr := serveRPC(t, s, body) + + require.Equal(t, http.StatusOK, rr.Code) + responses := decodeRPCBatch(t, rr.Body.Bytes()) + require.Len(t, responses, 3) + require.Nil(t, responses[0].Error) + require.EqualValues(t, 1, responses[0].ID) + requireRPCError(t, responses[1], nil, JSONRPC_INVALID_REQUEST) + require.Nil(t, responses[2].Error) + require.EqualValues(t, 3, responses[2].ID) +} + +func TestJSONRPCBatchStructurallyInvalidElementsDoNotPoisonValidSiblings(t *testing.T) { + tests := map[string]string{ + "null": `null`, + "empty object": `{}`, + "missing method": `{"jsonrpc":"2.0","id":2}`, + "invalid version": `{"jsonrpc":"1.0","method":"cartesi_getNodeVersion","id":2}`, + } + + for name, invalidElement := range tests { + t.Run(name, func(t *testing.T) { + s := newBatchTestService() + body := []byte(fmt.Sprintf(`[ + {"jsonrpc":"2.0","method":"cartesi_getNodeVersion","id":1}, + %s, + {"jsonrpc":"2.0","method":"cartesi_getNodeVersion","id":3} + ]`, invalidElement)) + rr := serveRPC(t, s, body) + + require.Equal(t, http.StatusOK, rr.Code) + responses := decodeRPCBatch(t, rr.Body.Bytes()) + require.Len(t, responses, 3) + + require.Nil(t, responses[0].Error) + require.EqualValues(t, 1, responses[0].ID) + + requireRPCError(t, responses[1], nil, JSONRPC_INVALID_REQUEST) + + require.Nil(t, responses[2].Error) + require.EqualValues(t, 3, responses[2].ID) + }) + } +} + +func TestJSONRPCBatchNotificationsReceiveNullIDResponses(t *testing.T) { + s := newBatchTestService() + rr := serveRPC(t, s, []byte(`[ + {"jsonrpc":"2.0","method":"cartesi_getNodeVersion"}, + {"jsonrpc":"2.0","method":"does_not_exist"} + ]`)) + + require.Equal(t, http.StatusOK, rr.Code) + responses := decodeRPCBatch(t, rr.Body.Bytes()) + require.Len(t, responses, 2, "notifications are deliberately answered by this server") + require.Nil(t, responses[0].ID) + require.Nil(t, responses[0].Error) + requireRPCError(t, responses[1], nil, JSONRPC_METHOD_NOT_FOUND) +} + +func TestJSONRPCBatchAlwaysReturnsHTTP200ForJSONErrors(t *testing.T) { + s := newBatchTestService() + tests := map[string][]byte{ + "parse error": []byte(`[nope`), + "invalid request": []byte(`[]`), + "error entries": []byte(`[false,{"jsonrpc":"2.0","method":"does_not_exist","id":2}]`), + } + for name, body := range tests { + t.Run(name, func(t *testing.T) { + rr := serveRPC(t, s, body) + require.Equal(t, http.StatusOK, rr.Code) + require.Equal(t, "application/json", rr.Header().Get("Content-Type")) + require.True(t, json.Valid(rr.Body.Bytes())) + }) + } +} + +func TestJSONRPCBatchReplacesResponsesAtCumulativeResponseBudget(t *testing.T) { + s := newBatchTestService() + var calls atomic.Int32 + const method = "test_large_batch_result" + largeResult := strings.Repeat("x", testLargeResultSize) + withTestRPCHandler(t, method, func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) { + calls.Add(1) + return largeResult, nil + }) + + requests := make([]json.RawMessage, testBatchSize) + for i := range requests { + requests[i] = json.RawMessage(fmt.Sprintf( + `{"jsonrpc":"2.0","method":%q,"params":{"limit":10000},"id":%d}`, method, i)) + } + body, err := json.Marshal(requests) + require.NoError(t, err) + require.Less(t, len(body), 10<<10, "the request cap must not be mistaken for a response cap") + rr := serveRPC(t, s, body) + + require.Equal(t, http.StatusOK, rr.Code) + responses := decodeRPCBatch(t, rr.Body.Bytes()) + require.Len(t, responses, testBatchSize) + require.Equal(t, int32(testBatchSuccessCount+1), calls.Load()) + require.LessOrEqual(t, rr.Body.Len(), (10<<20)+testResponseBudgetSlack) + for i := range responses[:testBatchSuccessCount] { + require.Equal(t, "2.0", responses[i].JSONRPC) + require.Equal(t, float64(i), responses[i].ID) + require.Nil(t, responses[i].Error) + require.Equal(t, responses[i].Result, largeResult) + } + for i := testBatchSuccessCount; i < len(responses); i++ { + requireRPCError(t, responses[i], float64(i), JSONRPC_INVALID_REQUEST) + require.Equal(t, "response size budget exceeded", responses[i].Error.Message) + } +} + +func TestJSONRPCBatchStopsBetweenEntriesWhenContextIsCanceled(t *testing.T) { + s := newBatchTestService() + var calls atomic.Int32 + var logs bytes.Buffer + s.Logger = slog.New(slog.NewJSONHandler(&logs, &slog.HandlerOptions{Level: slog.LevelDebug})) + ctx, cancel := context.WithCancel(context.Background()) + const method = "test_cancel_batch" + withTestRPCHandler(t, method, func(_ *Service, _ *http.Request, _ RPCRequest) (any, error) { + calls.Add(1) + cancel() + return true, nil + }) + + body := []byte(fmt.Sprintf(`[ + {"jsonrpc":"2.0","method":%q,"id":1}, + {"jsonrpc":"2.0","method":%q,"id":2}, + {"jsonrpc":"2.0","method":%q,"id":3} + ]`, method, method, method)) + req := httptest.NewRequest(http.MethodPost, "/rpc", bytes.NewReader(body)).WithContext(ctx) + rr := httptest.NewRecorder() + s.handleRPC(rr, req) + + require.Equal(t, int32(1), calls.Load(), + "a canceled request must not run the remaining batch handlers") + for _, line := range strings.Split(strings.TrimSpace(logs.String()), "\n") { + if line == "" { + continue + } + var record map[string]any + require.NoError(t, json.Unmarshal([]byte(line), &record)) + require.False(t, + record["level"] == "ERROR" && strings.Contains(strings.ToLower(line), "context canceled"), + "context.Canceled is a graceful stop and must not be ERROR logged") + } +} + +func TestJSONRPCBatchUsesOneAdmissionPermit(t *testing.T) { + s := newBatchTestService() + s.admission = service.NewSemaphoreAdmission(1) + s.server = &http.Server{ + Handler: rebuildHandlerWithAdmission(s), + ReadHeaderTimeout: 2 * time.Second, + } + var nestedAcquisitions atomic.Int32 + const method = "test_batch_admission" + withTestRPCHandler(t, method, func(s *Service, _ *http.Request, _ RPCRequest) (any, error) { + if s.admission.TryAcquire() { + nestedAcquisitions.Add(1) + s.admission.Release() + } + return true, nil + }) + + body := []byte(fmt.Sprintf(`[ + {"jsonrpc":"2.0","method":%q,"id":1}, + {"jsonrpc":"2.0","method":%q,"id":2} + ]`, method, method)) + req := httptest.NewRequest(http.MethodPost, "/rpc", bytes.NewReader(body)) + rr := httptest.NewRecorder() + s.server.Handler.ServeHTTP(rr, req) + + require.Equal(t, http.StatusOK, rr.Code) + require.Zero(t, nestedAcquisitions.Load(), + "the HTTP request's one permit must remain held for the whole batch") + require.Len(t, decodeRPCBatch(t, rr.Body.Bytes()), 2) +} + +func TestJSONRPCBatchLoggingHasOneInfoAndDebugMethods(t *testing.T) { + s := newBatchTestService() + var logs bytes.Buffer + s.Logger = slog.New(slog.NewJSONHandler(&logs, &slog.HandlerOptions{Level: slog.LevelDebug})) + + const entries = 3 + body := []byte(`[ + {"jsonrpc":"2.0","method":"attacker_method_0","id":0}, + {"jsonrpc":"2.0","method":"attacker_method_1","id":1}, + {"jsonrpc":"2.0","method":"attacker_method_2","id":2} + ]`) + serveRPC(t, s, body) + + var batchInfo int + debugMethods := map[string]bool{} + for _, line := range strings.Split(strings.TrimSpace(logs.String()), "\n") { + if line == "" { + continue + } + var record map[string]any + require.NoError(t, json.Unmarshal([]byte(line), &record)) + level, _ := record["level"].(string) + encoded := string(line) + if level == "INFO" && strings.Contains(strings.ToLower(encoded), "batch") { + batchInfo++ + require.Contains(t, encoded, fmt.Sprint(entries)) + } + for i := range entries { + method := fmt.Sprintf("attacker_method_%d", i) + if strings.Contains(encoded, method) { + require.Equal(t, "DEBUG", level, "per-entry method names must never be Info logged") + debugMethods[method] = true + } + } + } + require.Equal(t, 1, batchInfo) + require.Len(t, debugMethods, entries) +} + +func withTestRPCHandler(t *testing.T, method string, handler rpcHandler) { + t.Helper() + previous, existed := jsonrpcHandlers[method] + jsonrpcHandlers[method] = handler + t.Cleanup(func() { + if existed { + jsonrpcHandlers[method] = previous + } else { + delete(jsonrpcHandlers, method) + } + }) +} diff --git a/internal/jsonrpc/jsonrpc-discover.json b/internal/jsonrpc/jsonrpc-discover.json index ea21d239e..63b7ebe4c 100644 --- a/internal/jsonrpc/jsonrpc-discover.json +++ b/internal/jsonrpc/jsonrpc-discover.json @@ -3,7 +3,7 @@ "info": { "title": "Cartesi Rollups Node API", "version": "2.0.0", - "description": "A JSON-RPC API for reading rollups data. It provides information about applications, epochs, inputs, outputs, and reports in a read-only fashion.\n\nError handling: every method documents its possible errors under `errors`, and clients can dispatch on the error code. `-32002` (application not found) means the application identifier itself is unknown to this node; for application-scoped methods, this is a configuration error that will not resolve by retrying. `-32001` (resource not found) means the requested resource does not exist in the method's scope. For application-scoped methods, `-32001` means the application is known but the nested entity is missing; for node-scoped methods, it can also report missing node resources such as EVM reader configuration. For forward-looking application resources (e.g. the next epoch, input, or output index), `-32001` is the documented \"not created yet\" signal and is safe to poll. The error message names the missing resource. `-32603` (internal error) is never used for missing resources - clients should treat it as a node-side failure and alarm or back off, not poll. The transport-level codes `-32700` (parse error), `-32600` (invalid request), and `-32601` (method not found) follow the JSON-RPC 2.0 specification." + "description": "A JSON-RPC API for reading rollups data. It provides information about applications, epochs, inputs, outputs, and reports in a read-only fashion.\n\nBatch requests: JSON-RPC batch arrays are supported with a maximum of 100 entries per batch. Entries execute sequentially and responses are returned in the same order as their requests. The 1 MB request-body limit applies to the whole batch array. A cumulative 10 MB response-size budget also applies; once the budget is exceeded, the remaining requests receive `-32600` error entries. Every batch entry receives a response. Notification suppression is not supported: entries without an ID are answered with `id: null`. This is a documented deviation from JSON-RPC 2.0, under which notifications normally produce no response. A batch response uses HTTP status 200 even when some or all of its entries are errors. Because execution is sequential and the server time limit, heavy list calls should be kept outside large batches.\n\nError handling: every method documents its possible errors under `errors`, and clients can dispatch on the error code. `-32002` (application not found) means the application identifier itself is unknown to this node; for application-scoped methods, this is a configuration error that will not resolve by retrying. `-32001` (resource not found) means the requested resource does not exist in the method's scope. For application-scoped methods, `-32001` means the application is known but the nested entity is missing; for node-scoped methods, it can also report missing node resources such as EVM reader configuration. For forward-looking application resources (e.g. the next epoch, input, or output index), `-32001` is the documented \"not created yet\" signal and is safe to poll. The error message names the missing resource. `-32603` (internal error) is never used for missing resources - clients should treat it as a node-side failure and alarm or back off, not poll. The transport-level codes `-32700` (parse error), `-32600` (invalid request), and `-32601` (method not found) follow the JSON-RPC 2.0 specification." }, "methods": [ { diff --git a/internal/jsonrpc/jsonrpc.go b/internal/jsonrpc/jsonrpc.go index 71ffc07d9..a96cde1e0 100644 --- a/internal/jsonrpc/jsonrpc.go +++ b/internal/jsonrpc/jsonrpc.go @@ -4,6 +4,8 @@ package jsonrpc import ( + "bytes" + "context" "embed" "encoding/json" "errors" @@ -25,6 +27,8 @@ var discoverSpec embed.FS const ( // Maximum allowed body size (1 MB). MAX_BODY_SIZE = 1 << 20 //nolint: revive + // Maximum cumulative response size (10 MB). + MAX_RESPONSE_SIZE = 10 << 20 //nolint: revive // Maximum amount of items to list (10,000). LIST_ITEM_LIMIT = 10000 //nolint: revive // Default amount of item on a list (50) @@ -48,7 +52,7 @@ const ( JSONRPC_INTERNAL_ERROR int = -32603 //nolint: revive ) -type rpcHandler = func(*Service, http.ResponseWriter, *http.Request, RPCRequest) +type rpcHandler = func(*Service, *http.Request, RPCRequest) (any, error) type dispatchTable = map[string]rpcHandler var jsonrpcHandlers = dispatchTable{ @@ -83,6 +87,56 @@ var jsonrpcHandlers = dispatchTable{ // Dispatching JSON‑RPC methods // ----------------------------------------------------------------------------- +func (s *Service) handleResponseResult(w http.ResponseWriter, err error) bool { + switch err { + case nil: + return true + case io.ErrShortBuffer: + return false + } + s.Logger.Error("failed encoding JSON response body", "error", err) + http.Error(w, "expected JSON object or array", http.StatusInternalServerError) + return false +} + +func (s *Service) writeByte(w http.ResponseWriter, c byte) bool { + _, err := w.Write([]byte{c}) + return s.handleResponseResult(w, err) +} + +// writeRPCError sends a generic error response for internal errors. +func (s *Service) writeRPCError(w http.ResponseWriter, id any, code int, message string) bool { + err := writeRPCError(w, id, code, message, nil) + return s.handleResponseResult(w, err) +} + +func (s *Service) dispatchOneRequest(w io.Writer, r *http.Request, req RPCRequest) error { + switch { + case req.JSONRPC != "2.0": + fallthrough + case req.Method == "": + return writeRPCError(w, nil, JSONRPC_INVALID_REQUEST, "invalid request", nil) + } + fn, ok := jsonrpcHandlers[req.Method] + if !ok { + s.Logger.Debug(fmt.Sprintf("RPC method not found: %s", req.Method)) + return writeRPCError(w, req.ID, JSONRPC_METHOD_NOT_FOUND, "Method not found", nil) + } + + result, err := fn(s, r, req) + if err == nil { + return writeRPCResult(w, req.ID, result) + } + + var rpcErr *RPCError + if errors.As(err, &rpcErr) { + return writeRPCError(w, req.ID, rpcErr.Code, rpcErr.Message, rpcErr.Data) + } + + s.Logger.Error("RPC method failed", "method", req.Method, "error", err) + return writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) +} + func (s *Service) handleRPC(w http.ResponseWriter, r *http.Request) { // Limit request body size and ensure it is closed. r.Body = http.MaxBytesReader(w, r.Body, MAX_BODY_SIZE) @@ -97,17 +151,90 @@ func (s *Service) handleRPC(w http.ResponseWriter, r *http.Request) { http.Error(w, "Failed to read request body", http.StatusBadRequest) return } - var req RPCRequest - if err := json.Unmarshal(body, &req); err != nil { - http.Error(w, "Invalid JSON", http.StatusBadRequest) + + body = bytes.TrimSpace(body) + if len(body) == 0 { + http.Error(w, "Empty request body", http.StatusBadRequest) return } - s.Logger.Info(fmt.Sprintf("Received RPC request: %s", req.Method)) - if fn, ok := jsonrpcHandlers[req.Method]; ok { - fn(s, w, r, req) - } else { - s.Logger.Info(fmt.Sprintf("RPC method not found: %s", req.Method)) - writeRPCError(w, req.ID, JSONRPC_METHOD_NOT_FOUND, "Method not found", nil) + + switch body[0] { + case '{': + var req RPCRequest + if err := json.Unmarshal(body, &req); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + s.Logger.Info(fmt.Sprintf("Received RPC request: %s", req.Method)) + err := s.dispatchOneRequest(w, r, req) + s.handleResponseResult(w, err) + + case '[': + w.Header().Set("Content-Type", "application/json") + var reqSeq []json.RawMessage + if err := json.Unmarshal(body, &reqSeq); err != nil { + s.writeRPCError(w, nil, JSONRPC_PARSE_ERROR, "invalid request batch") + return + } + if len(reqSeq) == 0 || len(reqSeq) > 100 { + s.writeRPCError(w, nil, JSONRPC_INVALID_REQUEST, "invalid request batch size (expected [1..100])") + return + } + + s.Logger.Info(fmt.Sprintf("Received RPC request batch with %d items", len(reqSeq))) + if !s.writeByte(w, '[') { + return + } + + budgetResp := newBudgetWriter(w, MAX_RESPONSE_SIZE) + reqLoop: + for i, rawReq := range reqSeq { + + switch r.Context().Err() { + case context.DeadlineExceeded: + s.Logger.Warn("RPC method dispatch timeout") + fallthrough + case context.Canceled: + break reqLoop + } + + if i > 0 && !s.writeByte(w, ',') { + return + } + + var responded bool + var req RPCRequest + if err := json.Unmarshal(rawReq, &req); err != nil { + responded = s.writeRPCError(w, nil, JSONRPC_INVALID_REQUEST, "invalid request") + } else { + s.Logger.Debug(fmt.Sprintf("Dispatching RPC request: %s", req.Method)) + buffer := budgetResp.NewLimitedWriter() + if buffer == nil { + responded = s.writeRPCError(w, req.ID, JSONRPC_INVALID_REQUEST, "response size budget exceeded") + } else { + err := s.dispatchOneRequest(buffer, r, req) + switch { + case err == nil: + responded = s.handleResponseResult(w, buffer.Flush()) + case errors.Is(err, io.ErrShortBuffer): + responded = s.writeRPCError(w, req.ID, JSONRPC_INVALID_REQUEST, "response size budget exceeded") + default: + s.Logger.Error("RPC method response encode falied", "method", req.Method, "error", err) + responded = s.writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error") + } + } + } + + if !responded { + return + } + } + s.writeByte(w, ']') + + default: + http.Error(w, "expected JSON object or array", http.StatusBadRequest) + } } @@ -116,28 +243,25 @@ func (s *Service) handleRPC(w http.ResponseWriter, r *http.Request) { // ----------------------------------------------------------------------------- // Discovery: return the embedded specification. -func handleDiscover(s *Service, w http.ResponseWriter, _ *http.Request, req RPCRequest) { +func handleDiscover(s *Service, _ *http.Request, _ RPCRequest) (any, error) { data, err := discoverSpec.ReadFile("jsonrpc-discover.json") if err != nil { s.Logger.Error("Unable to read jsonrpc-discover content", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } var spec any if err := json.Unmarshal(data, &spec); err != nil { s.Logger.Error("Unable to unmarshal discovery spec JSON", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } - writeRPCResult(w, req.ID, spec) + return spec, nil } -func handleListApplications(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleListApplications(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListApplicationsParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Use default values if not provided if params.Limit <= 0 { @@ -154,57 +278,51 @@ func handleListApplications(s *Service, w http.ResponseWriter, r *http.Request, }, params.Descending) if err != nil { s.Logger.Error("Unable to retrieve applications from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } if apps == nil { apps = []*model.Application{} } - writeRPCResult(w, req.ID, api.ListResponse[*model.Application]{ + return api.ListResponse[*model.Application]{ Data: apps, Pagination: api.Pagination{ TotalCount: total, Limit: params.Limit, Offset: params.Offset, }, - }) + }, nil } -func handleGetApplication(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleGetApplication(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetApplicationParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } app, err := s.repository.GetApplication(r.Context(), params.Application) if err != nil { s.Logger.Error("Unable to retrieve application from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } if app == nil { - writeRPCError(w, req.ID, JSONRPC_APPLICATION_NOT_FOUND, "Application not found", nil) - return + return nil, newRPCError(JSONRPC_APPLICATION_NOT_FOUND, "Application not found", nil) } - writeRPCResult(w, req.ID, api.SingleResponse[*model.Application]{Data: app}) + return api.SingleResponse[*model.Application]{Data: app}, nil } -func handleListEpochs(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleListEpochs(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListEpochsParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Use default values if not provided @@ -218,16 +336,14 @@ func handleListEpochs(s *Service, w http.ResponseWriter, r *http.Request, req RP // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } var epochFilter repository.EpochFilter if params.Status != nil { var status model.EpochStatus if err := status.Scan(*params.Status); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch status: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch status: %v", err), nil) } epochFilter.Status = []model.EpochStatus{status} } @@ -238,101 +354,92 @@ func handleListEpochs(s *Service, w http.ResponseWriter, r *http.Request, req RP }, params.Descending) if err != nil { s.Logger.Error("Unable to retrieve epochs from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } - if len(epochs) == 0 && s.applicationAbsentOrError(w, r, req, params.Application) { - return + if len(epochs) == 0 { + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err + } } if epochs == nil { epochs = []*model.Epoch{} } - writeRPCResult(w, req.ID, api.ListResponse[*model.Epoch]{ + return api.ListResponse[*model.Epoch]{ Data: epochs, Pagination: api.Pagination{ TotalCount: total, Limit: params.Limit, Offset: params.Offset, }, - }) + }, nil } -func handleGetEpoch(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleGetEpoch(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetEpochParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } index, err := config.ToIndexFromString(params.EpochIndex) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) } epoch, err := s.repository.GetEpoch(r.Context(), params.Application, index) if err != nil { s.Logger.Error("Unable to retrieve epoch from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } if epoch == nil { - if s.applicationAbsentOrError(w, r, req, params.Application) { - return + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err } - writeRPCError(w, req.ID, JSONRPC_RESOURCE_NOT_FOUND, "Epoch not found", nil) - return + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Epoch not found", nil) } - writeRPCResult(w, req.ID, api.SingleResponse[*model.Epoch]{Data: epoch}) + return api.SingleResponse[*model.Epoch]{Data: epoch}, nil } -func handleGetLastAcceptedEpochIndex(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleGetLastAcceptedEpochIndex(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetLastAcceptedEpochIndexParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } index, err := s.repository.GetLastAcceptedEpochIndex(r.Context(), params.Application) if errors.Is(err, repository.ErrNotFound) { - if s.applicationAbsentOrError(w, r, req, params.Application) { - return + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err } - writeRPCError(w, req.ID, JSONRPC_RESOURCE_NOT_FOUND, "Epoch not found", nil) - return + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Epoch not found", nil) } if err != nil { s.Logger.Error("Unable to retrieve epoch from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } - writeRPCResult(w, req.ID, api.SingleResponse[string]{Data: fmt.Sprintf("0x%x", index)}) + return api.SingleResponse[string]{Data: fmt.Sprintf("0x%x", index)}, nil } -func handleListInputs(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleListInputs(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListInputsParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Use default values if not provided @@ -346,8 +453,7 @@ func handleListInputs(s *Service, w http.ResponseWriter, r *http.Request, req RP // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } // Create input filter based on params @@ -355,8 +461,7 @@ func handleListInputs(s *Service, w http.ResponseWriter, r *http.Request, req RP if params.EpochIndex != nil { epochIndex, err := config.ToIndexFromString(*params.EpochIndex) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) } inputFilter.EpochIndex = &epochIndex } @@ -365,16 +470,14 @@ func handleListInputs(s *Service, w http.ResponseWriter, r *http.Request, req RP if params.Sender != nil { sender, err := config.ToAddressFromString(*params.Sender) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid input sender address: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid input sender address: %v", err), nil) } inputFilter.Sender = &sender } if params.TransactionHash != nil { transactionHash, err := config.ToHashFromString(*params.TransactionHash) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid transaction hash: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid transaction hash: %v", err), nil) } inputFilter.TransactionHash = &transactionHash } @@ -385,11 +488,12 @@ func handleListInputs(s *Service, w http.ResponseWriter, r *http.Request, req RP }, params.Descending) if err != nil { s.Logger.Error("Unable to retrieve inputs from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } - if len(inputs) == 0 && s.applicationAbsentOrError(w, r, req, params.Application) { - return + if len(inputs) == 0 { + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err + } } resultInputs := make([]*api.DecodedInput, 0, len(inputs)) @@ -401,48 +505,43 @@ func handleListInputs(s *Service, w http.ResponseWriter, r *http.Request, req RP resultInputs = append(resultInputs, decoded) } - writeRPCResult(w, req.ID, api.ListResponse[*api.DecodedInput]{ + return api.ListResponse[*api.DecodedInput]{ Data: resultInputs, Pagination: api.Pagination{ TotalCount: total, Limit: params.Limit, Offset: params.Offset, }, - }) + }, nil } -func handleGetInput(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleGetInput(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetInputParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } index, err := config.ToIndexFromString(params.InputIndex) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid input index: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid input index: %v", err), nil) } input, err := s.repository.GetInput(r.Context(), params.Application, index) if err != nil { s.Logger.Error("Unable to retrieve input from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } if input == nil { - if s.applicationAbsentOrError(w, r, req, params.Application) { - return + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err } - writeRPCError(w, req.ID, JSONRPC_RESOURCE_NOT_FOUND, "Input not found", nil) - return + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Input not found", nil) } decoded, err := api.DecodeInput(input, s.inputABI) @@ -450,43 +549,38 @@ func handleGetInput(s *Service, w http.ResponseWriter, r *http.Request, req RPCR s.Logger.Error("Unable to decode Input", "app", params.Application, "index", input.Index, "err", err) } - writeRPCResult(w, req.ID, api.SingleResponse[*api.DecodedInput]{Data: decoded}) + return api.SingleResponse[*api.DecodedInput]{Data: decoded}, nil } -func handleGetProcessedInputCount(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleGetProcessedInputCount(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetApplicationParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } processedInputs, err := s.repository.GetProcessedInputCount(r.Context(), params.Application) if errors.Is(err, repository.ErrNotFound) { - writeRPCError(w, req.ID, JSONRPC_APPLICATION_NOT_FOUND, "Application not found", nil) - return + return nil, newRPCError(JSONRPC_APPLICATION_NOT_FOUND, "Application not found", nil) } if err != nil { s.Logger.Error("Unable to retrieve application from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } - writeRPCResult(w, req.ID, api.SingleResponse[string]{Data: fmt.Sprintf("0x%x", processedInputs)}) + return api.SingleResponse[string]{Data: fmt.Sprintf("0x%x", processedInputs)}, nil } -func handleListOutputs(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleListOutputs(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListOutputsParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Use default values if not provided @@ -500,8 +594,7 @@ func handleListOutputs(s *Service, w http.ResponseWriter, r *http.Request, req R // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } // Create output filter based on params @@ -509,8 +602,7 @@ func handleListOutputs(s *Service, w http.ResponseWriter, r *http.Request, req R if params.EpochIndex != nil { epochIndex, err := config.ToIndexFromString(*params.EpochIndex) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) } outputFilter.EpochIndex = &epochIndex } @@ -518,8 +610,7 @@ func handleListOutputs(s *Service, w http.ResponseWriter, r *http.Request, req R if params.InputIndex != nil { inputIndex, err := config.ToIndexFromString(*params.InputIndex) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid input index: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid input index: %v", err), nil) } outputFilter.InputIndex = &inputIndex } @@ -528,8 +619,7 @@ func handleListOutputs(s *Service, w http.ResponseWriter, r *http.Request, req R if params.OutputType != nil { outputType, err := api.ParseOutputType(*params.OutputType) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid output type: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid output type: %v", err), nil) } outputFilter.OutputType = &outputType } @@ -538,8 +628,7 @@ func handleListOutputs(s *Service, w http.ResponseWriter, r *http.Request, req R if params.VoucherAddress != nil { voucherAddress, err := config.ToAddressFromString(*params.VoucherAddress) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid voucher address: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid voucher address: %v", err), nil) } outputFilter.VoucherAddress = &voucherAddress } @@ -550,8 +639,7 @@ func handleListOutputs(s *Service, w http.ResponseWriter, r *http.Request, req R }, params.Descending) if err != nil { s.Logger.Error("Unable to retrieve outputs from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } resultOutputs := make([]*api.DecodedOutput, 0, len(outputs)) @@ -563,52 +651,49 @@ func handleListOutputs(s *Service, w http.ResponseWriter, r *http.Request, req R resultOutputs = append(resultOutputs, decoded) } - if len(resultOutputs) == 0 && s.applicationAbsentOrError(w, r, req, params.Application) { - return + if len(resultOutputs) == 0 { + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err + } } - writeRPCResult(w, req.ID, api.ListResponse[*api.DecodedOutput]{ + return api.ListResponse[*api.DecodedOutput]{ Data: resultOutputs, Pagination: api.Pagination{ TotalCount: total, Limit: params.Limit, Offset: params.Offset, }, - }) + }, nil } -func handleGetOutput(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleGetOutput(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetOutputParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } index, err := config.ToIndexFromString(params.OutputIndex) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid output index: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid output index: %v", err), nil) } output, err := s.repository.GetOutput(r.Context(), params.Application, index) if err != nil { s.Logger.Error("Unable to retrieve output from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } if output == nil { - if s.applicationAbsentOrError(w, r, req, params.Application) { - return + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err } - writeRPCError(w, req.ID, JSONRPC_RESOURCE_NOT_FOUND, "Output not found", nil) - return + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Output not found", nil) } decoded, err := api.DecodeOutput(output, s.outputABI) @@ -616,15 +701,14 @@ func handleGetOutput(s *Service, w http.ResponseWriter, r *http.Request, req RPC s.Logger.Error("Unable to decode Output", "app", params.Application, "index", output.Index, "err", err) } - writeRPCResult(w, req.ID, api.SingleResponse[*api.DecodedOutput]{Data: decoded}) + return api.SingleResponse[*api.DecodedOutput]{Data: decoded}, nil } -func handleListReports(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleListReports(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListReportsParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Use default values if not provided @@ -638,8 +722,7 @@ func handleListReports(s *Service, w http.ResponseWriter, r *http.Request, req R // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } // Create report filter based on params @@ -647,8 +730,7 @@ func handleListReports(s *Service, w http.ResponseWriter, r *http.Request, req R if params.EpochIndex != nil { epochIndex, err := config.ToIndexFromString(*params.EpochIndex) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) } reportFilter.EpochIndex = &epochIndex } @@ -656,8 +738,7 @@ func handleListReports(s *Service, w http.ResponseWriter, r *http.Request, req R if params.InputIndex != nil { inputIndex, err := config.ToIndexFromString(*params.InputIndex) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid input index: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid input index: %v", err), nil) } reportFilter.InputIndex = &inputIndex } @@ -668,70 +749,65 @@ func handleListReports(s *Service, w http.ResponseWriter, r *http.Request, req R }, params.Descending) if err != nil { s.Logger.Error("Unable to retrieve reports from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } - if len(reports) == 0 && s.applicationAbsentOrError(w, r, req, params.Application) { - return + if len(reports) == 0 { + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err + } } if reports == nil { reports = []*model.Report{} } - writeRPCResult(w, req.ID, api.ListResponse[*model.Report]{ + return api.ListResponse[*model.Report]{ Data: reports, Pagination: api.Pagination{ TotalCount: total, Limit: params.Limit, Offset: params.Offset, }, - }) + }, nil } -func handleGetReport(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleGetReport(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetReportParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } index, err := config.ToIndexFromString(params.ReportIndex) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid report index: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid report index: %v", err), nil) } report, err := s.repository.GetReport(r.Context(), params.Application, index) if err != nil { s.Logger.Error("Unable to retrieve report from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } if report == nil { - if s.applicationAbsentOrError(w, r, req, params.Application) { - return + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err } - writeRPCError(w, req.ID, JSONRPC_RESOURCE_NOT_FOUND, "Report not found", nil) - return + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Report not found", nil) } - writeRPCResult(w, req.ID, api.SingleResponse[*model.Report]{Data: report}) + return api.SingleResponse[*model.Report]{Data: report}, nil } -func handleListWithdrawals(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleListWithdrawals(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListWithdrawalsParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } if params.Limit <= 0 { @@ -742,16 +818,14 @@ func handleListWithdrawals(s *Service, w http.ResponseWriter, r *http.Request, r } if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } withdrawalFilter := repository.WithdrawalFilter{} if params.AccountIndex != nil { accountIndex, err := config.ToIndexFromString(*params.AccountIndex) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid account index: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid account index: %v", err), nil) } withdrawalFilter.AccountIndex = &accountIndex } @@ -763,69 +837,64 @@ func handleListWithdrawals(s *Service, w http.ResponseWriter, r *http.Request, r ) if err != nil { s.Logger.Error("Unable to retrieve withdrawals from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } - if len(withdrawals) == 0 && s.applicationAbsentOrError(w, r, req, params.Application) { - return + if len(withdrawals) == 0 { + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err + } } if withdrawals == nil { withdrawals = []*model.Withdrawal{} } - writeRPCResult(w, req.ID, api.ListResponse[*model.Withdrawal]{ + return api.ListResponse[*model.Withdrawal]{ Data: withdrawals, Pagination: api.Pagination{ TotalCount: total, Limit: params.Limit, Offset: params.Offset, }, - }) + }, nil } -func handleGetWithdrawal(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleGetWithdrawal(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetWithdrawalParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } accountIndex, err := config.ToIndexFromString(params.AccountIndex) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid account index: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid account index: %v", err), nil) } withdrawal, err := s.repository.GetWithdrawal(r.Context(), params.Application, accountIndex) if err != nil { s.Logger.Error("Unable to retrieve withdrawal from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } if withdrawal == nil { - if s.applicationAbsentOrError(w, r, req, params.Application) { - return + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err } - writeRPCError(w, req.ID, JSONRPC_RESOURCE_NOT_FOUND, "Withdrawal not found", nil) - return + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Withdrawal not found", nil) } - writeRPCResult(w, req.ID, api.SingleResponse[*model.Withdrawal]{Data: withdrawal}) + return api.SingleResponse[*model.Withdrawal]{Data: withdrawal}, nil } -func handleListTournaments(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleListTournaments(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListTournamentsParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Use default values if not provided @@ -839,8 +908,7 @@ func handleListTournaments(s *Service, w http.ResponseWriter, r *http.Request, r // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } // Create tournament filter based on params @@ -848,8 +916,7 @@ func handleListTournaments(s *Service, w http.ResponseWriter, r *http.Request, r if params.EpochIndex != nil { epochIndex, err := config.ToIndexFromString(*params.EpochIndex) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) } tournamentFilter.EpochIndex = &epochIndex } @@ -857,8 +924,7 @@ func handleListTournaments(s *Service, w http.ResponseWriter, r *http.Request, r if params.Level != nil { level, err := config.ToIndexFromString(*params.Level) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid level: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid level: %v", err), nil) } tournamentFilter.Level = &level } @@ -866,8 +932,7 @@ func handleListTournaments(s *Service, w http.ResponseWriter, r *http.Request, r if params.ParentTournamentAddress != nil { parentAddress, err := config.ToAddressFromString(*params.ParentTournamentAddress) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid parent tournament address: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid parent tournament address: %v", err), nil) } tournamentFilter.ParentTournamentAddress = &parentAddress } @@ -875,8 +940,7 @@ func handleListTournaments(s *Service, w http.ResponseWriter, r *http.Request, r if params.ParentMatchIDHash != nil { parentMatchIDHash, err := config.ToHashFromString(*params.ParentMatchIDHash) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid parent match ID hash: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid parent match ID hash: %v", err), nil) } tournamentFilter.ParentMatchIDHash = &parentMatchIDHash } @@ -887,69 +951,64 @@ func handleListTournaments(s *Service, w http.ResponseWriter, r *http.Request, r }, params.Descending) if err != nil { s.Logger.Error("Unable to retrieve tournaments from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } - if len(tournaments) == 0 && s.applicationAbsentOrError(w, r, req, params.Application) { - return + if len(tournaments) == 0 { + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err + } } if tournaments == nil { tournaments = []*model.Tournament{} } - writeRPCResult(w, req.ID, api.ListResponse[*model.Tournament]{ + return api.ListResponse[*model.Tournament]{ Data: tournaments, Pagination: api.Pagination{ TotalCount: total, Limit: params.Limit, Offset: params.Offset, }, - }) + }, nil } -func handleGetTournament(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleGetTournament(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetTournamentParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } // Validate tournament address if _, err := config.ToAddressFromString(params.Address); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) } tournament, err := s.repository.GetTournament(r.Context(), params.Application, params.Address) if err != nil { s.Logger.Error("Unable to retrieve tournament from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } if tournament == nil { - if s.applicationAbsentOrError(w, r, req, params.Application) { - return + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err } - writeRPCError(w, req.ID, JSONRPC_RESOURCE_NOT_FOUND, "Tournament not found", nil) - return + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Tournament not found", nil) } - writeRPCResult(w, req.ID, api.SingleResponse[*model.Tournament]{Data: tournament}) + return api.SingleResponse[*model.Tournament]{Data: tournament}, nil } -func handleListCommitments(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleListCommitments(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListCommitmentsParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Use default values if not provided @@ -963,8 +1022,7 @@ func handleListCommitments(s *Service, w http.ResponseWriter, r *http.Request, r // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } // Create commitment filter based on params @@ -972,16 +1030,14 @@ func handleListCommitments(s *Service, w http.ResponseWriter, r *http.Request, r if params.EpochIndex != nil { epochIndex, err := config.ToIndexFromString(*params.EpochIndex) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) } commitmentFilter.EpochIndex = &epochIndex } if params.TournamentAddress != nil { if _, err := config.ToAddressFromString(*params.TournamentAddress); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) } commitmentFilter.TournamentAddress = params.TournamentAddress } @@ -992,83 +1048,75 @@ func handleListCommitments(s *Service, w http.ResponseWriter, r *http.Request, r }, params.Descending) if err != nil { s.Logger.Error("Unable to retrieve commitments from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } - if len(commitments) == 0 && s.applicationAbsentOrError(w, r, req, params.Application) { - return + if len(commitments) == 0 { + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err + } } if commitments == nil { commitments = []*model.Commitment{} } - writeRPCResult(w, req.ID, api.ListResponse[*model.Commitment]{ + return api.ListResponse[*model.Commitment]{ Data: commitments, Pagination: api.Pagination{ TotalCount: total, Limit: params.Limit, Offset: params.Offset, }, - }) + }, nil } -func handleGetCommitment(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleGetCommitment(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetCommitmentParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } epochIndex, err := config.ToIndexFromString(params.EpochIndex) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) } if _, err := config.ToAddressFromString(params.TournamentAddress); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) } if len(params.Commitment) == 0 { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid commitment hex: Empty string", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid commitment hex: Empty string", nil) } if _, err := config.ToHashFromString(params.Commitment); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid commitment hex: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid commitment hex: %v", err), nil) } commitment, err := s.repository.GetCommitment(r.Context(), params.Application, epochIndex, params.TournamentAddress, params.Commitment) if err != nil { s.Logger.Error("Unable to retrieve commitment from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } if commitment == nil { - if s.applicationAbsentOrError(w, r, req, params.Application) { - return + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err } - writeRPCError(w, req.ID, JSONRPC_RESOURCE_NOT_FOUND, "Commitment not found", nil) - return + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Commitment not found", nil) } - writeRPCResult(w, req.ID, api.SingleResponse[*model.Commitment]{Data: commitment}) + return api.SingleResponse[*model.Commitment]{Data: commitment}, nil } -func handleListMatches(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleListMatches(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListMatchesParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Use default values if not provided @@ -1082,8 +1130,7 @@ func handleListMatches(s *Service, w http.ResponseWriter, r *http.Request, req R // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } // Create match filter based on params @@ -1091,16 +1138,14 @@ func handleListMatches(s *Service, w http.ResponseWriter, r *http.Request, req R if params.EpochIndex != nil { epochIndex, err := config.ToIndexFromString(*params.EpochIndex) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) } matchFilter.EpochIndex = &epochIndex } if params.TournamentAddress != nil { if _, err := config.ToAddressFromString(*params.TournamentAddress); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) } matchFilter.TournamentAddress = params.TournamentAddress } @@ -1111,79 +1156,72 @@ func handleListMatches(s *Service, w http.ResponseWriter, r *http.Request, req R }, params.Descending) if err != nil { s.Logger.Error("Unable to retrieve matches from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } - if len(matches) == 0 && s.applicationAbsentOrError(w, r, req, params.Application) { - return + if len(matches) == 0 { + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err + } } if matches == nil { matches = []*model.Match{} } - writeRPCResult(w, req.ID, api.ListResponse[*model.Match]{ + return api.ListResponse[*model.Match]{ Data: matches, Pagination: api.Pagination{ TotalCount: total, Limit: params.Limit, Offset: params.Offset, }, - }) + }, nil } -func handleGetMatch(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleGetMatch(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetMatchParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } epochIndex, err := config.ToIndexFromString(params.EpochIndex) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) } if _, err := config.ToAddressFromString(params.TournamentAddress); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) } if _, err := config.ToHashFromString(params.IDHash); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid ID hash: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid ID hash: %v", err), nil) } match, err := s.repository.GetMatch(r.Context(), params.Application, epochIndex, params.TournamentAddress, params.IDHash) if err != nil { s.Logger.Error("Unable to retrieve match from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } if match == nil { - if s.applicationAbsentOrError(w, r, req, params.Application) { - return + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err } - writeRPCError(w, req.ID, JSONRPC_RESOURCE_NOT_FOUND, "Match not found", nil) - return + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Match not found", nil) } - writeRPCResult(w, req.ID, api.SingleResponse[*model.Match]{Data: match}) + return api.SingleResponse[*model.Match]{Data: match}, nil } -func handleListMatchAdvances(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleListMatchAdvances(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListMatchAdvancesParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Use default values if not provided @@ -1197,25 +1235,21 @@ func handleListMatchAdvances(s *Service, w http.ResponseWriter, r *http.Request, // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } // Create match advance filter based on params epochIndex, err := config.ToIndexFromString(params.EpochIndex) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) } if _, err := config.ToAddressFromString(params.TournamentAddress); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) } if _, err := config.ToHashFromString(params.IDHash); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid ID hash: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid ID hash: %v", err), nil) } pagination := repository.Pagination{ @@ -1226,112 +1260,99 @@ func handleListMatchAdvances(s *Service, w http.ResponseWriter, r *http.Request, params.TournamentAddress, params.IDHash, pagination, params.Descending) if err != nil { s.Logger.Error("Unable to retrieve match advances from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } - if len(matchAdvances) == 0 && s.applicationAbsentOrError(w, r, req, params.Application) { - return + if len(matchAdvances) == 0 { + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err + } } if matchAdvances == nil { matchAdvances = []*model.MatchAdvanced{} } - writeRPCResult(w, req.ID, api.ListResponse[*model.MatchAdvanced]{ + return api.ListResponse[*model.MatchAdvanced]{ Data: matchAdvances, Pagination: api.Pagination{ TotalCount: total, Limit: params.Limit, Offset: params.Offset, }, - }) + }, nil } -func handleGetMatchAdvanced(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleGetMatchAdvanced(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetMatchAdvancedParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) } epochIndex, err := config.ToIndexFromString(params.EpochIndex) if err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) } if _, err := config.ToAddressFromString(params.TournamentAddress); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) } if _, err := config.ToHashFromString(params.IDHash); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid ID hash: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid ID hash: %v", err), nil) } if _, err := config.ToHashFromString(params.Parent); err != nil { - writeRPCError(w, req.ID, JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid parent hash: %v", err), nil) - return + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid parent hash: %v", err), nil) } matchAdvanced, err := s.repository.GetMatchAdvanced(r.Context(), params.Application, epochIndex, params.TournamentAddress, params.IDHash, params.Parent[2:]) // TODO: use parsed value if err != nil { s.Logger.Error("Unable to retrieve match advanced from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } if matchAdvanced == nil { - if s.applicationAbsentOrError(w, r, req, params.Application) { - return + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err } - writeRPCError(w, req.ID, JSONRPC_RESOURCE_NOT_FOUND, "Match advanced not found", nil) - return + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Match advanced not found", nil) } - writeRPCResult(w, req.ID, api.SingleResponse[*model.MatchAdvanced]{Data: matchAdvanced}) + return api.SingleResponse[*model.MatchAdvanced]{Data: matchAdvanced}, nil } -func handleGetChainID(s *Service, w http.ResponseWriter, r *http.Request, req RPCRequest) { +func handleGetChainID(s *Service, r *http.Request, _ RPCRequest) (any, error) { config, err := repository.LoadNodeConfig[evmreader.PersistentConfig](r.Context(), s.repository, evmreader.EvmReaderConfigKey) if errors.Is(err, repository.ErrNotFound) { - writeRPCError(w, req.ID, JSONRPC_RESOURCE_NOT_FOUND, "EVM Reader config not found", nil) - return + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "EVM Reader config not found", nil) } if err != nil { s.Logger.Error("Unable to retrieve evmreader config from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } - writeRPCResult(w, req.ID, api.SingleResponse[string]{Data: fmt.Sprintf("0x%x", config.Value.ChainID)}) + return api.SingleResponse[string]{Data: fmt.Sprintf("0x%x", config.Value.ChainID)}, nil } -func handleGetNodeVersion(_ *Service, w http.ResponseWriter, _ *http.Request, req RPCRequest) { - writeRPCResult(w, req.ID, api.SingleResponse[string]{Data: version.BuildVersion}) +func handleGetNodeVersion(_ *Service, _ *http.Request, _ RPCRequest) (any, error) { + return api.SingleResponse[string]{Data: version.BuildVersion}, nil } func (s *Service) applicationAbsentOrError( - w http.ResponseWriter, r *http.Request, - req RPCRequest, validatedNameOrAddress string, -) bool { +) error { app, err := s.repository.GetApplication(r.Context(), validatedNameOrAddress) if err != nil { s.Logger.Error("Unable to retrieve application from repository", "err", err) - writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) - return true + return newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) } else if app == nil { - writeRPCError(w, req.ID, JSONRPC_APPLICATION_NOT_FOUND, "Application not found", nil) - return true + return newRPCError(JSONRPC_APPLICATION_NOT_FOUND, "Application not found", nil) } - return false + return nil } diff --git a/internal/jsonrpc/limitedwriter.go b/internal/jsonrpc/limitedwriter.go new file mode 100644 index 000000000..2ab55396f --- /dev/null +++ b/internal/jsonrpc/limitedwriter.go @@ -0,0 +1,58 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package jsonrpc + +import ( + "bytes" + "io" +) + +type budgetWriter struct { + writer io.Writer + budget int + closed bool +} + +func newBudgetWriter(writer io.Writer, limit int) *budgetWriter { + return &budgetWriter{ + writer: writer, + budget: limit, + } +} + +func (w *budgetWriter) Write(data []byte) (int, error) { + written, err := w.writer.Write(data) + if err == nil { + w.budget -= written + } + return written, err +} + +func (w *budgetWriter) NewLimitedWriter() *limitedWriter { + if w.closed { + return nil + } + return &limitedWriter{writer: w} +} + +type limitedWriter struct { + writer *budgetWriter + buffer bytes.Buffer +} + +func (w *limitedWriter) Write(data []byte) (int, error) { + if w.buffer.Len()+len(data) > w.writer.budget { + w.writer.closed = true + return 0, io.ErrShortBuffer + } + return w.buffer.Write(data) +} + +func (w *limitedWriter) Flush() error { + if w.writer.closed { + return nil + } + _, err := w.writer.Write(w.buffer.Bytes()) + return err +} diff --git a/internal/jsonrpc/types.go b/internal/jsonrpc/types.go index 46607995e..9598bc03d 100644 --- a/internal/jsonrpc/types.go +++ b/internal/jsonrpc/types.go @@ -7,7 +7,7 @@ import ( "bytes" "encoding/json" "fmt" - "net/http" + "io" "reflect" "regexp" @@ -38,8 +38,16 @@ type RPCError struct { Data any `json:"data,omitempty"` } +func (e *RPCError) Error() string { + return e.Message +} + +func newRPCError(code int, message string, data any) error { + return &RPCError{Code: code, Message: message, Data: data} +} + // writeRPCError sends a generic error response for internal errors. -func writeRPCError(w http.ResponseWriter, id any, code int, message string, data any) { +func writeRPCError(w io.Writer, id any, code int, message string, data any) error { // Hide detailed error info for internal errors. if code == JSONRPC_INTERNAL_ERROR { message = "Internal server error" @@ -54,18 +62,16 @@ func writeRPCError(w http.ResponseWriter, id any, code int, message string, data }, ID: id, } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(resp) + return json.NewEncoder(w).Encode(resp) } -func writeRPCResult(w http.ResponseWriter, id any, result any) { +func writeRPCResult(w io.Writer, id any, result any) error { resp := RPCResponse{ JSONRPC: "2.0", Result: result, ID: id, } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(resp) + return json.NewEncoder(w).Encode(resp) } // UnmarshalParams supports both by-name (object) and by-position (array) parameter structures. From 17e63b9e0b689e791e10e0933d6d490e07b6f1ae Mon Sep 17 00:00:00 2001 From: Renato Maia <1887792+renatomaia@users.noreply.github.com> Date: Tue, 28 Jul 2026 00:56:09 -0300 Subject: [PATCH 02/13] refactor(jsonrcp): remove unused field 'data' from error reponses --- internal/jsonrpc/jsonrpc.go | 254 ++++++++++++++++++------------------ internal/jsonrpc/types.go | 9 +- 2 files changed, 130 insertions(+), 133 deletions(-) diff --git a/internal/jsonrpc/jsonrpc.go b/internal/jsonrpc/jsonrpc.go index a96cde1e0..4bd3e22f0 100644 --- a/internal/jsonrpc/jsonrpc.go +++ b/internal/jsonrpc/jsonrpc.go @@ -106,7 +106,7 @@ func (s *Service) writeByte(w http.ResponseWriter, c byte) bool { // writeRPCError sends a generic error response for internal errors. func (s *Service) writeRPCError(w http.ResponseWriter, id any, code int, message string) bool { - err := writeRPCError(w, id, code, message, nil) + err := writeRPCError(w, id, code, message) return s.handleResponseResult(w, err) } @@ -115,12 +115,12 @@ func (s *Service) dispatchOneRequest(w io.Writer, r *http.Request, req RPCReques case req.JSONRPC != "2.0": fallthrough case req.Method == "": - return writeRPCError(w, nil, JSONRPC_INVALID_REQUEST, "invalid request", nil) + return writeRPCError(w, nil, JSONRPC_INVALID_REQUEST, "invalid request") } fn, ok := jsonrpcHandlers[req.Method] if !ok { s.Logger.Debug(fmt.Sprintf("RPC method not found: %s", req.Method)) - return writeRPCError(w, req.ID, JSONRPC_METHOD_NOT_FOUND, "Method not found", nil) + return writeRPCError(w, req.ID, JSONRPC_METHOD_NOT_FOUND, "Method not found") } result, err := fn(s, r, req) @@ -130,11 +130,11 @@ func (s *Service) dispatchOneRequest(w io.Writer, r *http.Request, req RPCReques var rpcErr *RPCError if errors.As(err, &rpcErr) { - return writeRPCError(w, req.ID, rpcErr.Code, rpcErr.Message, rpcErr.Data) + return writeRPCError(w, req.ID, rpcErr.Code, rpcErr.Message) } s.Logger.Error("RPC method failed", "method", req.Method, "error", err) - return writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return writeRPCError(w, req.ID, JSONRPC_INTERNAL_ERROR, "Internal server error") } func (s *Service) handleRPC(w http.ResponseWriter, r *http.Request) { @@ -247,12 +247,12 @@ func handleDiscover(s *Service, _ *http.Request, _ RPCRequest) (any, error) { data, err := discoverSpec.ReadFile("jsonrpc-discover.json") if err != nil { s.Logger.Error("Unable to read jsonrpc-discover content", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } var spec any if err := json.Unmarshal(data, &spec); err != nil { s.Logger.Error("Unable to unmarshal discovery spec JSON", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } return spec, nil } @@ -261,7 +261,7 @@ func handleListApplications(s *Service, r *http.Request, req RPCRequest) (any, e var params api.ListApplicationsParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Use default values if not provided if params.Limit <= 0 { @@ -278,7 +278,7 @@ func handleListApplications(s *Service, r *http.Request, req RPCRequest) (any, e }, params.Descending) if err != nil { s.Logger.Error("Unable to retrieve applications from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } if apps == nil { apps = []*model.Application{} @@ -298,21 +298,21 @@ func handleGetApplication(s *Service, r *http.Request, req RPCRequest) (any, err var params api.GetApplicationParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } app, err := s.repository.GetApplication(r.Context(), params.Application) if err != nil { s.Logger.Error("Unable to retrieve application from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } if app == nil { - return nil, newRPCError(JSONRPC_APPLICATION_NOT_FOUND, "Application not found", nil) + return nil, newRPCError(JSONRPC_APPLICATION_NOT_FOUND, "Application not found") } return api.SingleResponse[*model.Application]{Data: app}, nil @@ -322,7 +322,7 @@ func handleListEpochs(s *Service, r *http.Request, req RPCRequest) (any, error) var params api.ListEpochsParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Use default values if not provided @@ -336,14 +336,14 @@ func handleListEpochs(s *Service, r *http.Request, req RPCRequest) (any, error) // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } var epochFilter repository.EpochFilter if params.Status != nil { var status model.EpochStatus if err := status.Scan(*params.Status); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch status: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch status: %v", err)) } epochFilter.Status = []model.EpochStatus{status} } @@ -354,7 +354,7 @@ func handleListEpochs(s *Service, r *http.Request, req RPCRequest) (any, error) }, params.Descending) if err != nil { s.Logger.Error("Unable to retrieve epochs from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } if len(epochs) == 0 { @@ -380,29 +380,29 @@ func handleGetEpoch(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetEpochParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } index, err := config.ToIndexFromString(params.EpochIndex) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err)) } epoch, err := s.repository.GetEpoch(r.Context(), params.Application, index) if err != nil { s.Logger.Error("Unable to retrieve epoch from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } if epoch == nil { if err := s.applicationAbsentOrError(r, params.Application); err != nil { return nil, err } - return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Epoch not found", nil) + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Epoch not found") } return api.SingleResponse[*model.Epoch]{Data: epoch}, nil @@ -412,12 +412,12 @@ func handleGetLastAcceptedEpochIndex(s *Service, r *http.Request, req RPCRequest var params api.GetLastAcceptedEpochIndexParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } index, err := s.repository.GetLastAcceptedEpochIndex(r.Context(), params.Application) @@ -425,11 +425,11 @@ func handleGetLastAcceptedEpochIndex(s *Service, r *http.Request, req RPCRequest if err := s.applicationAbsentOrError(r, params.Application); err != nil { return nil, err } - return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Epoch not found", nil) + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Epoch not found") } if err != nil { s.Logger.Error("Unable to retrieve epoch from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } return api.SingleResponse[string]{Data: fmt.Sprintf("0x%x", index)}, nil @@ -439,7 +439,7 @@ func handleListInputs(s *Service, r *http.Request, req RPCRequest) (any, error) var params api.ListInputsParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Use default values if not provided @@ -453,7 +453,7 @@ func handleListInputs(s *Service, r *http.Request, req RPCRequest) (any, error) // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } // Create input filter based on params @@ -461,7 +461,7 @@ func handleListInputs(s *Service, r *http.Request, req RPCRequest) (any, error) if params.EpochIndex != nil { epochIndex, err := config.ToIndexFromString(*params.EpochIndex) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err)) } inputFilter.EpochIndex = &epochIndex } @@ -470,14 +470,14 @@ func handleListInputs(s *Service, r *http.Request, req RPCRequest) (any, error) if params.Sender != nil { sender, err := config.ToAddressFromString(*params.Sender) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid input sender address: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid input sender address: %v", err)) } inputFilter.Sender = &sender } if params.TransactionHash != nil { transactionHash, err := config.ToHashFromString(*params.TransactionHash) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid transaction hash: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid transaction hash: %v", err)) } inputFilter.TransactionHash = &transactionHash } @@ -488,7 +488,7 @@ func handleListInputs(s *Service, r *http.Request, req RPCRequest) (any, error) }, params.Descending) if err != nil { s.Logger.Error("Unable to retrieve inputs from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } if len(inputs) == 0 { if err := s.applicationAbsentOrError(r, params.Application); err != nil { @@ -519,29 +519,29 @@ func handleGetInput(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetInputParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } index, err := config.ToIndexFromString(params.InputIndex) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid input index: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid input index: %v", err)) } input, err := s.repository.GetInput(r.Context(), params.Application, index) if err != nil { s.Logger.Error("Unable to retrieve input from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } if input == nil { if err := s.applicationAbsentOrError(r, params.Application); err != nil { return nil, err } - return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Input not found", nil) + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Input not found") } decoded, err := api.DecodeInput(input, s.inputABI) @@ -556,21 +556,21 @@ func handleGetProcessedInputCount(s *Service, r *http.Request, req RPCRequest) ( var params api.GetApplicationParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } processedInputs, err := s.repository.GetProcessedInputCount(r.Context(), params.Application) if errors.Is(err, repository.ErrNotFound) { - return nil, newRPCError(JSONRPC_APPLICATION_NOT_FOUND, "Application not found", nil) + return nil, newRPCError(JSONRPC_APPLICATION_NOT_FOUND, "Application not found") } if err != nil { s.Logger.Error("Unable to retrieve application from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } return api.SingleResponse[string]{Data: fmt.Sprintf("0x%x", processedInputs)}, nil @@ -580,7 +580,7 @@ func handleListOutputs(s *Service, r *http.Request, req RPCRequest) (any, error) var params api.ListOutputsParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Use default values if not provided @@ -594,7 +594,7 @@ func handleListOutputs(s *Service, r *http.Request, req RPCRequest) (any, error) // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } // Create output filter based on params @@ -602,7 +602,7 @@ func handleListOutputs(s *Service, r *http.Request, req RPCRequest) (any, error) if params.EpochIndex != nil { epochIndex, err := config.ToIndexFromString(*params.EpochIndex) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err)) } outputFilter.EpochIndex = &epochIndex } @@ -610,7 +610,7 @@ func handleListOutputs(s *Service, r *http.Request, req RPCRequest) (any, error) if params.InputIndex != nil { inputIndex, err := config.ToIndexFromString(*params.InputIndex) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid input index: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid input index: %v", err)) } outputFilter.InputIndex = &inputIndex } @@ -619,7 +619,7 @@ func handleListOutputs(s *Service, r *http.Request, req RPCRequest) (any, error) if params.OutputType != nil { outputType, err := api.ParseOutputType(*params.OutputType) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid output type: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid output type: %v", err)) } outputFilter.OutputType = &outputType } @@ -628,7 +628,7 @@ func handleListOutputs(s *Service, r *http.Request, req RPCRequest) (any, error) if params.VoucherAddress != nil { voucherAddress, err := config.ToAddressFromString(*params.VoucherAddress) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid voucher address: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid voucher address: %v", err)) } outputFilter.VoucherAddress = &voucherAddress } @@ -639,7 +639,7 @@ func handleListOutputs(s *Service, r *http.Request, req RPCRequest) (any, error) }, params.Descending) if err != nil { s.Logger.Error("Unable to retrieve outputs from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } resultOutputs := make([]*api.DecodedOutput, 0, len(outputs)) @@ -671,29 +671,29 @@ func handleGetOutput(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetOutputParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } index, err := config.ToIndexFromString(params.OutputIndex) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid output index: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid output index: %v", err)) } output, err := s.repository.GetOutput(r.Context(), params.Application, index) if err != nil { s.Logger.Error("Unable to retrieve output from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } if output == nil { if err := s.applicationAbsentOrError(r, params.Application); err != nil { return nil, err } - return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Output not found", nil) + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Output not found") } decoded, err := api.DecodeOutput(output, s.outputABI) @@ -708,7 +708,7 @@ func handleListReports(s *Service, r *http.Request, req RPCRequest) (any, error) var params api.ListReportsParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Use default values if not provided @@ -722,7 +722,7 @@ func handleListReports(s *Service, r *http.Request, req RPCRequest) (any, error) // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } // Create report filter based on params @@ -730,7 +730,7 @@ func handleListReports(s *Service, r *http.Request, req RPCRequest) (any, error) if params.EpochIndex != nil { epochIndex, err := config.ToIndexFromString(*params.EpochIndex) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err)) } reportFilter.EpochIndex = &epochIndex } @@ -738,7 +738,7 @@ func handleListReports(s *Service, r *http.Request, req RPCRequest) (any, error) if params.InputIndex != nil { inputIndex, err := config.ToIndexFromString(*params.InputIndex) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid input index: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid input index: %v", err)) } reportFilter.InputIndex = &inputIndex } @@ -749,7 +749,7 @@ func handleListReports(s *Service, r *http.Request, req RPCRequest) (any, error) }, params.Descending) if err != nil { s.Logger.Error("Unable to retrieve reports from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } if len(reports) == 0 { @@ -775,29 +775,29 @@ func handleGetReport(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetReportParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } index, err := config.ToIndexFromString(params.ReportIndex) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid report index: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid report index: %v", err)) } report, err := s.repository.GetReport(r.Context(), params.Application, index) if err != nil { s.Logger.Error("Unable to retrieve report from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } if report == nil { if err := s.applicationAbsentOrError(r, params.Application); err != nil { return nil, err } - return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Report not found", nil) + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Report not found") } return api.SingleResponse[*model.Report]{Data: report}, nil @@ -807,7 +807,7 @@ func handleListWithdrawals(s *Service, r *http.Request, req RPCRequest) (any, er var params api.ListWithdrawalsParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } if params.Limit <= 0 { @@ -818,14 +818,14 @@ func handleListWithdrawals(s *Service, r *http.Request, req RPCRequest) (any, er } if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } withdrawalFilter := repository.WithdrawalFilter{} if params.AccountIndex != nil { accountIndex, err := config.ToIndexFromString(*params.AccountIndex) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid account index: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid account index: %v", err)) } withdrawalFilter.AccountIndex = &accountIndex } @@ -837,7 +837,7 @@ func handleListWithdrawals(s *Service, r *http.Request, req RPCRequest) (any, er ) if err != nil { s.Logger.Error("Unable to retrieve withdrawals from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } if len(withdrawals) == 0 { @@ -863,28 +863,28 @@ func handleGetWithdrawal(s *Service, r *http.Request, req RPCRequest) (any, erro var params api.GetWithdrawalParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } accountIndex, err := config.ToIndexFromString(params.AccountIndex) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid account index: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid account index: %v", err)) } withdrawal, err := s.repository.GetWithdrawal(r.Context(), params.Application, accountIndex) if err != nil { s.Logger.Error("Unable to retrieve withdrawal from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } if withdrawal == nil { if err := s.applicationAbsentOrError(r, params.Application); err != nil { return nil, err } - return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Withdrawal not found", nil) + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Withdrawal not found") } return api.SingleResponse[*model.Withdrawal]{Data: withdrawal}, nil @@ -894,7 +894,7 @@ func handleListTournaments(s *Service, r *http.Request, req RPCRequest) (any, er var params api.ListTournamentsParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Use default values if not provided @@ -908,7 +908,7 @@ func handleListTournaments(s *Service, r *http.Request, req RPCRequest) (any, er // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } // Create tournament filter based on params @@ -916,7 +916,7 @@ func handleListTournaments(s *Service, r *http.Request, req RPCRequest) (any, er if params.EpochIndex != nil { epochIndex, err := config.ToIndexFromString(*params.EpochIndex) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err)) } tournamentFilter.EpochIndex = &epochIndex } @@ -924,7 +924,7 @@ func handleListTournaments(s *Service, r *http.Request, req RPCRequest) (any, er if params.Level != nil { level, err := config.ToIndexFromString(*params.Level) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid level: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid level: %v", err)) } tournamentFilter.Level = &level } @@ -932,7 +932,7 @@ func handleListTournaments(s *Service, r *http.Request, req RPCRequest) (any, er if params.ParentTournamentAddress != nil { parentAddress, err := config.ToAddressFromString(*params.ParentTournamentAddress) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid parent tournament address: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid parent tournament address: %v", err)) } tournamentFilter.ParentTournamentAddress = &parentAddress } @@ -940,7 +940,7 @@ func handleListTournaments(s *Service, r *http.Request, req RPCRequest) (any, er if params.ParentMatchIDHash != nil { parentMatchIDHash, err := config.ToHashFromString(*params.ParentMatchIDHash) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid parent match ID hash: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid parent match ID hash: %v", err)) } tournamentFilter.ParentMatchIDHash = &parentMatchIDHash } @@ -951,7 +951,7 @@ func handleListTournaments(s *Service, r *http.Request, req RPCRequest) (any, er }, params.Descending) if err != nil { s.Logger.Error("Unable to retrieve tournaments from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } if len(tournaments) == 0 { if err := s.applicationAbsentOrError(r, params.Application); err != nil { @@ -976,29 +976,29 @@ func handleGetTournament(s *Service, r *http.Request, req RPCRequest) (any, erro var params api.GetTournamentParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } // Validate tournament address if _, err := config.ToAddressFromString(params.Address); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err)) } tournament, err := s.repository.GetTournament(r.Context(), params.Application, params.Address) if err != nil { s.Logger.Error("Unable to retrieve tournament from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } if tournament == nil { if err := s.applicationAbsentOrError(r, params.Application); err != nil { return nil, err } - return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Tournament not found", nil) + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Tournament not found") } return api.SingleResponse[*model.Tournament]{Data: tournament}, nil @@ -1008,7 +1008,7 @@ func handleListCommitments(s *Service, r *http.Request, req RPCRequest) (any, er var params api.ListCommitmentsParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Use default values if not provided @@ -1022,7 +1022,7 @@ func handleListCommitments(s *Service, r *http.Request, req RPCRequest) (any, er // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } // Create commitment filter based on params @@ -1030,14 +1030,14 @@ func handleListCommitments(s *Service, r *http.Request, req RPCRequest) (any, er if params.EpochIndex != nil { epochIndex, err := config.ToIndexFromString(*params.EpochIndex) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err)) } commitmentFilter.EpochIndex = &epochIndex } if params.TournamentAddress != nil { if _, err := config.ToAddressFromString(*params.TournamentAddress); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err)) } commitmentFilter.TournamentAddress = params.TournamentAddress } @@ -1048,7 +1048,7 @@ func handleListCommitments(s *Service, r *http.Request, req RPCRequest) (any, er }, params.Descending) if err != nil { s.Logger.Error("Unable to retrieve commitments from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } if len(commitments) == 0 { if err := s.applicationAbsentOrError(r, params.Application); err != nil { @@ -1073,40 +1073,40 @@ func handleGetCommitment(s *Service, r *http.Request, req RPCRequest) (any, erro var params api.GetCommitmentParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } epochIndex, err := config.ToIndexFromString(params.EpochIndex) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err)) } if _, err := config.ToAddressFromString(params.TournamentAddress); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err)) } if len(params.Commitment) == 0 { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid commitment hex: Empty string", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid commitment hex: Empty string") } if _, err := config.ToHashFromString(params.Commitment); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid commitment hex: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid commitment hex: %v", err)) } commitment, err := s.repository.GetCommitment(r.Context(), params.Application, epochIndex, params.TournamentAddress, params.Commitment) if err != nil { s.Logger.Error("Unable to retrieve commitment from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } if commitment == nil { if err := s.applicationAbsentOrError(r, params.Application); err != nil { return nil, err } - return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Commitment not found", nil) + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Commitment not found") } return api.SingleResponse[*model.Commitment]{Data: commitment}, nil @@ -1116,7 +1116,7 @@ func handleListMatches(s *Service, r *http.Request, req RPCRequest) (any, error) var params api.ListMatchesParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Use default values if not provided @@ -1130,7 +1130,7 @@ func handleListMatches(s *Service, r *http.Request, req RPCRequest) (any, error) // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } // Create match filter based on params @@ -1138,14 +1138,14 @@ func handleListMatches(s *Service, r *http.Request, req RPCRequest) (any, error) if params.EpochIndex != nil { epochIndex, err := config.ToIndexFromString(*params.EpochIndex) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err)) } matchFilter.EpochIndex = &epochIndex } if params.TournamentAddress != nil { if _, err := config.ToAddressFromString(*params.TournamentAddress); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err)) } matchFilter.TournamentAddress = params.TournamentAddress } @@ -1156,7 +1156,7 @@ func handleListMatches(s *Service, r *http.Request, req RPCRequest) (any, error) }, params.Descending) if err != nil { s.Logger.Error("Unable to retrieve matches from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } if len(matches) == 0 { if err := s.applicationAbsentOrError(r, params.Application); err != nil { @@ -1181,37 +1181,37 @@ func handleGetMatch(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetMatchParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } epochIndex, err := config.ToIndexFromString(params.EpochIndex) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err)) } if _, err := config.ToAddressFromString(params.TournamentAddress); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err)) } if _, err := config.ToHashFromString(params.IDHash); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid ID hash: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid ID hash: %v", err)) } match, err := s.repository.GetMatch(r.Context(), params.Application, epochIndex, params.TournamentAddress, params.IDHash) if err != nil { s.Logger.Error("Unable to retrieve match from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } if match == nil { if err := s.applicationAbsentOrError(r, params.Application); err != nil { return nil, err } - return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Match not found", nil) + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Match not found") } return api.SingleResponse[*model.Match]{Data: match}, nil @@ -1221,7 +1221,7 @@ func handleListMatchAdvances(s *Service, r *http.Request, req RPCRequest) (any, var params api.ListMatchAdvancesParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Use default values if not provided @@ -1235,21 +1235,21 @@ func handleListMatchAdvances(s *Service, r *http.Request, req RPCRequest) (any, // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } // Create match advance filter based on params epochIndex, err := config.ToIndexFromString(params.EpochIndex) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err)) } if _, err := config.ToAddressFromString(params.TournamentAddress); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err)) } if _, err := config.ToHashFromString(params.IDHash); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid ID hash: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid ID hash: %v", err)) } pagination := repository.Pagination{ @@ -1260,7 +1260,7 @@ func handleListMatchAdvances(s *Service, r *http.Request, req RPCRequest) (any, params.TournamentAddress, params.IDHash, pagination, params.Descending) if err != nil { s.Logger.Error("Unable to retrieve match advances from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } if len(matchAdvances) == 0 { if err := s.applicationAbsentOrError(r, params.Application); err != nil { @@ -1285,42 +1285,42 @@ func handleGetMatchAdvanced(s *Service, r *http.Request, req RPCRequest) (any, e var params api.GetMatchAdvancedParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) - return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters", nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } // Validate application parameter if err := validateNameOrAddress(params.Application); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) } epochIndex, err := config.ToIndexFromString(params.EpochIndex) if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch index: %v", err)) } if _, err := config.ToAddressFromString(params.TournamentAddress); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid tournament address: %v", err)) } if _, err := config.ToHashFromString(params.IDHash); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid ID hash: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid ID hash: %v", err)) } if _, err := config.ToHashFromString(params.Parent); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid parent hash: %v", err), nil) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid parent hash: %v", err)) } matchAdvanced, err := s.repository.GetMatchAdvanced(r.Context(), params.Application, epochIndex, params.TournamentAddress, params.IDHash, params.Parent[2:]) // TODO: use parsed value if err != nil { s.Logger.Error("Unable to retrieve match advanced from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } if matchAdvanced == nil { if err := s.applicationAbsentOrError(r, params.Application); err != nil { return nil, err } - return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Match advanced not found", nil) + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Match advanced not found") } return api.SingleResponse[*model.MatchAdvanced]{Data: matchAdvanced}, nil @@ -1329,11 +1329,11 @@ func handleGetMatchAdvanced(s *Service, r *http.Request, req RPCRequest) (any, e func handleGetChainID(s *Service, r *http.Request, _ RPCRequest) (any, error) { config, err := repository.LoadNodeConfig[evmreader.PersistentConfig](r.Context(), s.repository, evmreader.EvmReaderConfigKey) if errors.Is(err, repository.ErrNotFound) { - return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "EVM Reader config not found", nil) + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "EVM Reader config not found") } if err != nil { s.Logger.Error("Unable to retrieve evmreader config from repository", "err", err) - return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } return api.SingleResponse[string]{Data: fmt.Sprintf("0x%x", config.Value.ChainID)}, nil @@ -1350,9 +1350,9 @@ func (s *Service) applicationAbsentOrError( app, err := s.repository.GetApplication(r.Context(), validatedNameOrAddress) if err != nil { s.Logger.Error("Unable to retrieve application from repository", "err", err) - return newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error", nil) + return newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") } else if app == nil { - return newRPCError(JSONRPC_APPLICATION_NOT_FOUND, "Application not found", nil) + return newRPCError(JSONRPC_APPLICATION_NOT_FOUND, "Application not found") } return nil } diff --git a/internal/jsonrpc/types.go b/internal/jsonrpc/types.go index 9598bc03d..00aa12f3d 100644 --- a/internal/jsonrpc/types.go +++ b/internal/jsonrpc/types.go @@ -35,30 +35,27 @@ type RPCResponse struct { type RPCError struct { Code int `json:"code"` Message string `json:"message"` - Data any `json:"data,omitempty"` } func (e *RPCError) Error() string { return e.Message } -func newRPCError(code int, message string, data any) error { - return &RPCError{Code: code, Message: message, Data: data} +func newRPCError(code int, message string) error { + return &RPCError{Code: code, Message: message} } // writeRPCError sends a generic error response for internal errors. -func writeRPCError(w io.Writer, id any, code int, message string, data any) error { +func writeRPCError(w io.Writer, id any, code int, message string) error { // Hide detailed error info for internal errors. if code == JSONRPC_INTERNAL_ERROR { message = "Internal server error" - data = nil } resp := RPCResponse{ JSONRPC: "2.0", Error: &RPCError{ Code: code, Message: message, - Data: data, }, ID: id, } From 76dd5e4c1e3ad8f011efd7dfc75c7d60746d14bb Mon Sep 17 00:00:00 2001 From: Renato Maia <1887792+renatomaia@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:38:29 -0300 Subject: [PATCH 03/13] feat(jsonrpc): add operation to get epoch by a virtual contiguous index --- internal/jsonrpc/api/params.go | 6 ++ internal/jsonrpc/jsonrpc-discover.json | 43 ++++++++++ internal/jsonrpc/jsonrpc.go | 33 +++++++ internal/jsonrpc/jsonrpc_test.go | 114 +++++++++++++++++++++++++ internal/jsonrpc/util_test.go | 1 + 5 files changed, 197 insertions(+) diff --git a/internal/jsonrpc/api/params.go b/internal/jsonrpc/api/params.go index ef21488c2..92f31e82d 100644 --- a/internal/jsonrpc/api/params.go +++ b/internal/jsonrpc/api/params.go @@ -30,6 +30,12 @@ type GetEpochParams struct { EpochIndex string `json:"epoch_index"` } +// GetEpochByVirtualIndexParams aligns with the OpenRPC specification +type GetEpochByVirtualIndexParams struct { + Application string `json:"application"` + VirtualIndex string `json:"virtual_index"` +} + // GetLastAcceptedEpochIndexParams with the OpenRPC specification type GetLastAcceptedEpochIndexParams struct { Application string `json:"application"` diff --git a/internal/jsonrpc/jsonrpc-discover.json b/internal/jsonrpc/jsonrpc-discover.json index 63b7ebe4c..b71ee1361 100644 --- a/internal/jsonrpc/jsonrpc-discover.json +++ b/internal/jsonrpc/jsonrpc-discover.json @@ -201,6 +201,49 @@ } ] }, + { + "name": "cartesi_getEpochByVirtualIndex", + "summary": "Get a specific epoch by its virtual index", + "description": "Fetches a single epoch by application and its virtual index, whichis the epoch's dense insertion rank — 0, 1, 2, … with no gaps by construction.", + "params": [ + { + "name": "application", + "description": "The application's name or hex encoded address.", + "schema": { + "$ref": "#/components/schemas/NameOrAddress" + }, + "required": true + }, + { + "name": "virtual_index", + "description": "The virtual index of the epoch to be retrieved (hex encoded).", + "schema": { + "$ref": "#/components/schemas/UnsignedInteger" + }, + "required": true + } + ], + "result": { + "name": "result", + "schema": { + "$ref": "#/components/schemas/EpochGetResult" + } + }, + "errors": [ + { + "$ref": "#/components/errors/InvalidParams" + }, + { + "$ref": "#/components/errors/ApplicationNotFound" + }, + { + "$ref": "#/components/errors/EpochNotFound" + }, + { + "$ref": "#/components/errors/InternalError" + } + ] + }, { "name": "cartesi_getLastAcceptedEpochIndex", "summary": "Get the last accepted epoch index", diff --git a/internal/jsonrpc/jsonrpc.go b/internal/jsonrpc/jsonrpc.go index 4bd3e22f0..56e579ef4 100644 --- a/internal/jsonrpc/jsonrpc.go +++ b/internal/jsonrpc/jsonrpc.go @@ -61,6 +61,7 @@ var jsonrpcHandlers = dispatchTable{ "cartesi_getApplication": handleGetApplication, "cartesi_listEpochs": handleListEpochs, "cartesi_getEpoch": handleGetEpoch, + "cartesi_getEpochByVirtualIndex": handleGetEpochByVirtualIndex, "cartesi_getLastAcceptedEpochIndex": handleGetLastAcceptedEpochIndex, "cartesi_listInputs": handleListInputs, "cartesi_getInput": handleGetInput, @@ -408,6 +409,38 @@ func handleGetEpoch(s *Service, r *http.Request, req RPCRequest) (any, error) { return api.SingleResponse[*model.Epoch]{Data: epoch}, nil } +func handleGetEpochByVirtualIndex(s *Service, r *http.Request, req RPCRequest) (any, error) { + var params api.GetEpochByVirtualIndexParams + if err := UnmarshalParams(req.Params, ¶ms); err != nil { + s.Logger.Debug("Invalid parameters", "err", err) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") + } + + // Validate application parameter + if err := validateNameOrAddress(params.Application); err != nil { + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) + } + + index, err := config.ToIndexFromString(params.VirtualIndex) + if err != nil { + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid virtual index: %v", err)) + } + + epoch, err := s.repository.GetEpochByVirtualIndex(r.Context(), params.Application, index) + if err != nil { + s.Logger.Error("Unable to retrieve epoch from repository", "err", err) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") + } + if epoch == nil { + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err + } + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "Epoch not found") + } + + return api.SingleResponse[*model.Epoch]{Data: epoch}, nil +} + func handleGetLastAcceptedEpochIndex(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetLastAcceptedEpochIndexParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { diff --git a/internal/jsonrpc/jsonrpc_test.go b/internal/jsonrpc/jsonrpc_test.go index d4fd906f3..a45ec34b1 100644 --- a/internal/jsonrpc/jsonrpc_test.go +++ b/internal/jsonrpc/jsonrpc_test.go @@ -328,6 +328,120 @@ func TestMethod(t *testing.T) { }) }) + //////////////////////////////////////////////////////////////////////// + // getEpochByVirtualIndex + //////////////////////////////////////////////////////////////////////// + t.Run("cartesi_getEpochByVirtualIndex", func(t *testing.T) { + method := getName(t.Name()) + + // failure: virtual_index not hex encoded -> invalid param + t.Run("malformedVirtualIndex", func(t *testing.T) { + testHistogram.inc(method) + s := newTestService(t, t.Name()) + + body := s.doRequest(t, 0, fmt.Appendf([]byte{}, `{ + "jsonrpc": "2.0", + "method": "cartesi_getEpochByVirtualIndex", + "params": { + "application": "%v", + "virtual_index": 0 + }, + "id": 0 + }`, numberToName(1))) + + resp := testRPCResponse[any]{} + require.NoError(t, json.Unmarshal(body, &resp)) + require.NotNil(t, resp.Error) + assert.Equal(t, JSONRPC_INVALID_PARAMS, resp.Error.Code) + assert.Equal(t, "Invalid parameters", resp.Error.Message) + }) + + // failure: virtual index not in the database -> resource not found + t.Run("absent", func(t *testing.T) { + testHistogram.inc(method) + s := newTestService(t, t.Name()) + ctx := context.Background() + + app := uint64(1) + appID := s.newTestApplication(ctx, t, app) + s.createTestEpoch(ctx, t, numberToName(app), + repotest.NewEpochBuilder(appID). + WithIndex(5). + WithStatus(model.EpochStatus_ClaimAccepted). + Build()) + + body := s.doRequest(t, 0, fmt.Appendf([]byte{}, `{ + "jsonrpc": "2.0", + "method": "cartesi_getEpochByVirtualIndex", + "params": { + "application": "%v", + "virtual_index": "%v" + }, + "id": 0 + }`, numberToName(app), hexutil.EncodeUint64(1))) + + resp := testRPCResponse[any]{} + require.NoError(t, json.Unmarshal(body, &resp)) + require.NotNil(t, resp.Error) + assert.Equal(t, JSONRPC_RESOURCE_NOT_FOUND, resp.Error.Code) + assert.Equal(t, "Epoch not found", resp.Error.Message) + }) + + // failure: application not in the database -> application not found + t.Run("absentApplication", func(t *testing.T) { + testHistogram.inc(method) + s := newTestService(t, t.Name()) + + body := s.doRequest(t, 0, fmt.Appendf([]byte{}, `{ + "jsonrpc": "2.0", + "method": "cartesi_getEpochByVirtualIndex", + "params": { + "application": "%v", + "virtual_index": "0x0" + }, + "id": 0 + }`, numberToName(0xdeadbeef))) + + resp := testRPCResponse[any]{} + require.NoError(t, json.Unmarshal(body, &resp)) + require.NotNil(t, resp.Error) + assert.Equal(t, JSONRPC_APPLICATION_NOT_FOUND, resp.Error.Code) + assert.Equal(t, "Application not found", resp.Error.Message) + }) + + // success: lookup uses the dense virtual index, not the physical epoch index + t.Run("presentWithDivergentPhysicalIndex", func(t *testing.T) { + testHistogram.inc(method) + s := newTestService(t, t.Name()) + ctx := context.Background() + + app := uint64(1) + appID := s.newTestApplication(ctx, t, app) + s.createTestEpoch(ctx, t, numberToName(app), + repotest.NewEpochBuilder(appID). + WithIndex(5). + WithStatus(model.EpochStatus_ClaimAccepted). + Build()) + + body := s.doRequest(t, 0, fmt.Appendf([]byte{}, `{ + "jsonrpc": "2.0", + "method": "cartesi_getEpochByVirtualIndex", + "params": { + "application": "%v", + "virtual_index": "0x0" + }, + "id": 0 + }`, numberToName(app))) + + resp := testRPCResponse[*model.Epoch]{} + require.NoError(t, json.Unmarshal(body, &resp)) + require.Nil(t, resp.Error) + require.NotNil(t, resp.Result.Data) + assert.Equal(t, uint64(5), resp.Result.Data.Index) + assert.Equal(t, uint64(0), resp.Result.Data.VirtualIndex) + }) + }) + //////////////////////////////////////////////////////////////////////// // getInput //////////////////////////////////////////////////////////////////////// diff --git a/internal/jsonrpc/util_test.go b/internal/jsonrpc/util_test.go index e68660282..aadfbe8f5 100644 --- a/internal/jsonrpc/util_test.go +++ b/internal/jsonrpc/util_test.go @@ -96,6 +96,7 @@ func newTestServiceFull(t *testing.T, name string, maxInflight uint64, corsOrigi repo, err := factory.NewRepositoryFromConnectionString(ctx, dbTestEndpoint) require.NoError(t, err) + t.Cleanup(repo.Close) logLevel, err := config.GetLogLevel() require.NoError(t, err) From 0552681009452a184ff54e84293adcd6e50d3ed6 Mon Sep 17 00:00:00 2001 From: Renato Maia <1887792+renatomaia@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:31:15 -0300 Subject: [PATCH 04/13] feat(jsonrpc): add operation to get Node info like its chain ID, version, and default block --- internal/jsonrpc/api/response.go | 6 +++ internal/jsonrpc/jsonrpc-discover.json | 52 ++++++++++++++++++++++ internal/jsonrpc/jsonrpc.go | 18 ++++++++ internal/jsonrpc/jsonrpc_test.go | 60 ++++++++++++++++++++++++++ 4 files changed, 136 insertions(+) diff --git a/internal/jsonrpc/api/response.go b/internal/jsonrpc/api/response.go index 8f974a8ad..69caa614c 100644 --- a/internal/jsonrpc/api/response.go +++ b/internal/jsonrpc/api/response.go @@ -20,3 +20,9 @@ type ListResponse[T any] struct { type SingleResponse[T any] struct { Data T `json:"data"` } + +type NodeInfo struct { + ChainID string `json:"chain_id"` + Version string `json:"version"` + DefaultBlock string `json:"default_block"` // FINALIZED | SAFE | LATEST | PENDING +} diff --git a/internal/jsonrpc/jsonrpc-discover.json b/internal/jsonrpc/jsonrpc-discover.json index b71ee1361..1e0b548d1 100644 --- a/internal/jsonrpc/jsonrpc-discover.json +++ b/internal/jsonrpc/jsonrpc-discover.json @@ -1365,10 +1365,31 @@ } ] }, + { + "name": "cartesi_getNodeInfo", + "summary": "Get node information", + "description": "Fetches the chain ID, semantic node version, and default blockchain block tag used by the node.", + "params": [], + "result": { + "name": "result", + "schema": { + "$ref": "#/components/schemas/NodeInfoResult" + } + }, + "errors": [ + { + "$ref": "#/components/errors/NodeConfigNotFound" + }, + { + "$ref": "#/components/errors/InternalError" + } + ] + }, { "name": "cartesi_getChainId", "summary": "Get node's chain ID", "description": "Fetches the chain ID that node is operating on.", + "deprecated": true, "params": [], "result": { "name": "result", @@ -1389,6 +1410,7 @@ "name": "cartesi_getNodeVersion", "summary": "Get node version", "description": "Fetches the semantic version of the Cartesi rollups node.", + "deprecated": true, "params": [], "result": { "name": "result", @@ -2156,6 +2178,36 @@ } } }, + "NodeInfo": { + "type": "object", + "properties": { + "chain_id": { + "$ref": "#/components/schemas/UnsignedInteger" + }, + "version": { + "type": "string", + "format": "semver", + "pattern": "^[a-zA-Z0-9_-\\.]+$" + }, + "default_block": { + "type": "string", + "enum": [ + "FINALIZED", + "SAFE", + "LATEST", + "PENDING" + ] + } + } + }, + "NodeInfoResult": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/NodeInfo" + } + } + }, "NodeVersionResult": { "type": "object", "properties": { diff --git a/internal/jsonrpc/jsonrpc.go b/internal/jsonrpc/jsonrpc.go index 56e579ef4..d67813314 100644 --- a/internal/jsonrpc/jsonrpc.go +++ b/internal/jsonrpc/jsonrpc.go @@ -80,6 +80,7 @@ var jsonrpcHandlers = dispatchTable{ "cartesi_getMatch": handleGetMatch, "cartesi_listMatchAdvances": handleListMatchAdvances, "cartesi_getMatchAdvanced": handleGetMatchAdvanced, + "cartesi_getNodeInfo": handleGetNodeInfo, "cartesi_getChainId": handleGetChainID, "cartesi_getNodeVersion": handleGetNodeVersion, } @@ -1359,6 +1360,23 @@ func handleGetMatchAdvanced(s *Service, r *http.Request, req RPCRequest) (any, e return api.SingleResponse[*model.MatchAdvanced]{Data: matchAdvanced}, nil } +func handleGetNodeInfo(s *Service, r *http.Request, _ RPCRequest) (any, error) { + cfg, err := repository.LoadNodeConfig[evmreader.PersistentConfig](r.Context(), s.repository, evmreader.EvmReaderConfigKey) + if errors.Is(err, repository.ErrNotFound) { + return nil, newRPCError(JSONRPC_RESOURCE_NOT_FOUND, "EVM Reader config not found") + } + if err != nil { + s.Logger.Error("Unable to retrieve evmreader config from repository", "err", err) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") + } + + return api.SingleResponse[api.NodeInfo]{Data: api.NodeInfo{ + ChainID: fmt.Sprintf("0x%x", cfg.Value.ChainID), + Version: version.BuildVersion, + DefaultBlock: string(cfg.Value.DefaultBlock), // FINALIZED | SAFE | LATEST | PENDING + }}, nil +} + func handleGetChainID(s *Service, r *http.Request, _ RPCRequest) (any, error) { config, err := repository.LoadNodeConfig[evmreader.PersistentConfig](r.Context(), s.repository, evmreader.EvmReaderConfigKey) if errors.Is(err, repository.ErrNotFound) { diff --git a/internal/jsonrpc/jsonrpc_test.go b/internal/jsonrpc/jsonrpc_test.go index a45ec34b1..f97486273 100644 --- a/internal/jsonrpc/jsonrpc_test.go +++ b/internal/jsonrpc/jsonrpc_test.go @@ -159,6 +159,66 @@ func TestMethod(t *testing.T) { }) }) + //////////////////////////////////////////////////////////////////////// + // getNodeInfo + //////////////////////////////////////////////////////////////////////// + t.Run("cartesi_getNodeInfo", func(t *testing.T) { + method := getName(t.Name()) + + // failure: evm reader not configured -> resource not found + t.Run("absent", func(t *testing.T) { + testHistogram.inc(method) + s := newTestService(t, t.Name()) + + body := s.doRequest(t, 0, []byte(`{ + "jsonrpc": "2.0", + "method": "cartesi_getNodeInfo", + "params": {}, + "id": 0 + }`)) + + resp := testRPCResponse[any]{} + require.NoError(t, json.Unmarshal(body, &resp)) + require.NotNil(t, resp.Error) + assert.Equal(t, JSONRPC_RESOURCE_NOT_FOUND, resp.Error.Code) + assert.Equal(t, "EVM Reader config not found", resp.Error.Message) + }) + + // success: combine persisted node configuration with the build version + t.Run("present", func(t *testing.T) { + testHistogram.inc(method) + ctx := context.Background() + s := newTestService(t, t.Name()) + + chainID := uint64(0xdeadbeef) + defaultBlock := model.DefaultBlock_Safe + err := repository.SaveNodeConfig(ctx, s.repository, + &model.NodeConfig[evmreader.PersistentConfig]{ + Key: evmreader.EvmReaderConfigKey, + Value: evmreader.PersistentConfig{ + ChainID: chainID, + DefaultBlock: defaultBlock, + }, + }, + ) + require.NoError(t, err) + + body := s.doRequest(t, 0, []byte(`{ + "jsonrpc": "2.0", + "method": "cartesi_getNodeInfo", + "params": {}, + "id": 0 + }`)) + + resp := testRPCResponse[api.NodeInfo]{} + require.NoError(t, json.Unmarshal(body, &resp)) + require.Nil(t, resp.Error) + assert.Equal(t, hexutil.EncodeUint64(chainID), resp.Result.Data.ChainID) + assert.Equal(t, version.BuildVersion, resp.Result.Data.Version) + assert.Equal(t, string(defaultBlock), resp.Result.Data.DefaultBlock) + }) + }) + //////////////////////////////////////////////////////////////////////// // getChainId //////////////////////////////////////////////////////////////////////// From 07c1815a7c12a5203534f351e69216c2cf6d47e6 Mon Sep 17 00:00:00 2001 From: Renato Maia <1887792+renatomaia@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:05:06 -0300 Subject: [PATCH 05/13] feat(jsonrpc): add inclusive index ranges to list epochs, inputs, outputs, and reports Changes include: - Added optional from and to JSON-RPC parameters. - Added IndexRange *Range to all four repository filters. - Added index-backed >= from and <= to PostgreSQL predicates shared by COUNT(*) and list queries. - Added shared hex-bound parsing and from <= to validation returning -32602 (invalid params). - Preserved positional-parameter compatibility by appending new fields after existing parameters. - Updated the OpenRPC specification. - Added repository tests confirming range composition with total count, descending order, offset, and limit. - Added no-database tests for reversed and malformed bounds. --- internal/jsonrpc/api/params.go | 8 +++ internal/jsonrpc/jsonrpc-discover.json | 64 +++++++++++++++++++ internal/jsonrpc/jsonrpc.go | 47 ++++++++++++++ internal/jsonrpc/jsonrpc_test.go | 51 +++++++++++++++ internal/repository/postgres/epoch.go | 6 ++ internal/repository/postgres/input.go | 6 ++ internal/repository/postgres/output.go | 6 ++ internal/repository/postgres/report.go | 6 ++ internal/repository/repository.go | 4 ++ .../repository/repotest/epoch_test_cases.go | 25 ++++++++ .../repository/repotest/input_test_cases.go | 24 +++++++ .../repository/repotest/output_test_cases.go | 16 +++++ .../repository/repotest/report_test_cases.go | 16 +++++ 13 files changed, 279 insertions(+) diff --git a/internal/jsonrpc/api/params.go b/internal/jsonrpc/api/params.go index 92f31e82d..031e315e2 100644 --- a/internal/jsonrpc/api/params.go +++ b/internal/jsonrpc/api/params.go @@ -22,6 +22,8 @@ type ListEpochsParams struct { Limit uint64 `json:"limit"` Offset uint64 `json:"offset"` Descending bool `json:"descending,omitempty"` + From *string `json:"from,omitempty"` // inclusive lower bound on the epoch index (hex) + To *string `json:"to,omitempty"` // inclusive upper bound on the epoch index (hex) } // GetEpochParams aligns with the OpenRPC specification @@ -50,6 +52,8 @@ type ListInputsParams struct { Limit uint64 `json:"limit"` Offset uint64 `json:"offset"` Descending bool `json:"descending,omitempty"` + From *string `json:"from,omitempty"` // inclusive lower bound on the input index (hex) + To *string `json:"to,omitempty"` // inclusive upper bound on the input index (hex) } // GetInputParams aligns with the OpenRPC specification @@ -73,6 +77,8 @@ type ListOutputsParams struct { Limit uint64 `json:"limit"` Offset uint64 `json:"offset"` Descending bool `json:"descending,omitempty"` + From *string `json:"from,omitempty"` // inclusive lower bound on the output index (hex) + To *string `json:"to,omitempty"` // inclusive upper bound on the output index (hex) } // GetOutputParams aligns with the OpenRPC specification @@ -89,6 +95,8 @@ type ListReportsParams struct { Limit uint64 `json:"limit"` Offset uint64 `json:"offset"` Descending bool `json:"descending,omitempty"` + From *string `json:"from,omitempty"` // inclusive lower bound on the report index (hex) + To *string `json:"to,omitempty"` // inclusive upper bound on the report index (hex) } // GetReportParams aligns with the OpenRPC specification diff --git a/internal/jsonrpc/jsonrpc-discover.json b/internal/jsonrpc/jsonrpc-discover.json index 1e0b548d1..6dab60503 100644 --- a/internal/jsonrpc/jsonrpc-discover.json +++ b/internal/jsonrpc/jsonrpc-discover.json @@ -137,6 +137,22 @@ "default": false }, "required": false + }, + { + "name": "from", + "description": "Inclusive lower bound on the epoch index (hex encoded).", + "schema": { + "$ref": "#/components/schemas/UnsignedInteger" + }, + "required": false + }, + { + "name": "to", + "description": "Inclusive upper bound on the epoch index (hex encoded).", + "schema": { + "$ref": "#/components/schemas/UnsignedInteger" + }, + "required": false } ], "result": { @@ -344,6 +360,22 @@ "default": false }, "required": false + }, + { + "name": "from", + "description": "Inclusive lower bound on the input index (hex encoded).", + "schema": { + "$ref": "#/components/schemas/UnsignedInteger" + }, + "required": false + }, + { + "name": "to", + "description": "Inclusive upper bound on the input index (hex encoded).", + "schema": { + "$ref": "#/components/schemas/UnsignedInteger" + }, + "required": false } ], "result": { @@ -512,6 +544,22 @@ "default": false }, "required": false + }, + { + "name": "from", + "description": "Inclusive lower bound on the output index (hex encoded).", + "schema": { + "$ref": "#/components/schemas/UnsignedInteger" + }, + "required": false + }, + { + "name": "to", + "description": "Inclusive upper bound on the output index (hex encoded).", + "schema": { + "$ref": "#/components/schemas/UnsignedInteger" + }, + "required": false } ], "result": { @@ -630,6 +678,22 @@ "default": false }, "required": false + }, + { + "name": "from", + "description": "Inclusive lower bound on the report index (hex encoded).", + "schema": { + "$ref": "#/components/schemas/UnsignedInteger" + }, + "required": false + }, + { + "name": "to", + "description": "Inclusive upper bound on the report index (hex encoded).", + "schema": { + "$ref": "#/components/schemas/UnsignedInteger" + }, + "required": false } ], "result": { diff --git a/internal/jsonrpc/jsonrpc.go b/internal/jsonrpc/jsonrpc.go index d67813314..bf72d698b 100644 --- a/internal/jsonrpc/jsonrpc.go +++ b/internal/jsonrpc/jsonrpc.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "io" + "math" "net/http" "github.com/cartesi/rollups-node/internal/config" @@ -342,6 +343,11 @@ func handleListEpochs(s *Service, r *http.Request, req RPCRequest) (any, error) } var epochFilter repository.EpochFilter + indexRange, err := parseIndexRange(params.From, params.To) + if err != nil { + return nil, newRPCError(JSONRPC_INVALID_PARAMS, err.Error()) + } + epochFilter.IndexRange = indexRange if params.Status != nil { var status model.EpochStatus if err := status.Scan(*params.Status); err != nil { @@ -492,6 +498,11 @@ func handleListInputs(s *Service, r *http.Request, req RPCRequest) (any, error) // Create input filter based on params inputFilter := repository.InputFilter{} + indexRange, err := parseIndexRange(params.From, params.To) + if err != nil { + return nil, newRPCError(JSONRPC_INVALID_PARAMS, err.Error()) + } + inputFilter.IndexRange = indexRange if params.EpochIndex != nil { epochIndex, err := config.ToIndexFromString(*params.EpochIndex) if err != nil { @@ -633,6 +644,11 @@ func handleListOutputs(s *Service, r *http.Request, req RPCRequest) (any, error) // Create output filter based on params outputFilter := repository.OutputFilter{} + indexRange, err := parseIndexRange(params.From, params.To) + if err != nil { + return nil, newRPCError(JSONRPC_INVALID_PARAMS, err.Error()) + } + outputFilter.IndexRange = indexRange if params.EpochIndex != nil { epochIndex, err := config.ToIndexFromString(*params.EpochIndex) if err != nil { @@ -761,6 +777,11 @@ func handleListReports(s *Service, r *http.Request, req RPCRequest) (any, error) // Create report filter based on params reportFilter := repository.ReportFilter{} + indexRange, err := parseIndexRange(params.From, params.To) + if err != nil { + return nil, newRPCError(JSONRPC_INVALID_PARAMS, err.Error()) + } + reportFilter.IndexRange = indexRange if params.EpochIndex != nil { epochIndex, err := config.ToIndexFromString(*params.EpochIndex) if err != nil { @@ -1394,6 +1415,32 @@ func handleGetNodeVersion(_ *Service, _ *http.Request, _ RPCRequest) (any, error return api.SingleResponse[string]{Data: version.BuildVersion}, nil } +func parseIndexRange(from, to *string) (*repository.Range, error) { + if from == nil && to == nil { + return nil, nil + } + + indexRange := repository.Range{End: math.MaxUint64} + if from != nil { + value, err := config.ToIndexFromString(*from) + if err != nil { + return nil, fmt.Errorf("invalid from index: %w", err) + } + indexRange.Start = value + } + if to != nil { + value, err := config.ToIndexFromString(*to) + if err != nil { + return nil, fmt.Errorf("invalid to index: %w", err) + } + indexRange.End = value + } + if indexRange.Start > indexRange.End { + return nil, fmt.Errorf("invalid index range: from must be less than or equal to to") + } + return &indexRange, nil +} + func (s *Service) applicationAbsentOrError( r *http.Request, validatedNameOrAddress string, diff --git a/internal/jsonrpc/jsonrpc_test.go b/internal/jsonrpc/jsonrpc_test.go index f97486273..8e6e0478b 100644 --- a/internal/jsonrpc/jsonrpc_test.go +++ b/internal/jsonrpc/jsonrpc_test.go @@ -19,6 +19,8 @@ import ( "context" "encoding/json" "fmt" + "math" + "net/http" "os" "time" @@ -3651,3 +3653,52 @@ func TestMethod(t *testing.T) { t.Errorf("Method coverage issues:\n%s", strings.Join(errors, "\n")) } } + +func TestListIndexRangeValidation(t *testing.T) { + for _, method := range []string{ + "cartesi_listEpochs", + "cartesi_listInputs", + "cartesi_listOutputs", + "cartesi_listReports", + } { + t.Run(method, func(t *testing.T) { + s := newBatchTestService() + body := []byte(fmt.Sprintf(`{ + "jsonrpc":"2.0", + "method":%q, + "params":{"application":"app","from":"0x2","to":"0x1"}, + "id":1 + }`, method)) + rr := serveRPC(t, s, body) + + require.Equal(t, http.StatusOK, rr.Code) + response := decodeRPCResponse(t, rr.Body.Bytes()) + requireRPCError(t, response, float64(1), JSONRPC_INVALID_PARAMS) + require.Equal(t, "invalid index range: from must be less than or equal to to", response.Error.Message) + }) + } +} + +func TestParseIndexRange(t *testing.T) { + from := "0x2" + to := "0x4" + indexRange, err := parseIndexRange(&from, &to) + require.NoError(t, err) + require.Equal(t, repository.Range{Start: 2, End: 4}, *indexRange) + + indexRange, err = parseIndexRange(&from, nil) + require.NoError(t, err) + require.Equal(t, uint64(2), indexRange.Start) + require.Equal(t, uint64(math.MaxUint64), indexRange.End) + + indexRange, err = parseIndexRange(nil, &to) + require.NoError(t, err) + require.Equal(t, uint64(0), indexRange.Start) + require.Equal(t, uint64(4), indexRange.End) + + invalid := "2" + _, err = parseIndexRange(&invalid, nil) + require.EqualError(t, err, "invalid from index: expected hex encoded value") + _, err = parseIndexRange(nil, &invalid) + require.EqualError(t, err, "invalid to index: expected hex encoded value") +} diff --git a/internal/repository/postgres/epoch.go b/internal/repository/postgres/epoch.go index 8930eecc0..5a9b8986e 100644 --- a/internal/repository/postgres/epoch.go +++ b/internal/repository/postgres/epoch.go @@ -842,6 +842,12 @@ func (r *PostgresRepository) ListEpochs( ) conditions := []postgres.BoolExpression{whereClause} + if f.IndexRange != nil { + conditions = append(conditions, + table.Epoch.Index.GT_EQ(uint64Expr(f.IndexRange.Start)), + table.Epoch.Index.LT_EQ(uint64Expr(f.IndexRange.End)), + ) + } if len(f.Status) > 0 { statuses := make([]postgres.Expression, 0, len(f.Status)) for _, status := range f.Status { diff --git a/internal/repository/postgres/input.go b/internal/repository/postgres/input.go index 5d944d4c2..4056f4b6d 100644 --- a/internal/repository/postgres/input.go +++ b/internal/repository/postgres/input.go @@ -223,6 +223,12 @@ func (r *PostgresRepository) ListInputs( ) conditions := []postgres.BoolExpression{whereClause} + if f.IndexRange != nil { + conditions = append(conditions, + table.Input.Index.GT_EQ(uint64Expr(f.IndexRange.Start)), + table.Input.Index.LT_EQ(uint64Expr(f.IndexRange.End)), + ) + } if f.EpochIndex != nil { conditions = append(conditions, table.Input.EpochIndex.EQ(uint64Expr(*f.EpochIndex))) } diff --git a/internal/repository/postgres/output.go b/internal/repository/postgres/output.go index b18f54b28..6bcf3123e 100644 --- a/internal/repository/postgres/output.go +++ b/internal/repository/postgres/output.go @@ -169,6 +169,12 @@ func (r *PostgresRepository) ListOutputs( ) conditions := []postgres.BoolExpression{whereClause} + if f.IndexRange != nil { + conditions = append(conditions, + table.Output.Index.GT_EQ(uint64Expr(f.IndexRange.Start)), + table.Output.Index.LT_EQ(uint64Expr(f.IndexRange.End)), + ) + } if f.BlockRange != nil { conditions = append(conditions, table.Input.BlockNumber.BETWEEN( uint64Expr(f.BlockRange.Start), diff --git a/internal/repository/postgres/report.go b/internal/repository/postgres/report.go index d653ad9a1..b011e57ff 100644 --- a/internal/repository/postgres/report.go +++ b/internal/repository/postgres/report.go @@ -90,6 +90,12 @@ func (r *PostgresRepository) ListReports( ) conditions := []postgres.BoolExpression{whereClause} + if f.IndexRange != nil { + conditions = append(conditions, + table.Report.Index.GT_EQ(uint64Expr(f.IndexRange.Start)), + table.Report.Index.LT_EQ(uint64Expr(f.IndexRange.End)), + ) + } if f.InputIndex != nil { conditions = append(conditions, table.Report.InputIndex.EQ(uint64Expr(*f.InputIndex))) } diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 44f0d6115..df104ae81 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -62,6 +62,7 @@ func ExecutableApplicationsFilter() ApplicationFilter { type EpochFilter struct { Status []EpochStatus BeforeBlock *uint64 + IndexRange *Range } type InputFilter struct { @@ -70,6 +71,7 @@ type InputFilter struct { NotStatus *InputCompletionStatus Sender *common.Address TransactionHash *common.Hash + IndexRange *Range } type Range struct { @@ -81,6 +83,7 @@ type OutputFilter struct { EpochIndex *uint64 InputIndex *uint64 BlockRange *Range + IndexRange *Range OutputType *[]byte VoucherAddress *common.Address } @@ -88,6 +91,7 @@ type OutputFilter struct { type ReportFilter struct { EpochIndex *uint64 InputIndex *uint64 + IndexRange *Range } type StateHashFilter struct { diff --git a/internal/repository/repotest/epoch_test_cases.go b/internal/repository/repotest/epoch_test_cases.go index 3128fc4f4..b00d69fe8 100644 --- a/internal/repository/repotest/epoch_test_cases.go +++ b/internal/repository/repotest/epoch_test_cases.go @@ -371,6 +371,31 @@ func (s *EpochSuite) TestListEpochs() { s.Equal(uint64(5), total) }) + s.Run("IndexRangeComposesWithPaginationAndDescending", func() { + app := NewApplicationBuilder().Create(s.Ctx, s.T(), s.Repo) + epochInputMap := make(map[*Epoch][]*Input) + for i := range uint64(5) { + epoch := NewEpochBuilder(app.ID). + WithIndex(i).WithStatus(EpochStatus_Closed). + WithBlocks(i*10, i*10+9).WithInputBounds(i, i).Build() + input := NewInputBuilder().WithIndex(i).WithEpochIndex(i).WithBlockNumber(i*10 + 5).Build() + epochInputMap[epoch] = []*Input{input} + } + err := s.Repo.CreateEpochsAndInputs( + s.Ctx, app.IApplicationAddress.String(), epochInputMap, 50) + s.Require().NoError(err) + + indexRange := repository.Range{Start: 1, End: 3} + epochs, total, err := s.Repo.ListEpochs( + s.Ctx, app.IApplicationAddress.String(), + repository.EpochFilter{IndexRange: &indexRange}, + repository.Pagination{Limit: 1, Offset: 1}, true) + s.Require().NoError(err) + s.Require().Len(epochs, 1) + s.Equal(uint64(3), total) + s.Equal(uint64(2), epochs[0].Index) + }) + s.Run("Descending", func() { app := NewApplicationBuilder().Create(s.Ctx, s.T(), s.Repo) diff --git a/internal/repository/repotest/input_test_cases.go b/internal/repository/repotest/input_test_cases.go index 5e725d68e..ea1114a51 100644 --- a/internal/repository/repotest/input_test_cases.go +++ b/internal/repository/repotest/input_test_cases.go @@ -251,6 +251,30 @@ func (s *InputSuite) TestListInputs() { s.Equal(uint64(3), total) }) + s.Run("IndexRangeComposesWithPaginationAndDescending", func() { + app := NewApplicationBuilder().Create(s.Ctx, s.T(), s.Repo) + epoch := NewEpochBuilder(app.ID). + WithIndex(0).WithStatus(EpochStatus_Closed). + WithBlocks(0, 49).WithInputBounds(0, 4).Build() + inputs := make([]*Input, 5) + for i := range uint64(5) { + inputs[i] = NewInputBuilder().WithIndex(i).WithBlockNumber(i*10 + 5).Build() + } + err := s.Repo.CreateEpochsAndInputs( + s.Ctx, app.IApplicationAddress.String(), map[*Epoch][]*Input{epoch: inputs}, 50) + s.Require().NoError(err) + + indexRange := repository.Range{Start: 1, End: 3} + got, total, err := s.Repo.ListInputs( + s.Ctx, app.IApplicationAddress.String(), + repository.InputFilter{IndexRange: &indexRange}, + repository.Pagination{Limit: 1, Offset: 1}, true) + s.Require().NoError(err) + s.Require().Len(got, 1) + s.Equal(uint64(3), total) + s.Equal(uint64(2), got[0].Index) + }) + s.Run("FilterByEpochIndex", func() { app := NewApplicationBuilder().Create(s.Ctx, s.T(), s.Repo) diff --git a/internal/repository/repotest/output_test_cases.go b/internal/repository/repotest/output_test_cases.go index 49eae9f38..2e0ebdc6d 100644 --- a/internal/repository/repotest/output_test_cases.go +++ b/internal/repository/repotest/output_test_cases.go @@ -62,6 +62,22 @@ func (s *OutputSuite) TestListOutputs() { s.Equal(uint64(3), total) }) + s.Run("IndexRangeComposesWithPaginationAndDescending", func() { + seed := Seed(s.Ctx, s.T(), s.Repo) + s.storeAdvanceResult(seed.App.ID, 0, 0, + [][]byte{[]byte("o0"), []byte("o1"), []byte("o2"), []byte("o3"), []byte("o4")}, nil) + + indexRange := repository.Range{Start: 1, End: 3} + outputs, total, err := s.Repo.ListOutputs( + s.Ctx, seed.App.IApplicationAddress.String(), + repository.OutputFilter{IndexRange: &indexRange}, + repository.Pagination{Limit: 1, Offset: 1}, true) + s.Require().NoError(err) + s.Require().Len(outputs, 1) + s.Equal(uint64(3), total) + s.Equal(uint64(2), outputs[0].Index) + }) + s.Run("FilterByEpochIndex", func() { seed := Seed(s.Ctx, s.T(), s.Repo) diff --git a/internal/repository/repotest/report_test_cases.go b/internal/repository/repotest/report_test_cases.go index 8d5c74571..2791bef91 100644 --- a/internal/repository/repotest/report_test_cases.go +++ b/internal/repository/repotest/report_test_cases.go @@ -62,6 +62,22 @@ func (s *ReportSuite) TestListReports() { s.Equal(uint64(3), total) }) + s.Run("IndexRangeComposesWithPaginationAndDescending", func() { + seed := Seed(s.Ctx, s.T(), s.Repo) + s.storeAdvanceResult(seed.App.ID, 0, 0, nil, + [][]byte{[]byte("r0"), []byte("r1"), []byte("r2"), []byte("r3"), []byte("r4")}) + + indexRange := repository.Range{Start: 1, End: 3} + reports, total, err := s.Repo.ListReports( + s.Ctx, seed.App.IApplicationAddress.String(), + repository.ReportFilter{IndexRange: &indexRange}, + repository.Pagination{Limit: 1, Offset: 1}, true) + s.Require().NoError(err) + s.Require().Len(reports, 1) + s.Equal(uint64(3), total) + s.Equal(uint64(2), reports[0].Index) + }) + s.Run("FilterByEpochIndex", func() { seed := Seed(s.Ctx, s.T(), s.Repo) From 393464b250b68a1858e66acde4b0e693adf48f6a Mon Sep 17 00:00:00 2001 From: Renato Maia <1887792+renatomaia@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:08:56 -0300 Subject: [PATCH 06/13] feat(jsonrpc): allows to filter output by execution and multiple selectors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: - Added optional executed *bool to ListOutputsParams. - output_type now accepts either one selector string or a non-empty selector array. - Empty arrays return -32602 (invalid paramters). - Changed OutputFilter.OutputType to a selector slice and added Executed. - PostgreSQL now uses IN for selector OR semantics. - Extracted shared execution and selector predicates reused by GetNumberOfPendingExecutableOutputs. - Updated OpenRPC with string-or-array schema and executed. - Added tests for: - Single-selector compatibility - Selector arrays - Empty-array rejection - Executed tri-state decoding - Combined selector/execution filtering - Unchanged nil-filter behavior required by validator claim generation Targeted tests, package compilation, OpenRPC validation, and git diff --check pass. Validator compilation remains blocked by the environment’s missing Cartesi machine C header. --- .../root/read/outputs/outputs.go | 2 +- .../root/read/service/jsonrpc.go | 6 +- .../root/read/service/repository.go | 10 ++- internal/jsonrpc/api/params.go | 48 ++++++++++++--- internal/jsonrpc/api/params_test.go | 56 +++++++++++++++++ internal/jsonrpc/batchcalls_test.go | 15 +++++ internal/jsonrpc/jsonrpc-discover.json | 23 ++++++- internal/jsonrpc/jsonrpc.go | 16 +++-- internal/repository/postgres/output.go | 33 +++++++--- internal/repository/repository.go | 3 +- .../repository/repotest/output_test_cases.go | 61 ++++++++++++++++++- 11 files changed, 241 insertions(+), 32 deletions(-) create mode 100644 internal/jsonrpc/api/params_test.go diff --git a/cmd/cartesi-rollups-cli/root/read/outputs/outputs.go b/cmd/cartesi-rollups-cli/root/read/outputs/outputs.go index d584689ea..cdb5722df 100644 --- a/cmd/cartesi-rollups-cli/root/read/outputs/outputs.go +++ b/cmd/cartesi-rollups-cli/root/read/outputs/outputs.go @@ -128,7 +128,7 @@ func run(cmd *cobra.Command, args []string) { // Add output type filter if provided if cmd.Flags().Changed("output-type") { - params.OutputType = &outputType + params.OutputType = &api.OutputTypeSelectors{outputType} } // Add voucher address filter if provided diff --git a/cmd/cartesi-rollups-cli/root/read/service/jsonrpc.go b/cmd/cartesi-rollups-cli/root/read/service/jsonrpc.go index b6bbb9efb..3d7c41914 100644 --- a/cmd/cartesi-rollups-cli/root/read/service/jsonrpc.go +++ b/cmd/cartesi-rollups-cli/root/read/service/jsonrpc.go @@ -130,8 +130,10 @@ func (s *JsonrpcReadService) ListOutputs(ctx context.Context, params api.ListOut } // Add output type filter if provided if params.OutputType != nil { - if _, err := api.ParseOutputType(*params.OutputType); err != nil { - return nil, fmt.Errorf("invalid output type: %w", err) + for i, selector := range *params.OutputType { + if _, err := api.ParseOutputType(selector); err != nil { + return nil, fmt.Errorf("invalid output type #%d: %w", i+1, err) + } } } // Add voucher address filter if provided diff --git a/cmd/cartesi-rollups-cli/root/read/service/repository.go b/cmd/cartesi-rollups-cli/root/read/service/repository.go index 89cb1399b..a1f16f353 100644 --- a/cmd/cartesi-rollups-cli/root/read/service/repository.go +++ b/cmd/cartesi-rollups-cli/root/read/service/repository.go @@ -289,9 +289,13 @@ func (s *RepositoryReadService) ListOutputs(ctx context.Context, params api.List } // Add output type filter if provided if params.OutputType != nil { - outputTypeVal, err := api.ParseOutputType(*params.OutputType) - if err != nil { - return nil, fmt.Errorf("invalid output type: %w", err) + outputTypeVal := make([][]byte, len(*params.OutputType)) + for i, selector := range *params.OutputType { + parsed, err := api.ParseOutputType(selector) + if err != nil { + return nil, fmt.Errorf("invalid output type #%d: %w", i+1, err) + } + outputTypeVal[i] = parsed } filter.OutputType = &outputTypeVal } diff --git a/internal/jsonrpc/api/params.go b/internal/jsonrpc/api/params.go index 031e315e2..fafbda9a1 100644 --- a/internal/jsonrpc/api/params.go +++ b/internal/jsonrpc/api/params.go @@ -3,6 +3,33 @@ package api +import ( + "bytes" + "encoding/json" + "fmt" +) + +type OutputTypeSelectors []string + +func (s *OutputTypeSelectors) UnmarshalJSON(data []byte) error { + data = bytes.TrimSpace(data) + if len(data) > 0 && data[0] == '"' { + var value string + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *s = []string{value} + return nil + } + + var values []string + if err := json.Unmarshal(data, &values); err != nil { + return fmt.Errorf("expected a string or an array of strings: %w", err) + } + *s = values + return nil +} + // ListApplicationsParams aligns with the OpenRPC specification type ListApplicationsParams struct { Limit uint64 `json:"limit"` @@ -69,16 +96,17 @@ type GetProcessedInputCountParams struct { // ListOutputsParams aligns with the OpenRPC specification type ListOutputsParams struct { - Application string `json:"application"` - EpochIndex *string `json:"epoch_index,omitempty"` - InputIndex *string `json:"input_index,omitempty"` - OutputType *string `json:"output_type,omitempty"` - VoucherAddress *string `json:"voucher_address,omitempty"` - Limit uint64 `json:"limit"` - Offset uint64 `json:"offset"` - Descending bool `json:"descending,omitempty"` - From *string `json:"from,omitempty"` // inclusive lower bound on the output index (hex) - To *string `json:"to,omitempty"` // inclusive upper bound on the output index (hex) + Application string `json:"application"` + EpochIndex *string `json:"epoch_index,omitempty"` + InputIndex *string `json:"input_index,omitempty"` + OutputType *OutputTypeSelectors `json:"output_type,omitempty"` + VoucherAddress *string `json:"voucher_address,omitempty"` + Limit uint64 `json:"limit"` + Offset uint64 `json:"offset"` + Descending bool `json:"descending,omitempty"` + From *string `json:"from,omitempty"` // inclusive lower bound on the output index (hex) + To *string `json:"to,omitempty"` // inclusive upper bound on the output index (hex) + Executed *bool `json:"executed,omitempty"` } // GetOutputParams aligns with the OpenRPC specification diff --git a/internal/jsonrpc/api/params_test.go b/internal/jsonrpc/api/params_test.go new file mode 100644 index 000000000..9c4fbdca7 --- /dev/null +++ b/internal/jsonrpc/api/params_test.go @@ -0,0 +1,56 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package api + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestListOutputsParamsOutputTypeSelectors(t *testing.T) { + tests := map[string]struct { + input string + expected OutputTypeSelectors + }{ + "single selector": { + input: `{"output_type":"0x237a816f"}`, + expected: OutputTypeSelectors{"0x237a816f"}, + }, + "selector list": { + input: `{"output_type":["0x237a816f","0x10321e8b"]}`, + expected: OutputTypeSelectors{"0x237a816f", "0x10321e8b"}, + }, + "empty list": { + input: `{"output_type":[]}`, + expected: OutputTypeSelectors{}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + var params ListOutputsParams + require.NoError(t, json.Unmarshal([]byte(test.input), ¶ms)) + require.NotNil(t, params.OutputType) + require.Equal(t, test.expected, *params.OutputType) + }) + } +} + +func TestListOutputsParamsExecutedIsOptional(t *testing.T) { + var omitted ListOutputsParams + require.NoError(t, json.Unmarshal([]byte(`{}`), &omitted)) + require.Nil(t, omitted.Executed) + + var executed ListOutputsParams + require.NoError(t, json.Unmarshal([]byte(`{"executed":true}`), &executed)) + require.NotNil(t, executed.Executed) + require.True(t, *executed.Executed) + + var pending ListOutputsParams + require.NoError(t, json.Unmarshal([]byte(`{"executed":false}`), &pending)) + require.NotNil(t, pending.Executed) + require.False(t, *pending.Executed) +} diff --git a/internal/jsonrpc/batchcalls_test.go b/internal/jsonrpc/batchcalls_test.go index d937c9581..982ed12db 100644 --- a/internal/jsonrpc/batchcalls_test.go +++ b/internal/jsonrpc/batchcalls_test.go @@ -66,6 +66,21 @@ func requireRPCError(t *testing.T, response RPCResponse, id any, code int) { require.Equal(t, code, response.Error.Code) } +func TestListOutputsRejectsEmptyOutputTypeList(t *testing.T) { + s := newBatchTestService() + rr := serveRPC(t, s, []byte(`{ + "jsonrpc":"2.0", + "method":"cartesi_listOutputs", + "params":{"application":"app","output_type":[]}, + "id":1 + }`)) + + require.Equal(t, http.StatusOK, rr.Code) + response := decodeRPCResponse(t, rr.Body.Bytes()) + requireRPCError(t, response, float64(1), JSONRPC_INVALID_PARAMS) + require.Equal(t, "Invalid output type: expected at least one selector", response.Error.Message) +} + func TestJSONRPCBatchRejectsEmptyBatchWithSingleObject(t *testing.T) { s := newBatchTestService() rr := serveRPC(t, s, []byte(`[]`)) diff --git a/internal/jsonrpc/jsonrpc-discover.json b/internal/jsonrpc/jsonrpc-discover.json index 6dab60503..6e41bbbbf 100644 --- a/internal/jsonrpc/jsonrpc-discover.json +++ b/internal/jsonrpc/jsonrpc-discover.json @@ -502,9 +502,20 @@ }, { "name": "output_type", - "description": "Filter outputs by output type (first 4 bytes of raw data hex encoded).", + "description": "Filter outputs by one or more output type selectors (the first 4 bytes of raw data, hex encoded). A single selector string is accepted for compatibility; arrays use OR semantics and must not be empty.", "schema": { - "$ref": "#/components/schemas/FunctionSelector" + "oneOf": [ + { + "$ref": "#/components/schemas/FunctionSelector" + }, + { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/FunctionSelector" + } + } + ] }, "required": false }, @@ -560,6 +571,14 @@ "$ref": "#/components/schemas/UnsignedInteger" }, "required": false + }, + { + "name": "executed", + "description": "Filter by execution status: true selects outputs with an execution transaction hash; false selects outputs without one.", + "schema": { + "type": "boolean" + }, + "required": false } ], "result": { diff --git a/internal/jsonrpc/jsonrpc.go b/internal/jsonrpc/jsonrpc.go index bf72d698b..cd69f7bfd 100644 --- a/internal/jsonrpc/jsonrpc.go +++ b/internal/jsonrpc/jsonrpc.go @@ -667,12 +667,20 @@ func handleListOutputs(s *Service, r *http.Request, req RPCRequest) (any, error) // Add output type filter if provided if params.OutputType != nil { - outputType, err := api.ParseOutputType(*params.OutputType) - if err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid output type: %v", err)) + if len(*params.OutputType) == 0 { + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid output type: expected at least one selector") + } + outputTypes := make([][]byte, 0, len(*params.OutputType)) + for _, selector := range *params.OutputType { + outputType, err := api.ParseOutputType(selector) + if err != nil { + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid output type: %v", err)) + } + outputTypes = append(outputTypes, outputType) } - outputFilter.OutputType = &outputType + outputFilter.OutputType = &outputTypes } + outputFilter.Executed = params.Executed // Add sender filter if provided if params.VoucherAddress != nil { diff --git a/internal/repository/postgres/output.go b/internal/repository/postgres/output.go index 6bcf3123e..688835b7e 100644 --- a/internal/repository/postgres/output.go +++ b/internal/repository/postgres/output.go @@ -21,6 +21,21 @@ var ( voucherSelector = []byte{0x23, 0x7a, 0x81, 0x6f} ) +func outputExecutionCondition(executed bool) postgres.BoolExpression { + if executed { + return table.Output.ExecutionTransactionHash.IS_NOT_NULL() + } + return table.Output.ExecutionTransactionHash.IS_NULL() +} + +func outputTypesCondition(selectors [][]byte) postgres.BoolExpression { + values := make([]postgres.Expression, 0, len(selectors)) + for _, selector := range selectors { + values = append(values, postgres.Bytea(selector)) + } + return SubstrBytea(table.Output.RawData, 1, 4).IN(values...) +} + func (r *PostgresRepository) GetOutput( ctx context.Context, nameOrAddress string, @@ -193,9 +208,11 @@ func (r *PostgresRepository) ListOutputs( } if f.OutputType != nil { - conditions = append(conditions, - SubstrBytea(table.Output.RawData, 1, 4).EQ(postgres.Bytea(*f.OutputType)), - ) + conditions = append(conditions, outputTypesCondition(*f.OutputType)) + } + + if f.Executed != nil { + conditions = append(conditions, outputExecutionCondition(*f.Executed)) } if f.VoucherAddress != nil { @@ -325,8 +342,6 @@ func (r *PostgresRepository) GetNumberOfPendingExecutableOutputs( ) (uint64, error) { whereClause := getWhereClauseFromNameOrAddress(nameOrAddress) - outputType := SubstrBytea(table.Output.RawData, 1, 4) - sel := table.Output. SELECT(postgres.COUNT(postgres.STAR)). FROM( @@ -337,9 +352,11 @@ func (r *PostgresRepository) GetNumberOfPendingExecutableOutputs( ). WHERE( whereClause. - AND(table.Output.ExecutionTransactionHash.IS_NULL()). - AND(outputType.EQ(postgres.Bytea(delegateCallVoucherSelector)). - OR(outputType.EQ(postgres.Bytea(voucherSelector)))), + AND(outputExecutionCondition(false)). + AND(outputTypesCondition([][]byte{ + delegateCallVoucherSelector, + voucherSelector, + })), ) sqlStr, args := sel.Sql() diff --git a/internal/repository/repository.go b/internal/repository/repository.go index df104ae81..b60c2c92c 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -84,7 +84,8 @@ type OutputFilter struct { InputIndex *uint64 BlockRange *Range IndexRange *Range - OutputType *[]byte + OutputType *[][]byte + Executed *bool VoucherAddress *common.Address } diff --git a/internal/repository/repotest/output_test_cases.go b/internal/repository/repotest/output_test_cases.go index 2e0ebdc6d..2d79edb4f 100644 --- a/internal/repository/repotest/output_test_cases.go +++ b/internal/repository/repotest/output_test_cases.go @@ -247,9 +247,10 @@ func (s *OutputSuite) TestListOutputs() { s.storeAdvanceResult(seed.App.ID, 0, 0, [][]byte{rawWithType, rawWithOther}, nil) + targetTypes := [][]byte{targetType} outputs, total, err := s.Repo.ListOutputs( s.Ctx, seed.App.IApplicationAddress.String(), - repository.OutputFilter{OutputType: &targetType}, + repository.OutputFilter{OutputType: &targetTypes}, repository.Pagination{Limit: 10}, false) s.Require().NoError(err) s.Len(outputs, 1) @@ -257,6 +258,64 @@ func (s *OutputSuite) TestListOutputs() { s.Equal(rawWithType, outputs[0].RawData) }) + s.Run("FilterByOutputTypesAndExecutionStatus", func() { + seed := Seed(s.Ctx, s.T(), s.Repo) + + voucherSelector := []byte{0x23, 0x7a, 0x81, 0x6f} + delegateCallVoucherSelector := []byte{0x10, 0x32, 0x1e, 0x8b} + voucher := append([]byte{}, voucherSelector...) + delegateCallVoucher := append([]byte{}, delegateCallVoucherSelector...) + notice := []byte{0xc2, 0x58, 0xd6, 0xe5} + executedVoucher := append([]byte{}, voucherSelector...) + s.storeAdvanceResult(seed.App.ID, 0, 0, + [][]byte{voucher, delegateCallVoucher, notice, executedVoucher}, nil) + + txHash := UniqueHash() + err := s.Repo.UpdateOutputsExecution( + s.Ctx, + seed.App.IApplicationAddress.String(), + []*Output{{ + InputEpochApplicationID: seed.App.ID, + Index: 3, + ExecutionTransactionHash: &txHash, + }}, + 200, + ) + s.Require().NoError(err) + + outputTypes := [][]byte{voucherSelector, delegateCallVoucherSelector} + executed := false + outputs, total, err := s.Repo.ListOutputs( + s.Ctx, seed.App.IApplicationAddress.String(), + repository.OutputFilter{OutputType: &outputTypes, Executed: &executed}, + repository.Pagination{Limit: 10}, false) + s.Require().NoError(err) + s.Require().Len(outputs, 2) + s.Equal(uint64(2), total) + s.Equal(uint64(0), outputs[0].Index) + s.Equal(uint64(1), outputs[1].Index) + + executed = true + outputs, total, err = s.Repo.ListOutputs( + s.Ctx, seed.App.IApplicationAddress.String(), + repository.OutputFilter{OutputType: &outputTypes, Executed: &executed}, + repository.Pagination{Limit: 10}, false) + s.Require().NoError(err) + s.Require().Len(outputs, 1) + s.Equal(uint64(1), total) + s.Equal(uint64(3), outputs[0].Index) + + // The validator uses the nil-filter path to reproduce epoch claims; + // it must continue to include every output type and execution state. + outputs, total, err = s.Repo.ListOutputs( + s.Ctx, seed.App.IApplicationAddress.String(), + repository.OutputFilter{}, + repository.Pagination{}, false) + s.Require().NoError(err) + s.Len(outputs, 4) + s.Equal(uint64(4), total) + }) + s.Run("FilterByVoucherAddress", func() { seed := Seed(s.Ctx, s.T(), s.Repo) From d4bad4f38c76a75602838c0be006d9f45ed8edaa Mon Sep 17 00:00:00 2001 From: Renato Maia <1887792+renatomaia@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:15:44 -0300 Subject: [PATCH 07/13] feat(cli): allows to filter output by execution and multiple selectors - Added flag '--executed' to `read outputs` - '--executed' filters for executed outputs. - '--executed=false' filters for unexecuted outputs. - Omitting the flag leaves execution status unfiltered. - Updated the CLI example. - Flag '--output-type' now can be repeated multiple times. - All selectors are forwarded in request order. - The help example and flag description were updated. --- .../root/read/outputs/outputs.go | 21 +++++++++++++------ .../root/read/service/repository.go | 1 + 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/cmd/cartesi-rollups-cli/root/read/outputs/outputs.go b/cmd/cartesi-rollups-cli/root/read/outputs/outputs.go index cdb5722df..98feffa04 100644 --- a/cmd/cartesi-rollups-cli/root/read/outputs/outputs.go +++ b/cmd/cartesi-rollups-cli/root/read/outputs/outputs.go @@ -39,8 +39,8 @@ cartesi-rollups-cli read outputs echo-dapp 10 # Read all outputs: cartesi-rollups-cli read outputs echo-dapp -# Read all outputs with filter: -cartesi-rollups-cli read outputs echo-dapp --epoch-index 10 --input-index 10 --output-type 0x237a816f --voucher-address 0x95eac57f9d67c5e0f255d5a19eb5d3fd00cafa73 +# Read all outputs with filters: +cartesi-rollups-cli read outputs echo-dapp --epoch-index 10 --input-index 10 --output-type 0x237a816f --output-type 0x10321e8b --executed --voucher-address 0x95eac57f9d67c5e0f255d5a19eb5d3fd00cafa73 # Read all outputs with pagination: cartesi-rollups-cli read outputs echo-dapp --limit 10 --offset 10 --descending @@ -49,7 +49,8 @@ cartesi-rollups-cli read outputs echo-dapp --limit 10 --offset 10 --descending var ( epochIndex string inputIndex string - outputType string + outputTypes []string + executed bool voucherAddress string limit uint64 offset uint64 @@ -61,8 +62,10 @@ func init() { "Filter outputs by epoch index (decimal or hex encoded)") Cmd.Flags().StringVar(&inputIndex, "input-index", "", "Filter outputs by input index (decimal or hex encoded)") - Cmd.Flags().StringVar(&outputType, "output-type", "", - "Filter outputs by output type (first 4 bytes of raw data hex encoded)") + Cmd.Flags().StringArrayVar(&outputTypes, "output-type", nil, + "Filter outputs by output type (first 4 bytes of raw data hex encoded); may be specified multiple times") + Cmd.Flags().BoolVar(&executed, "executed", false, + "Filter outputs by execution status") Cmd.Flags().StringVar(&voucherAddress, "voucher-address", "", "Filter outputs by voucher address (hex encoded)") Cmd.Flags().Uint64Var(&limit, "limit", 50, //nolint: mnd @@ -128,7 +131,13 @@ func run(cmd *cobra.Command, args []string) { // Add output type filter if provided if cmd.Flags().Changed("output-type") { - params.OutputType = &api.OutputTypeSelectors{outputType} + selectors := api.OutputTypeSelectors(outputTypes) + params.OutputType = &selectors + } + + // Add execution status filter if provided + if cmd.Flags().Changed("executed") { + params.Executed = &executed } // Add voucher address filter if provided diff --git a/cmd/cartesi-rollups-cli/root/read/service/repository.go b/cmd/cartesi-rollups-cli/root/read/service/repository.go index a1f16f353..234e999e6 100644 --- a/cmd/cartesi-rollups-cli/root/read/service/repository.go +++ b/cmd/cartesi-rollups-cli/root/read/service/repository.go @@ -307,6 +307,7 @@ func (s *RepositoryReadService) ListOutputs(ctx context.Context, params api.List } filter.VoucherAddress = &voucherAddressVal } + filter.Executed = params.Executed pagination.Limit = params.Limit pagination.Offset = params.Offset From 12786d03c9c93cf81feb2b2fdeb0f53e43515fa9 Mon Sep 17 00:00:00 2001 From: Renato Maia <1887792+renatomaia@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:29:18 -0300 Subject: [PATCH 08/13] feat(jsonrpc): add methods to get the count of executed and pending outputs Methods added: - cartesi_getExecutedOutputCount - cartesi_getPendingExecutableOutputCount Each method: - Accepts an application parameter. - Returns {"data":"0x..."}. - Explicitly checks application existence before querying the aggregate. - Returns -32002 (application not found) for unknown applications. - Is registered in the dispatch table. Also updated the OpenRPC specification with the requested descriptions and added tests distinguishing unknown applications from existing applications with zero outputs. --- internal/jsonrpc/jsonrpc-discover.json | 64 ++++++++++++ internal/jsonrpc/jsonrpc.go | 102 ++++++++++++++----- internal/jsonrpc/jsonrpc_test.go | 131 ++++++++++++++++++++++++- 3 files changed, 269 insertions(+), 28 deletions(-) diff --git a/internal/jsonrpc/jsonrpc-discover.json b/internal/jsonrpc/jsonrpc-discover.json index 6e41bbbbf..fd431b72c 100644 --- a/internal/jsonrpc/jsonrpc-discover.json +++ b/internal/jsonrpc/jsonrpc-discover.json @@ -471,6 +471,70 @@ } ] }, + { + "name": "cartesi_getExecutedOutputCount", + "summary": "Retrieve the number of executed outputs for the application", + "description": "Returns a monotone change signal: an unchanged value means no new executions; not a resume cursor.", + "params": [ + { + "name": "application", + "description": "The application's name or hex encoded address.", + "schema": { + "$ref": "#/components/schemas/NameOrAddress" + }, + "required": true + } + ], + "result": { + "name": "result", + "schema": { + "$ref": "#/components/schemas/ProcessedInputCountResult" + } + }, + "errors": [ + { + "$ref": "#/components/errors/InvalidParams" + }, + { + "$ref": "#/components/errors/ApplicationNotFound" + }, + { + "$ref": "#/components/errors/InternalError" + } + ] + }, + { + "name": "cartesi_getPendingExecutableOutputCount", + "summary": "Retrieve the number of pending executable outputs for the application", + "description": "Returns a non-monotone gauge (grows with new vouchers, shrinks with executions): do not use for change detection — poll the executed count instead.", + "params": [ + { + "name": "application", + "description": "The application's name or hex encoded address.", + "schema": { + "$ref": "#/components/schemas/NameOrAddress" + }, + "required": true + } + ], + "result": { + "name": "result", + "schema": { + "$ref": "#/components/schemas/ProcessedInputCountResult" + } + }, + "errors": [ + { + "$ref": "#/components/errors/InvalidParams" + }, + { + "$ref": "#/components/errors/ApplicationNotFound" + }, + { + "$ref": "#/components/errors/InternalError" + } + ] + }, { "name": "cartesi_listOutputs", "summary": "Retrieve a List of Outputs", diff --git a/internal/jsonrpc/jsonrpc.go b/internal/jsonrpc/jsonrpc.go index cd69f7bfd..e116cf0a8 100644 --- a/internal/jsonrpc/jsonrpc.go +++ b/internal/jsonrpc/jsonrpc.go @@ -57,33 +57,35 @@ type rpcHandler = func(*Service, *http.Request, RPCRequest) (any, error) type dispatchTable = map[string]rpcHandler var jsonrpcHandlers = dispatchTable{ - "rpc.discover": handleDiscover, - "cartesi_listApplications": handleListApplications, - "cartesi_getApplication": handleGetApplication, - "cartesi_listEpochs": handleListEpochs, - "cartesi_getEpoch": handleGetEpoch, - "cartesi_getEpochByVirtualIndex": handleGetEpochByVirtualIndex, - "cartesi_getLastAcceptedEpochIndex": handleGetLastAcceptedEpochIndex, - "cartesi_listInputs": handleListInputs, - "cartesi_getInput": handleGetInput, - "cartesi_getProcessedInputCount": handleGetProcessedInputCount, - "cartesi_listOutputs": handleListOutputs, - "cartesi_getOutput": handleGetOutput, - "cartesi_listReports": handleListReports, - "cartesi_getReport": handleGetReport, - "cartesi_listWithdrawals": handleListWithdrawals, - "cartesi_getWithdrawal": handleGetWithdrawal, - "cartesi_listTournaments": handleListTournaments, - "cartesi_getTournament": handleGetTournament, - "cartesi_listCommitments": handleListCommitments, - "cartesi_getCommitment": handleGetCommitment, - "cartesi_listMatches": handleListMatches, - "cartesi_getMatch": handleGetMatch, - "cartesi_listMatchAdvances": handleListMatchAdvances, - "cartesi_getMatchAdvanced": handleGetMatchAdvanced, - "cartesi_getNodeInfo": handleGetNodeInfo, - "cartesi_getChainId": handleGetChainID, - "cartesi_getNodeVersion": handleGetNodeVersion, + "rpc.discover": handleDiscover, + "cartesi_listApplications": handleListApplications, + "cartesi_getApplication": handleGetApplication, + "cartesi_listEpochs": handleListEpochs, + "cartesi_getEpoch": handleGetEpoch, + "cartesi_getEpochByVirtualIndex": handleGetEpochByVirtualIndex, + "cartesi_getLastAcceptedEpochIndex": handleGetLastAcceptedEpochIndex, + "cartesi_listInputs": handleListInputs, + "cartesi_getInput": handleGetInput, + "cartesi_getProcessedInputCount": handleGetProcessedInputCount, + "cartesi_getExecutedOutputCount": handleGetExecutedOutputCount, + "cartesi_getPendingExecutableOutputCount": handleGetPendingExecutableOutputCount, + "cartesi_listOutputs": handleListOutputs, + "cartesi_getOutput": handleGetOutput, + "cartesi_listReports": handleListReports, + "cartesi_getReport": handleGetReport, + "cartesi_listWithdrawals": handleListWithdrawals, + "cartesi_getWithdrawal": handleGetWithdrawal, + "cartesi_listTournaments": handleListTournaments, + "cartesi_getTournament": handleGetTournament, + "cartesi_listCommitments": handleListCommitments, + "cartesi_getCommitment": handleGetCommitment, + "cartesi_listMatches": handleListMatches, + "cartesi_getMatch": handleGetMatch, + "cartesi_listMatchAdvances": handleListMatchAdvances, + "cartesi_getMatchAdvanced": handleGetMatchAdvanced, + "cartesi_getNodeInfo": handleGetNodeInfo, + "cartesi_getChainId": handleGetChainID, + "cartesi_getNodeVersion": handleGetNodeVersion, } // ----------------------------------------------------------------------------- @@ -621,6 +623,52 @@ func handleGetProcessedInputCount(s *Service, r *http.Request, req RPCRequest) ( return api.SingleResponse[string]{Data: fmt.Sprintf("0x%x", processedInputs)}, nil } +func handleGetExecutedOutputCount(s *Service, r *http.Request, req RPCRequest) (any, error) { + var params api.GetApplicationParams + if err := UnmarshalParams(req.Params, ¶ms); err != nil { + s.Logger.Debug("Invalid parameters", "err", err) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") + } + + if err := validateNameOrAddress(params.Application); err != nil { + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) + } + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err + } + + count, err := s.repository.GetNumberOfExecutedOutputs(r.Context(), params.Application) + if err != nil { + s.Logger.Error("Unable to retrieve executed output count from repository", "err", err) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") + } + + return api.SingleResponse[string]{Data: fmt.Sprintf("0x%x", count)}, nil +} + +func handleGetPendingExecutableOutputCount(s *Service, r *http.Request, req RPCRequest) (any, error) { + var params api.GetApplicationParams + if err := UnmarshalParams(req.Params, ¶ms); err != nil { + s.Logger.Debug("Invalid parameters", "err", err) + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") + } + + if err := validateNameOrAddress(params.Application); err != nil { + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid application identifier: %v", err)) + } + if err := s.applicationAbsentOrError(r, params.Application); err != nil { + return nil, err + } + + count, err := s.repository.GetNumberOfPendingExecutableOutputs(r.Context(), params.Application) + if err != nil { + s.Logger.Error("Unable to retrieve pending executable output count from repository", "err", err) + return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") + } + + return api.SingleResponse[string]{Data: fmt.Sprintf("0x%x", count)}, nil +} + func handleListOutputs(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListOutputsParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { diff --git a/internal/jsonrpc/jsonrpc_test.go b/internal/jsonrpc/jsonrpc_test.go index 8e6e0478b..64ef9598e 100644 --- a/internal/jsonrpc/jsonrpc_test.go +++ b/internal/jsonrpc/jsonrpc_test.go @@ -911,9 +911,138 @@ func TestMethod(t *testing.T) { assert.Equal(t, uint64(0), uint64(resp.Result.Data)) }) - // TODO: test with inputs (use createTestEpochWithInput) + t.Run("processedInputs", func(t *testing.T) { + testHistogram.inc(method) + s := newTestService(t, t.Name()) + ctx := context.Background() + + app := uint64(1) + appID := s.newTestApplication(ctx, t, app) + epoch := repotest.NewEpochBuilder(appID). + WithIndex(0). + WithStatus(model.EpochStatus_ClaimAccepted). + Build() + inputs := []*model.Input{ + repotest.NewInputBuilder().WithIndex(0).WithRawData(emptyInput()).Build(), + repotest.NewInputBuilder().WithIndex(1).WithRawData(emptyInput()).Build(), + } + err := s.repository.CreateEpochsAndInputs( + ctx, + numberToName(app), + map[*model.Epoch][]*model.Input{epoch: inputs}, + 10, + ) + require.NoError(t, err) + s.advanceInput(ctx, t, appID, 0, 0, nil, nil) + s.advanceInput(ctx, t, appID, 0, 1, nil, nil) + + body := s.doRequest(t, 0, fmt.Appendf([]byte{}, `{ + "jsonrpc": "2.0", + "method": "cartesi_getProcessedInputCount", + "params": { "application": "%s" }, + "id": 0 + }`, numberToName(app))) + + resp := testRPCResponse[hex64]{} + require.NoError(t, json.Unmarshal(body, &resp)) + assert.Nil(t, resp.Error) + assert.Equal(t, uint64(2), uint64(resp.Result.Data)) + }) }) + for _, methodName := range []string{ + "cartesi_getExecutedOutputCount", + "cartesi_getPendingExecutableOutputCount", + } { + t.Run(methodName, func(t *testing.T) { + method := getName(t.Name()) + + t.Run("absentApplication", func(t *testing.T) { + testHistogram.inc(method) + s := newTestService(t, t.Name()) + + body := s.doRequest(t, 0, fmt.Appendf([]byte{}, `{ + "jsonrpc": "2.0", + "method": "%s", + "params": { "application": "%s" }, + "id": 0 + }`, method, numberToName(1))) + + resp := testRPCResponse[hex64]{} + require.NoError(t, json.Unmarshal(body, &resp)) + assert.Equal(t, JSONRPC_APPLICATION_NOT_FOUND, resp.Error.Code) + assert.Equal(t, "Application not found", resp.Error.Message) + }) + + t.Run("existingApplicationWithNoOutputs", func(t *testing.T) { + testHistogram.inc(method) + s := newTestService(t, t.Name()) + app := uint64(1) + s.newTestApplication(context.Background(), t, app) + + body := s.doRequest(t, 0, fmt.Appendf([]byte{}, `{ + "jsonrpc": "2.0", + "method": "%s", + "params": { "application": "%s" }, + "id": 0 + }`, method, numberToName(app))) + + resp := testRPCResponse[hex64]{} + require.NoError(t, json.Unmarshal(body, &resp)) + assert.Nil(t, resp.Error) + assert.Equal(t, uint64(0), uint64(resp.Result.Data)) + }) + + t.Run("outputsPresent", func(t *testing.T) { + testHistogram.inc(method) + s := newTestService(t, t.Name()) + ctx := context.Background() + + app := uint64(1) + appID := s.newTestApplication(ctx, t, app) + epoch := repotest.NewEpochBuilder(appID). + WithIndex(0). + WithStatus(model.EpochStatus_ClaimAccepted). + Build() + input := repotest.NewInputBuilder(). + WithIndex(0). + WithRawData(emptyInput()). + Build() + s.createTestEpochWithInput(ctx, t, numberToName(app), epoch, input) + s.advanceInput(ctx, t, appID, 0, 0, [][]byte{ + emptyVoucher(), + {0x10, 0x32, 0x1e, 0x8b}, + {0xc2, 0x58, 0xd6, 0xe5}, + }, nil) + + txHash := common.HexToHash("0x1") + err := s.repository.UpdateOutputsExecution( + ctx, + numberToName(app), + []*model.Output{{ + InputEpochApplicationID: appID, + Index: 0, + ExecutionTransactionHash: &txHash, + }}, + 10, + ) + require.NoError(t, err) + + body := s.doRequest(t, 0, fmt.Appendf([]byte{}, `{ + "jsonrpc": "2.0", + "method": "%s", + "params": { "application": "%s" }, + "id": 0 + }`, method, numberToName(app))) + + resp := testRPCResponse[hex64]{} + require.NoError(t, json.Unmarshal(body, &resp)) + assert.Nil(t, resp.Error) + assert.Equal(t, uint64(1), uint64(resp.Result.Data)) + }) + }) + } + //////////////////////////////////////////////////////////////////////// // getReport //////////////////////////////////////////////////////////////////////// From 3848b0ef8ebf1a3aeba7b98aa4324023df2be6b0 Mon Sep 17 00:00:00 2001 From: Renato Maia <1887792+renatomaia@users.noreply.github.com> Date: Wed, 29 Jul 2026 17:49:58 -0300 Subject: [PATCH 09/13] feat(jsonrpc): support listing epochs with multiple statuses - 'status' now accepts either a string or string array. - Every status is validated through 'EpochStatus.Scan'. - Invalid values return -32602 (invalid params) and identify the bad value. - An explicit empty array returns -32602 with Invalid epoch status: expected at least one status. - Added coverage for scalar/list decoding, multiple-status filtering, invalid list elements, and empty arrays. - Documents 'status' as 'oneOf': a single EpochStatus or a non-empty array of EpochStatus. - Documents omission as no filter and rejects empty arrays. - Documents the non-terminal watch set. - Clarifies terminal statuses never regress, preventing settled epochs from being re-read. --- .../root/read/epochs/epochs.go | 2 +- .../root/read/outputs/outputs.go | 2 +- .../root/read/service/jsonrpc.go | 10 ++- .../root/read/service/repository.go | 11 ++- internal/jsonrpc/api/params.go | 40 +++++----- internal/jsonrpc/api/params_test.go | 39 +++++++-- internal/jsonrpc/jsonrpc-discover.json | 17 +++- internal/jsonrpc/jsonrpc.go | 15 +++- internal/jsonrpc/jsonrpc_test.go | 80 +++++++++++++++++++ 9 files changed, 174 insertions(+), 42 deletions(-) diff --git a/cmd/cartesi-rollups-cli/root/read/epochs/epochs.go b/cmd/cartesi-rollups-cli/root/read/epochs/epochs.go index 4f1aea19e..434816acd 100644 --- a/cmd/cartesi-rollups-cli/root/read/epochs/epochs.go +++ b/cmd/cartesi-rollups-cli/root/read/epochs/epochs.go @@ -106,7 +106,7 @@ func run(cmd *cobra.Command, args []string) { // Add status filter if provided if cmd.Flags().Changed("status") { - params.Status = &status + params.Status = &api.StringOrList{status} } params.Limit = limit params.Offset = offset diff --git a/cmd/cartesi-rollups-cli/root/read/outputs/outputs.go b/cmd/cartesi-rollups-cli/root/read/outputs/outputs.go index 98feffa04..0b8512704 100644 --- a/cmd/cartesi-rollups-cli/root/read/outputs/outputs.go +++ b/cmd/cartesi-rollups-cli/root/read/outputs/outputs.go @@ -131,7 +131,7 @@ func run(cmd *cobra.Command, args []string) { // Add output type filter if provided if cmd.Flags().Changed("output-type") { - selectors := api.OutputTypeSelectors(outputTypes) + selectors := api.StringOrList(outputTypes) params.OutputType = &selectors } diff --git a/cmd/cartesi-rollups-cli/root/read/service/jsonrpc.go b/cmd/cartesi-rollups-cli/root/read/service/jsonrpc.go index 3d7c41914..489920f46 100644 --- a/cmd/cartesi-rollups-cli/root/read/service/jsonrpc.go +++ b/cmd/cartesi-rollups-cli/root/read/service/jsonrpc.go @@ -46,11 +46,13 @@ func (s *JsonrpcReadService) ListEpochs(ctx context.Context, params api.ListEpoc if _, err := config.ToApplicationNameOrAddressFromString(params.Application); err != nil { return nil, fmt.Errorf("invalid application: %w", err) } - // Add status filter if provided + // Validate status filter if provided if params.Status != nil { - var statusVal model.EpochStatus - if err := statusVal.Scan(*params.Status); err != nil { - return nil, fmt.Errorf("invalid status: %w", err) + for i, status := range *params.Status { + var statusVal model.EpochStatus + if err := statusVal.Scan(status); err != nil { + return nil, fmt.Errorf("invalid status #%d: %w", i+1, err) + } } } diff --git a/cmd/cartesi-rollups-cli/root/read/service/repository.go b/cmd/cartesi-rollups-cli/root/read/service/repository.go index 234e999e6..33c1ea5b5 100644 --- a/cmd/cartesi-rollups-cli/root/read/service/repository.go +++ b/cmd/cartesi-rollups-cli/root/read/service/repository.go @@ -87,11 +87,14 @@ func (s *RepositoryReadService) ListEpochs(ctx context.Context, params api.ListE pagination := repository.Pagination{} // Add status filter if provided if params.Status != nil { - var statusVal model.EpochStatus - if err := statusVal.Scan(*params.Status); err != nil { - return nil, fmt.Errorf("invalid status: %w", err) + filter.Status = make([]model.EpochStatus, len(*params.Status)) + for i, status := range *params.Status { + var statusVal model.EpochStatus + if err := statusVal.Scan(status); err != nil { + return nil, fmt.Errorf("invalid status #%d: %w", i, err) + } + filter.Status[i] = statusVal } - filter.Status = []model.EpochStatus{statusVal} } pagination.Limit = params.Limit pagination.Offset = params.Offset diff --git a/internal/jsonrpc/api/params.go b/internal/jsonrpc/api/params.go index fafbda9a1..47f56e7fa 100644 --- a/internal/jsonrpc/api/params.go +++ b/internal/jsonrpc/api/params.go @@ -9,9 +9,9 @@ import ( "fmt" ) -type OutputTypeSelectors []string +type StringOrList []string -func (s *OutputTypeSelectors) UnmarshalJSON(data []byte) error { +func (s *StringOrList) UnmarshalJSON(data []byte) error { data = bytes.TrimSpace(data) if len(data) > 0 && data[0] == '"' { var value string @@ -44,13 +44,13 @@ type GetApplicationParams struct { // ListEpochsParams aligns with the OpenRPC specification type ListEpochsParams struct { - Application string `json:"application"` - Status *string `json:"status,omitempty"` - Limit uint64 `json:"limit"` - Offset uint64 `json:"offset"` - Descending bool `json:"descending,omitempty"` - From *string `json:"from,omitempty"` // inclusive lower bound on the epoch index (hex) - To *string `json:"to,omitempty"` // inclusive upper bound on the epoch index (hex) + Application string `json:"application"` + Status *StringOrList `json:"status,omitempty"` + Limit uint64 `json:"limit"` + Offset uint64 `json:"offset"` + Descending bool `json:"descending,omitempty"` + From *string `json:"from,omitempty"` // inclusive lower bound on the epoch index (hex) + To *string `json:"to,omitempty"` // inclusive upper bound on the epoch index (hex) } // GetEpochParams aligns with the OpenRPC specification @@ -96,17 +96,17 @@ type GetProcessedInputCountParams struct { // ListOutputsParams aligns with the OpenRPC specification type ListOutputsParams struct { - Application string `json:"application"` - EpochIndex *string `json:"epoch_index,omitempty"` - InputIndex *string `json:"input_index,omitempty"` - OutputType *OutputTypeSelectors `json:"output_type,omitempty"` - VoucherAddress *string `json:"voucher_address,omitempty"` - Limit uint64 `json:"limit"` - Offset uint64 `json:"offset"` - Descending bool `json:"descending,omitempty"` - From *string `json:"from,omitempty"` // inclusive lower bound on the output index (hex) - To *string `json:"to,omitempty"` // inclusive upper bound on the output index (hex) - Executed *bool `json:"executed,omitempty"` + Application string `json:"application"` + EpochIndex *string `json:"epoch_index,omitempty"` + InputIndex *string `json:"input_index,omitempty"` + OutputType *StringOrList `json:"output_type,omitempty"` + VoucherAddress *string `json:"voucher_address,omitempty"` + Limit uint64 `json:"limit"` + Offset uint64 `json:"offset"` + Descending bool `json:"descending,omitempty"` + From *string `json:"from,omitempty"` // inclusive lower bound on the output index (hex) + To *string `json:"to,omitempty"` // inclusive upper bound on the output index (hex) + Executed *bool `json:"executed,omitempty"` } // GetOutputParams aligns with the OpenRPC specification diff --git a/internal/jsonrpc/api/params_test.go b/internal/jsonrpc/api/params_test.go index 9c4fbdca7..d47aa81cd 100644 --- a/internal/jsonrpc/api/params_test.go +++ b/internal/jsonrpc/api/params_test.go @@ -10,22 +10,22 @@ import ( "github.com/stretchr/testify/require" ) -func TestListOutputsParamsOutputTypeSelectors(t *testing.T) { +func TestListOutputsParamsStringOrList(t *testing.T) { tests := map[string]struct { input string - expected OutputTypeSelectors + expected StringOrList }{ "single selector": { input: `{"output_type":"0x237a816f"}`, - expected: OutputTypeSelectors{"0x237a816f"}, + expected: StringOrList{"0x237a816f"}, }, "selector list": { input: `{"output_type":["0x237a816f","0x10321e8b"]}`, - expected: OutputTypeSelectors{"0x237a816f", "0x10321e8b"}, + expected: StringOrList{"0x237a816f", "0x10321e8b"}, }, "empty list": { input: `{"output_type":[]}`, - expected: OutputTypeSelectors{}, + expected: StringOrList{}, }, } @@ -39,6 +39,35 @@ func TestListOutputsParamsOutputTypeSelectors(t *testing.T) { } } +func TestListEpochsParamsStringOrList(t *testing.T) { + tests := map[string]struct { + input string + expected StringOrList + }{ + "single status": { + input: `{"status":"OPEN"}`, + expected: StringOrList{"OPEN"}, + }, + "status list": { + input: `{"status":["OPEN","CLOSED"]}`, + expected: StringOrList{"OPEN", "CLOSED"}, + }, + "empty list": { + input: `{"status":[]}`, + expected: StringOrList{}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + var params ListEpochsParams + require.NoError(t, json.Unmarshal([]byte(test.input), ¶ms)) + require.NotNil(t, params.Status) + require.Equal(t, test.expected, *params.Status) + }) + } +} + func TestListOutputsParamsExecutedIsOptional(t *testing.T) { var omitted ListOutputsParams require.NoError(t, json.Unmarshal([]byte(`{}`), &omitted)) diff --git a/internal/jsonrpc/jsonrpc-discover.json b/internal/jsonrpc/jsonrpc-discover.json index fd431b72c..79fb28df4 100644 --- a/internal/jsonrpc/jsonrpc-discover.json +++ b/internal/jsonrpc/jsonrpc-discover.json @@ -91,7 +91,7 @@ { "name": "cartesi_listEpochs", "summary": "List epochs", - "description": "Returns a paginated list of epochs for the specified application. Can filter by epoch status.", + "description": "Returns a paginated list of epochs for the specified application. Can filter by one or more epoch statuses.\n\nTo watch epochs that have not settled, repeatedly filter by the non-terminal statuses `OPEN`, `CLOSED`, `INPUTS_PROCESSED`, `CLAIM_COMPUTED`, `CLAIM_SUBMITTED`, and `CLAIM_STAGED`. Terminal statuses (`CLAIM_ACCEPTED`, `CLAIM_REJECTED`, and `CLAIM_FORECLOSED`) never regress, so this non-terminal watch pattern never re-reads settled epochs.", "params": [ { "name": "application", @@ -103,9 +103,20 @@ }, { "name": "status", - "description": "Filter epochs by status.", + "description": "Filter epochs by one status or a non-empty list of statuses. Omit this parameter to disable status filtering; an empty list is invalid.", "schema": { - "$ref": "#/components/schemas/EpochStatus" + "oneOf": [ + { + "$ref": "#/components/schemas/EpochStatus" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/EpochStatus" + }, + "minItems": 1 + } + ] }, "required": false }, diff --git a/internal/jsonrpc/jsonrpc.go b/internal/jsonrpc/jsonrpc.go index e116cf0a8..46e0364f9 100644 --- a/internal/jsonrpc/jsonrpc.go +++ b/internal/jsonrpc/jsonrpc.go @@ -351,11 +351,18 @@ func handleListEpochs(s *Service, r *http.Request, req RPCRequest) (any, error) } epochFilter.IndexRange = indexRange if params.Status != nil { - var status model.EpochStatus - if err := status.Scan(*params.Status); err != nil { - return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch status: %v", err)) + if len(*params.Status) == 0 { + return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid epoch status: expected at least one status") } - epochFilter.Status = []model.EpochStatus{status} + statuses := make([]model.EpochStatus, 0, len(*params.Status)) + for _, value := range *params.Status { + var status model.EpochStatus + if err := status.Scan(value); err != nil { + return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid epoch status: %v", err)) + } + statuses = append(statuses, status) + } + epochFilter.Status = statuses } epochs, total, err := s.repository.ListEpochs(r.Context(), params.Application, epochFilter, repository.Pagination{ diff --git a/internal/jsonrpc/jsonrpc_test.go b/internal/jsonrpc/jsonrpc_test.go index 64ef9598e..4b44e2a83 100644 --- a/internal/jsonrpc/jsonrpc_test.go +++ b/internal/jsonrpc/jsonrpc_test.go @@ -1415,6 +1415,86 @@ func TestMethod(t *testing.T) { assert.Equal(t, "Invalid epoch status: invalid value 'INVALID' for EpochStatus enum", resp.Error.Message) }) + // failure: any invalid status in a list -> invalid params + t.Run("invalidInList", func(t *testing.T) { + testHistogram.inc(method) + s := newTestService(t, t.Name()) + + body := s.doRequest(t, 0, []byte(`{ + "jsonrpc": "2.0", + "method": "cartesi_listEpochs", + "params": { + "application": "app", + "status": ["OPEN", "INVALID"] + }, + "id": 0 + }`)) + + resp := testRPCResponse[[]model.Epoch]{} + assert.Nil(t, json.Unmarshal(body, &resp)) + assert.Equal(t, JSONRPC_INVALID_PARAMS, resp.Error.Code) + assert.Equal(t, "Invalid epoch status: invalid value 'INVALID' for EpochStatus enum", resp.Error.Message) + }) + + // failure: an explicitly empty status list -> invalid params + t.Run("emptyStatusList", func(t *testing.T) { + testHistogram.inc(method) + s := newTestService(t, t.Name()) + + body := s.doRequest(t, 0, []byte(`{ + "jsonrpc": "2.0", + "method": "cartesi_listEpochs", + "params": { + "application": "app", + "status": [] + }, + "id": 0 + }`)) + + resp := testRPCResponse[[]model.Epoch]{} + assert.Nil(t, json.Unmarshal(body, &resp)) + assert.Equal(t, JSONRPC_INVALID_PARAMS, resp.Error.Code) + assert.Equal(t, "Invalid epoch status: expected at least one status", resp.Error.Message) + }) + + // success: status may contain multiple values + t.Run("multipleStatuses", func(t *testing.T) { + testHistogram.inc(method) + s := newTestService(t, t.Name()) + ctx := context.Background() + + nr := uint64(1) + appID := s.newTestApplication(ctx, t, nr) + for i, status := range []model.EpochStatus{ + model.EpochStatus_Open, + model.EpochStatus_Closed, + model.EpochStatus_ClaimAccepted, + } { + s.createTestEpoch(ctx, t, numberToName(nr), + repotest.NewEpochBuilder(appID). + WithIndex(uint64(i)). + WithStatus(status). + Build()) + } + + body := s.doRequest(t, 0, fmt.Appendf([]byte{}, `{ + "jsonrpc": "2.0", + "method": "cartesi_listEpochs", + "params": { + "application": "%v", + "status": ["OPEN", "CLOSED"] + }, + "id": 0 + }`, numberToName(nr))) + + resp := testRPCResponse[[]model.Epoch]{} + assert.Nil(t, json.Unmarshal(body, &resp)) + assert.Nil(t, resp.Error) + assert.Len(t, resp.Result.Data, 2) + assert.Equal(t, model.EpochStatus_Open, resp.Result.Data[0].Status) + assert.Equal(t, model.EpochStatus_Closed, resp.Result.Data[1].Status) + }) + // success: many epochs is in the database -> limit t.Run("many", func(t *testing.T) { testHistogram.inc(method) From b3447d91b3bf9679f234d4b54e3b514f0cd6f34d Mon Sep 17 00:00:00 2001 From: Renato Maia <1887792+renatomaia@users.noreply.github.com> Date: Wed, 29 Jul 2026 17:59:24 -0300 Subject: [PATCH 10/13] feat(cli): support listing epochs with multiple statuses - '--status' now uses a repeatable string-array flag. - Multiple values are sent as StringOrList. - Updated help text and example. --- cmd/cartesi-rollups-cli/root/read/epochs/epochs.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cmd/cartesi-rollups-cli/root/read/epochs/epochs.go b/cmd/cartesi-rollups-cli/root/read/epochs/epochs.go index 434816acd..22c0aca23 100644 --- a/cmd/cartesi-rollups-cli/root/read/epochs/epochs.go +++ b/cmd/cartesi-rollups-cli/root/read/epochs/epochs.go @@ -40,23 +40,23 @@ cartesi-rollups-cli read epochs echo-dapp 10 cartesi-rollups-cli read epochs echo-dapp # Read all epochs with filter: -cartesi-rollups-cli read epochs echo-dapp --status OPEN +cartesi-rollups-cli read epochs echo-dapp --status OPEN --status CLOSED # Read all epochs with pagination: cartesi-rollups-cli read epochs echo-dapp --limit 10 --offset 10 --descending ` var ( - status string + statuses []string limit uint64 offset uint64 descending bool ) func init() { - Cmd.Flags().StringVar(&status, "status", "", + Cmd.Flags().StringArrayVar(&statuses, "status", nil, "Filter epochs by status (OPEN, CLOSED, INPUTS_PROCESSED, CLAIM_COMPUTED, CLAIM_SUBMITTED, "+ - "CLAIM_STAGED, CLAIM_ACCEPTED, CLAIM_REJECTED, CLAIM_FORECLOSED)") + "CLAIM_STAGED, CLAIM_ACCEPTED, CLAIM_REJECTED, CLAIM_FORECLOSED); may be specified multiple times") Cmd.Flags().Uint64Var(&limit, "limit", 50, //nolint: mnd "Maximum number of epochs to return") Cmd.Flags().Uint64Var(&offset, "offset", 0, @@ -106,7 +106,8 @@ func run(cmd *cobra.Command, args []string) { // Add status filter if provided if cmd.Flags().Changed("status") { - params.Status = &api.StringOrList{status} + epochStatuses := api.StringOrList(statuses) + params.Status = &epochStatuses } params.Limit = limit params.Offset = offset From e0019ce929e49ad1a93e13153c998a514a6e13e7 Mon Sep 17 00:00:00 2001 From: Renato Maia <1887792+renatomaia@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:25:29 -0300 Subject: [PATCH 11/13] fix(jsonrpc): report 256-bit integer fields in OpenRPC specification Added 'UnsignedInteger256' schema for uint256 reference it in field 'Voucher.value'. Updated these 'EvmAdvance' uint256 fields to reference 'UnsignedInteger256': - chain_id - block_number - block_timestamp - prev_randao Kept 'EvmAdvance.index' and other genuine uint64 indexes on 'UnsignedInteger'. --- internal/jsonrpc/jsonrpc-discover.json | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/internal/jsonrpc/jsonrpc-discover.json b/internal/jsonrpc/jsonrpc-discover.json index 79fb28df4..bf5df27fd 100644 --- a/internal/jsonrpc/jsonrpc-discover.json +++ b/internal/jsonrpc/jsonrpc-discover.json @@ -1956,7 +1956,7 @@ "type": "object", "properties": { "chain_id": { - "$ref": "#/components/schemas/UnsignedInteger" + "$ref": "#/components/schemas/UnsignedInteger256" }, "application_contract": { "$ref": "#/components/schemas/EthereumAddress" @@ -1965,13 +1965,13 @@ "$ref": "#/components/schemas/EthereumAddress" }, "block_number": { - "$ref": "#/components/schemas/UnsignedInteger" + "$ref": "#/components/schemas/UnsignedInteger256" }, "block_timestamp": { - "$ref": "#/components/schemas/UnsignedInteger" + "$ref": "#/components/schemas/UnsignedInteger256" }, "prev_randao": { - "$ref": "#/components/schemas/ByteArray" + "$ref": "#/components/schemas/UnsignedInteger256" }, "index": { "$ref": "#/components/schemas/UnsignedInteger" @@ -2104,7 +2104,8 @@ "$ref": "#/components/schemas/EthereumAddress" }, "value": { - "type": "string" + "$ref": "#/components/schemas/UnsignedInteger256", + "description": "Amount of Wei transferred by the voucher's call" }, "payload": { "$ref": "#/components/schemas/ByteArray" @@ -2441,6 +2442,12 @@ "format": "hex-uint64", "pattern": "^0x[a-fA-F0-9]{1,16}$" }, + "UnsignedInteger256": { + "type": "string", + "format": "hex-uint256", + "pattern": "^0x[a-fA-F0-9]{1,64}$", + "description": "256-bit unsigned integer, hex encoded (the node emits minimal hex)" + }, "FunctionSelector": { "type": "string", "format": "hex-byte", From b54eddd35732ac826b7cfd0b327c44dddf1cb66d Mon Sep 17 00:00:00 2001 From: Renato Maia <1887792+renatomaia@users.noreply.github.com> Date: Wed, 29 Jul 2026 21:35:43 -0300 Subject: [PATCH 12/13] refactor(jsonrpc): rename cartesi_getMatchAdvanced as cartesi_getMatchAdvance - Renamed handler to handleGetMatchAdvance. - Renamed params struct to GetMatchAdvanceParams across node and CLI. - Updated OpenRPC discovery and JSON-RPC tests. - Normalized the parsed parent hash before repository lookup. - Added a mixed-case parent-hash regression test. --- .../root/read/matchadvances/matchadvances.go | 2 +- .../root/read/service/jsonrpc.go | 4 ++-- .../root/read/service/repository.go | 2 +- .../root/read/service/types.go | 2 +- internal/jsonrpc/api/params.go | 4 ++-- internal/jsonrpc/jsonrpc-discover.json | 2 +- internal/jsonrpc/jsonrpc.go | 11 ++++++----- internal/jsonrpc/jsonrpc_test.go | 19 ++++++++++--------- 8 files changed, 24 insertions(+), 22 deletions(-) diff --git a/cmd/cartesi-rollups-cli/root/read/matchadvances/matchadvances.go b/cmd/cartesi-rollups-cli/root/read/matchadvances/matchadvances.go index a0e3847e7..6253038ff 100644 --- a/cmd/cartesi-rollups-cli/root/read/matchadvances/matchadvances.go +++ b/cmd/cartesi-rollups-cli/root/read/matchadvances/matchadvances.go @@ -90,7 +90,7 @@ func run(cmd *cobra.Command, args []string) { var result json.RawMessage if len(args) >= 5 { - var params api.GetMatchAdvancedParams + var params api.GetMatchAdvanceParams params.Application = args[0] params.EpochIndex, err = config.AsHexString(args[1]) cobra.CheckErr(err) diff --git a/cmd/cartesi-rollups-cli/root/read/service/jsonrpc.go b/cmd/cartesi-rollups-cli/root/read/service/jsonrpc.go index 489920f46..1a96b2099 100644 --- a/cmd/cartesi-rollups-cli/root/read/service/jsonrpc.go +++ b/cmd/cartesi-rollups-cli/root/read/service/jsonrpc.go @@ -342,7 +342,7 @@ func (s *JsonrpcReadService) ListMatches(ctx context.Context, params api.ListMat return resp, err } -func (s *JsonrpcReadService) GetMatchAdvanced(ctx context.Context, params api.GetMatchAdvancedParams) (json.RawMessage, error) { +func (s *JsonrpcReadService) GetMatchAdvanced(ctx context.Context, params api.GetMatchAdvanceParams) (json.RawMessage, error) { if _, err := config.ToApplicationNameOrAddressFromString(params.Application); err != nil { return nil, fmt.Errorf("invalid application: %w", err) } @@ -360,7 +360,7 @@ func (s *JsonrpcReadService) GetMatchAdvanced(ctx context.Context, params api.Ge } var resp json.RawMessage - err := s.Client.Call(ctx, "cartesi_getMatchAdvanced", params, &resp) + err := s.Client.Call(ctx, "cartesi_getMatchAdvance", params, &resp) return resp, err } diff --git a/cmd/cartesi-rollups-cli/root/read/service/repository.go b/cmd/cartesi-rollups-cli/root/read/service/repository.go index 33c1ea5b5..d0cc9f338 100644 --- a/cmd/cartesi-rollups-cli/root/read/service/repository.go +++ b/cmd/cartesi-rollups-cli/root/read/service/repository.go @@ -790,7 +790,7 @@ func (s *RepositoryReadService) ListMatches(ctx context.Context, params api.List return json.RawMessage(result), err } -func (s *RepositoryReadService) GetMatchAdvanced(ctx context.Context, params api.GetMatchAdvancedParams) (json.RawMessage, error) { +func (s *RepositoryReadService) GetMatchAdvanced(ctx context.Context, params api.GetMatchAdvanceParams) (json.RawMessage, error) { repo := s.Repository application, err := config.ToApplicationNameOrAddressFromString(params.Application) if err != nil { diff --git a/cmd/cartesi-rollups-cli/root/read/service/types.go b/cmd/cartesi-rollups-cli/root/read/service/types.go index fbdc8f3d4..f8ecae6e2 100644 --- a/cmd/cartesi-rollups-cli/root/read/service/types.go +++ b/cmd/cartesi-rollups-cli/root/read/service/types.go @@ -35,7 +35,7 @@ type ReadService interface { ListCommitments(ctx context.Context, params api.ListCommitmentsParams) (json.RawMessage, error) GetMatch(ctx context.Context, params api.GetMatchParams) (json.RawMessage, error) ListMatches(ctx context.Context, params api.ListMatchesParams) (json.RawMessage, error) - GetMatchAdvanced(ctx context.Context, params api.GetMatchAdvancedParams) (json.RawMessage, error) + GetMatchAdvanced(ctx context.Context, params api.GetMatchAdvanceParams) (json.RawMessage, error) ListMatchAdvances(ctx context.Context, params api.ListMatchAdvancesParams) (json.RawMessage, error) Close() } diff --git a/internal/jsonrpc/api/params.go b/internal/jsonrpc/api/params.go index 47f56e7fa..adffd98ef 100644 --- a/internal/jsonrpc/api/params.go +++ b/internal/jsonrpc/api/params.go @@ -198,8 +198,8 @@ type ListMatchAdvancesParams struct { Descending bool `json:"descending,omitempty"` } -// GetMatchAdvancedParams aligns with the OpenRPC specification -type GetMatchAdvancedParams struct { +// GetMatchAdvanceParams aligns with the OpenRPC specification +type GetMatchAdvanceParams struct { Application string `json:"application"` EpochIndex string `json:"epoch_index"` TournamentAddress string `json:"tournament_address"` diff --git a/internal/jsonrpc/jsonrpc-discover.json b/internal/jsonrpc/jsonrpc-discover.json index bf5df27fd..aba09a6bb 100644 --- a/internal/jsonrpc/jsonrpc-discover.json +++ b/internal/jsonrpc/jsonrpc-discover.json @@ -1457,7 +1457,7 @@ ] }, { - "name": "cartesi_getMatchAdvanced", + "name": "cartesi_getMatchAdvance", "summary": "Get a specific match advance", "description": "Fetches a single match advance by application, epoch index, tournament address, ID hash and parent.", "params": [ diff --git a/internal/jsonrpc/jsonrpc.go b/internal/jsonrpc/jsonrpc.go index 46e0364f9..59988e180 100644 --- a/internal/jsonrpc/jsonrpc.go +++ b/internal/jsonrpc/jsonrpc.go @@ -82,7 +82,7 @@ var jsonrpcHandlers = dispatchTable{ "cartesi_listMatches": handleListMatches, "cartesi_getMatch": handleGetMatch, "cartesi_listMatchAdvances": handleListMatchAdvances, - "cartesi_getMatchAdvanced": handleGetMatchAdvanced, + "cartesi_getMatchAdvance": handleGetMatchAdvance, "cartesi_getNodeInfo": handleGetNodeInfo, "cartesi_getChainId": handleGetChainID, "cartesi_getNodeVersion": handleGetNodeVersion, @@ -1399,8 +1399,8 @@ func handleListMatchAdvances(s *Service, r *http.Request, req RPCRequest) (any, }, nil } -func handleGetMatchAdvanced(s *Service, r *http.Request, req RPCRequest) (any, error) { - var params api.GetMatchAdvancedParams +func handleGetMatchAdvance(s *Service, r *http.Request, req RPCRequest) (any, error) { + var params api.GetMatchAdvanceParams if err := UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") @@ -1424,12 +1424,13 @@ func handleGetMatchAdvanced(s *Service, r *http.Request, req RPCRequest) (any, e return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid ID hash: %v", err)) } - if _, err := config.ToHashFromString(params.Parent); err != nil { + parent, err := config.ToHashFromString(params.Parent) + if err != nil { return nil, newRPCError(JSONRPC_INVALID_PARAMS, fmt.Sprintf("Invalid parent hash: %v", err)) } matchAdvanced, err := s.repository.GetMatchAdvanced(r.Context(), params.Application, epochIndex, - params.TournamentAddress, params.IDHash, params.Parent[2:]) // TODO: use parsed value + params.TournamentAddress, params.IDHash, parent.Hex()[2:]) if err != nil { s.Logger.Error("Unable to retrieve match advanced from repository", "err", err) return nil, newRPCError(JSONRPC_INTERNAL_ERROR, "Internal server error") diff --git a/internal/jsonrpc/jsonrpc_test.go b/internal/jsonrpc/jsonrpc_test.go index 4b44e2a83..041a2d85d 100644 --- a/internal/jsonrpc/jsonrpc_test.go +++ b/internal/jsonrpc/jsonrpc_test.go @@ -2743,9 +2743,9 @@ func TestMethod(t *testing.T) { }) //////////////////////////////////////////////////////////////////////// - // getMatchAdvanced + // getMatchAdvance //////////////////////////////////////////////////////////////////////// - t.Run("cartesi_getMatchAdvanced", func(t *testing.T) { + t.Run("cartesi_getMatchAdvance", func(t *testing.T) { method := getName(t.Name()) // failure: epoch_index not hex encoded -> invalid param @@ -2758,7 +2758,7 @@ func TestMethod(t *testing.T) { body := s.doRequest(t, 0, fmt.Appendf([]byte{}, `{ "jsonrpc": "2.0", - "method": "cartesi_getMatchAdvanced", + "method": "cartesi_getMatchAdvance", "params": { "application": "%v", "epoch_index": "%v" @@ -2790,7 +2790,7 @@ func TestMethod(t *testing.T) { body := s.doRequest(t, 0, fmt.Appendf([]byte{}, `{ "jsonrpc": "2.0", - "method": "cartesi_getMatchAdvanced", + "method": "cartesi_getMatchAdvance", "params": { "application": "%v", "epoch_index": "0x%020x", @@ -2815,7 +2815,7 @@ func TestMethod(t *testing.T) { nr := uint64(0xdeadbeef) body := s.doRequest(t, 0, fmt.Appendf([]byte{}, `{ "jsonrpc": "2.0", - "method": "cartesi_getMatchAdvanced", + "method": "cartesi_getMatchAdvance", "params": { "application": "%v", "epoch_index": "0x%020x", @@ -2842,7 +2842,8 @@ func TestMethod(t *testing.T) { nr := uint64(2) address := common.HexToAddress("0x03") idHash := common.HexToHash("0x04") - parent := common.HexToHash("0x05") + parentHex := "0xAbCdEf0123456789aBcDeF0123456789AbCdEf0123456789aBcDeF0123456789" + parent := common.HexToHash(parentHex) appID := s.newTestApplication(ctx, t, app) s.createTestEpoch(ctx, t, numberToName(app), @@ -2888,16 +2889,16 @@ func TestMethod(t *testing.T) { body := s.doRequest(t, 0, fmt.Appendf([]byte{}, `{ "jsonrpc": "2.0", - "method": "cartesi_getMatchAdvanced", + "method": "cartesi_getMatchAdvance", "params": { "application": "%v", "epoch_index": "0x%020x", "tournament_address": "0x%020x", "id_hash": "0x%064x", - "parent": "0x%064x" + "parent": "%s" }, "id": 0 - }`, numberToName(app), nr, address, idHash, parent)) + }`, numberToName(app), nr, address, idHash, parentHex)) resp := testRPCResponse[getMatchAdvancedResult]{} assert.Nil(t, json.Unmarshal(body, &resp)) From 31e82e1f5f7fa80a05d1279b4c90e77bdd54aba5 Mon Sep 17 00:00:00 2001 From: Renato Maia <1887792+renatomaia@users.noreply.github.com> Date: Thu, 30 Jul 2026 09:17:46 -0300 Subject: [PATCH 13/13] test(jsonrpc): add tests for positional decoding of parameters Tests covers all structs declared in `params.go`, comparing positional decoding against equivalent named decoding. --- internal/jsonrpc/api/params.go | 47 ++++++++++ internal/jsonrpc/api/params_test.go | 136 ++++++++++++++++++++++++++++ internal/jsonrpc/jsonrpc.go | 50 +++++----- internal/jsonrpc/types.go | 48 ---------- 4 files changed, 208 insertions(+), 73 deletions(-) diff --git a/internal/jsonrpc/api/params.go b/internal/jsonrpc/api/params.go index adffd98ef..2a5958556 100644 --- a/internal/jsonrpc/api/params.go +++ b/internal/jsonrpc/api/params.go @@ -7,6 +7,7 @@ import ( "bytes" "encoding/json" "fmt" + "reflect" ) type StringOrList []string @@ -221,3 +222,49 @@ type GetWithdrawalParams struct { Application string `json:"application"` AccountIndex string `json:"account_index"` } + +// UnmarshalParams supports both by-name (object) and by-position (array) parameter structures. +// If params is an object, it simply does json.Unmarshal; if it's an array, it will attempt +// to unmarshal each positional parameter into the target struct field in declaration order. +func UnmarshalParams(data json.RawMessage, target any) error { + data = bytes.TrimSpace(data) + if len(data) > 0 && data[0] == '[' { + // Unmarshal positional parameters into a slice of json.RawMessage. + var rawParams []json.RawMessage + if err := json.Unmarshal(data, &rawParams); err != nil { + return err + } + // Use reflection to set values in the target struct in the order they appear. + val := reflect.ValueOf(target) + if val.Kind() != reflect.Pointer || val.IsNil() { + return fmt.Errorf("error unmarshalling positional parameters target must be a non-nil pointer to a struct") + } + val = val.Elem() + if val.Kind() != reflect.Struct { + return fmt.Errorf("error unmarshalling positional parameters target must point to a struct") + } + typ := val.Type() + if len(rawParams) > typ.NumField() { + return fmt.Errorf("error unmarshalling positional parameters, expected %d params, got %d", + typ.NumField(), len(rawParams)) + } + // For each field in the struct, if a positional parameter exists, unmarshal that parameter. + for i := 0; i < typ.NumField() && i < len(rawParams); i++ { + sf := typ.Field(i) + if sf.Tag.Get("json") == "-" { + continue + } + field := val.Field(i) + if !field.CanSet() { + return fmt.Errorf("error unmarshalling positional parameter field %q is not settable", typ.Field(i).Name) + } + // Unmarshal the corresponding raw parameter into the field. + if err := json.Unmarshal(rawParams[i], field.Addr().Interface()); err != nil { + return fmt.Errorf("error unmarshalling positional parameter %d for field %s: %w", i, typ.Field(i).Name, err) + } + } + return nil + } + // Otherwise, assume by-name structure. + return json.Unmarshal(data, target) +} diff --git a/internal/jsonrpc/api/params_test.go b/internal/jsonrpc/api/params_test.go index d47aa81cd..c774a45e7 100644 --- a/internal/jsonrpc/api/params_test.go +++ b/internal/jsonrpc/api/params_test.go @@ -83,3 +83,139 @@ func TestListOutputsParamsExecutedIsOptional(t *testing.T) { require.NotNil(t, pending.Executed) require.False(t, *pending.Executed) } + +func TestPositionalParamsDeclarationOrder(t *testing.T) { + tests := map[string]struct { + newTarget func() any + positional string + named string + }{ + "ListApplicationsParams": { + func() any { return &ListApplicationsParams{} }, + `[25,3,true]`, + `{"limit":25,"offset":3,"descending":true}`, + }, + "GetApplicationParams": { + func() any { return &GetApplicationParams{} }, + `["app"]`, + `{"application":"app"}`, + }, + "ListEpochsParams": { + func() any { return &ListEpochsParams{} }, + `["app",["OPEN","CLOSED"],25,3,true,"0x2","0x9"]`, + `{"application":"app","status":["OPEN","CLOSED"],"limit":25,"offset":3,"descending":true,"from":"0x2","to":"0x9"}`, + }, + "GetEpochParams": { + func() any { return &GetEpochParams{} }, + `["app","0x4"]`, + `{"application":"app","epoch_index":"0x4"}`, + }, + "GetEpochByVirtualIndexParams": { + func() any { return &GetEpochByVirtualIndexParams{} }, + `["app","0x7"]`, + `{"application":"app","virtual_index":"0x7"}`, + }, + "GetLastAcceptedEpochIndexParams": { + func() any { return &GetLastAcceptedEpochIndexParams{} }, + `["app"]`, + `{"application":"app"}`, + }, + "ListInputsParams": { + func() any { return &ListInputsParams{} }, + `["app","0x4","sender","transaction-hash",25,3,true,"0x2","0x9"]`, + `{"application":"app","epoch_index":"0x4","sender":"sender","transaction_hash":"transaction-hash","limit":25,"offset":3,"descending":true,"from":"0x2","to":"0x9"}`, + }, + "GetInputParams": { + func() any { return &GetInputParams{} }, + `["app","0x5"]`, + `{"application":"app","input_index":"0x5"}`, + }, + "GetProcessedInputCountParams": { + func() any { return &GetProcessedInputCountParams{} }, + `["app"]`, + `{"application":"app"}`, + }, + "ListOutputsParams": { + func() any { return &ListOutputsParams{} }, + `["app","0x4","0x5",["0x237a816f","0x10321e8b"],"voucher",25,3,true,"0x2","0x9",true]`, + `{"application":"app","epoch_index":"0x4","input_index":"0x5","output_type":["0x237a816f","0x10321e8b"],"voucher_address":"voucher","limit":25,"offset":3,"descending":true,"from":"0x2","to":"0x9","executed":true}`, + }, + "GetOutputParams": { + func() any { return &GetOutputParams{} }, + `["app","0x6"]`, + `{"application":"app","output_index":"0x6"}`, + }, + "ListReportsParams": { + func() any { return &ListReportsParams{} }, + `["app","0x4","0x5",25,3,true,"0x2","0x9"]`, + `{"application":"app","epoch_index":"0x4","input_index":"0x5","limit":25,"offset":3,"descending":true,"from":"0x2","to":"0x9"}`, + }, + "GetReportParams": { + func() any { return &GetReportParams{} }, + `["app","0x7"]`, + `{"application":"app","report_index":"0x7"}`, + }, + "ListTournamentsParams": { + func() any { return &ListTournamentsParams{} }, + `["app","0x4","0x2","parent-tournament","parent-match",25,3,true]`, + `{"application":"app","epoch_index":"0x4","level":"0x2","parent_tournament_address":"parent-tournament","parent_match_id_hash":"parent-match","limit":25,"offset":3,"descending":true}`, + }, + "GetTournamentParams": { + func() any { return &GetTournamentParams{} }, + `["app","tournament"]`, + `{"application":"app","address":"tournament"}`, + }, + "ListCommitmentsParams": { + func() any { return &ListCommitmentsParams{} }, + `["app","0x4","tournament",25,3,true]`, + `{"application":"app","epoch_index":"0x4","tournament_address":"tournament","limit":25,"offset":3,"descending":true}`, + }, + "GetCommitmentParams": { + func() any { return &GetCommitmentParams{} }, + `["app","0x4","tournament","commitment"]`, + `{"application":"app","epoch_index":"0x4","tournament_address":"tournament","commitment":"commitment"}`, + }, + "ListMatchesParams": { + func() any { return &ListMatchesParams{} }, + `["app","0x4","tournament",25,3,true]`, + `{"application":"app","epoch_index":"0x4","tournament_address":"tournament","limit":25,"offset":3,"descending":true}`, + }, + "GetMatchParams": { + func() any { return &GetMatchParams{} }, + `["app","0x4","tournament","id-hash"]`, + `{"application":"app","epoch_index":"0x4","tournament_address":"tournament","id_hash":"id-hash"}`, + }, + "ListMatchAdvancesParams": { + func() any { return &ListMatchAdvancesParams{} }, + `["app","0x4","tournament","id-hash",25,3,true]`, + `{"application":"app","epoch_index":"0x4","tournament_address":"tournament","id_hash":"id-hash","limit":25,"offset":3,"descending":true}`, + }, + "GetMatchAdvanceParams": { + func() any { return &GetMatchAdvanceParams{} }, + `["app","0x4","tournament","id-hash","parent"]`, + `{"application":"app","epoch_index":"0x4","tournament_address":"tournament","id_hash":"id-hash","parent":"parent"}`, + }, + "ListWithdrawalsParams": { + func() any { return &ListWithdrawalsParams{} }, + `["app","0x8",25,3,true]`, + `{"application":"app","account_index":"0x8","limit":25,"offset":3,"descending":true}`, + }, + "GetWithdrawalParams": { + func() any { return &GetWithdrawalParams{} }, + `["app","0x8"]`, + `{"application":"app","account_index":"0x8"}`, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + expected := test.newTarget() + require.NoError(t, json.Unmarshal([]byte(test.named), expected)) + + actual := test.newTarget() + require.NoError(t, UnmarshalParams(json.RawMessage(test.positional), actual)) + + require.Equal(t, expected, actual) + }) + } +} diff --git a/internal/jsonrpc/jsonrpc.go b/internal/jsonrpc/jsonrpc.go index 59988e180..9faa9fdff 100644 --- a/internal/jsonrpc/jsonrpc.go +++ b/internal/jsonrpc/jsonrpc.go @@ -264,7 +264,7 @@ func handleDiscover(s *Service, _ *http.Request, _ RPCRequest) (any, error) { func handleListApplications(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListApplicationsParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -301,7 +301,7 @@ func handleListApplications(s *Service, r *http.Request, req RPCRequest) (any, e func handleGetApplication(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetApplicationParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -325,7 +325,7 @@ func handleGetApplication(s *Service, r *http.Request, req RPCRequest) (any, err func handleListEpochs(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListEpochsParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -395,7 +395,7 @@ func handleListEpochs(s *Service, r *http.Request, req RPCRequest) (any, error) func handleGetEpoch(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetEpochParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -427,7 +427,7 @@ func handleGetEpoch(s *Service, r *http.Request, req RPCRequest) (any, error) { func handleGetEpochByVirtualIndex(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetEpochByVirtualIndexParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -459,7 +459,7 @@ func handleGetEpochByVirtualIndex(s *Service, r *http.Request, req RPCRequest) ( func handleGetLastAcceptedEpochIndex(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetLastAcceptedEpochIndexParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -486,7 +486,7 @@ func handleGetLastAcceptedEpochIndex(s *Service, r *http.Request, req RPCRequest func handleListInputs(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListInputsParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -571,7 +571,7 @@ func handleListInputs(s *Service, r *http.Request, req RPCRequest) (any, error) func handleGetInput(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetInputParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -608,7 +608,7 @@ func handleGetInput(s *Service, r *http.Request, req RPCRequest) (any, error) { func handleGetProcessedInputCount(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetApplicationParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -632,7 +632,7 @@ func handleGetProcessedInputCount(s *Service, r *http.Request, req RPCRequest) ( func handleGetExecutedOutputCount(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetApplicationParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -655,7 +655,7 @@ func handleGetExecutedOutputCount(s *Service, r *http.Request, req RPCRequest) ( func handleGetPendingExecutableOutputCount(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetApplicationParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -678,7 +678,7 @@ func handleGetPendingExecutableOutputCount(s *Service, r *http.Request, req RPCR func handleListOutputs(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListOutputsParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -782,7 +782,7 @@ func handleListOutputs(s *Service, r *http.Request, req RPCRequest) (any, error) func handleGetOutput(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetOutputParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -819,7 +819,7 @@ func handleGetOutput(s *Service, r *http.Request, req RPCRequest) (any, error) { func handleListReports(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListReportsParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -891,7 +891,7 @@ func handleListReports(s *Service, r *http.Request, req RPCRequest) (any, error) func handleGetReport(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetReportParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -923,7 +923,7 @@ func handleGetReport(s *Service, r *http.Request, req RPCRequest) (any, error) { func handleListWithdrawals(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListWithdrawalsParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -979,7 +979,7 @@ func handleListWithdrawals(s *Service, r *http.Request, req RPCRequest) (any, er func handleGetWithdrawal(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetWithdrawalParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -1010,7 +1010,7 @@ func handleGetWithdrawal(s *Service, r *http.Request, req RPCRequest) (any, erro func handleListTournaments(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListTournamentsParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -1092,7 +1092,7 @@ func handleListTournaments(s *Service, r *http.Request, req RPCRequest) (any, er func handleGetTournament(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetTournamentParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -1124,7 +1124,7 @@ func handleGetTournament(s *Service, r *http.Request, req RPCRequest) (any, erro func handleListCommitments(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListCommitmentsParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -1189,7 +1189,7 @@ func handleListCommitments(s *Service, r *http.Request, req RPCRequest) (any, er func handleGetCommitment(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetCommitmentParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -1232,7 +1232,7 @@ func handleGetCommitment(s *Service, r *http.Request, req RPCRequest) (any, erro func handleListMatches(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListMatchesParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -1297,7 +1297,7 @@ func handleListMatches(s *Service, r *http.Request, req RPCRequest) (any, error) func handleGetMatch(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetMatchParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -1337,7 +1337,7 @@ func handleGetMatch(s *Service, r *http.Request, req RPCRequest) (any, error) { func handleListMatchAdvances(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.ListMatchAdvancesParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } @@ -1401,7 +1401,7 @@ func handleListMatchAdvances(s *Service, r *http.Request, req RPCRequest) (any, func handleGetMatchAdvance(s *Service, r *http.Request, req RPCRequest) (any, error) { var params api.GetMatchAdvanceParams - if err := UnmarshalParams(req.Params, ¶ms); err != nil { + if err := api.UnmarshalParams(req.Params, ¶ms); err != nil { s.Logger.Debug("Invalid parameters", "err", err) return nil, newRPCError(JSONRPC_INVALID_PARAMS, "Invalid parameters") } diff --git a/internal/jsonrpc/types.go b/internal/jsonrpc/types.go index 00aa12f3d..55c3a3191 100644 --- a/internal/jsonrpc/types.go +++ b/internal/jsonrpc/types.go @@ -4,11 +4,9 @@ package jsonrpc import ( - "bytes" "encoding/json" "fmt" "io" - "reflect" "regexp" "github.com/cartesi/rollups-node/internal/config" @@ -71,52 +69,6 @@ func writeRPCResult(w io.Writer, id any, result any) error { return json.NewEncoder(w).Encode(resp) } -// UnmarshalParams supports both by-name (object) and by-position (array) parameter structures. -// If params is an object, it simply does json.Unmarshal; if it's an array, it will attempt -// to unmarshal each positional parameter into the target struct field in declaration order. -func UnmarshalParams(data json.RawMessage, target any) error { - data = bytes.TrimSpace(data) - if len(data) > 0 && data[0] == '[' { - // Unmarshal positional parameters into a slice of json.RawMessage. - var rawParams []json.RawMessage - if err := json.Unmarshal(data, &rawParams); err != nil { - return err - } - // Use reflection to set values in the target struct in the order they appear. - val := reflect.ValueOf(target) - if val.Kind() != reflect.Pointer || val.IsNil() { - return fmt.Errorf("error unmarshalling positional parameters target must be a non-nil pointer to a struct") - } - val = val.Elem() - if val.Kind() != reflect.Struct { - return fmt.Errorf("error unmarshalling positional parameters target must point to a struct") - } - typ := val.Type() - if len(rawParams) > typ.NumField() { - return fmt.Errorf("error unmarshalling positional parameters, expected %d params, got %d", - typ.NumField(), len(rawParams)) - } - // For each field in the struct, if a positional parameter exists, unmarshal that parameter. - for i := 0; i < typ.NumField() && i < len(rawParams); i++ { - sf := typ.Field(i) - if sf.Tag.Get("json") == "-" { - continue - } - field := val.Field(i) - if !field.CanSet() { - return fmt.Errorf("error unmarshalling positional parameter field %q is not settable", typ.Field(i).Name) - } - // Unmarshal the corresponding raw parameter into the field. - if err := json.Unmarshal(rawParams[i], field.Addr().Interface()); err != nil { - return fmt.Errorf("error unmarshalling positional parameter %d for field %s: %w", i, typ.Field(i).Name, err) - } - } - return nil - } - // Otherwise, assume by-name structure. - return json.Unmarshal(data, target) -} - // ----------------------------------------------------------------------------- // Validation helpers (server-only) // -----------------------------------------------------------------------------