Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion evmrpc/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Several `eth_` prefixed endpoints have a `sei_` prefixed counterpart. `eth_` end

The **`sei2`** namespace exposes the same **block** JSON-RPC shape as `sei` blocks, with **bank transfers** included in block payloads (HTTP only). There are seven `sei2_*` methods (block + block receipts + tx counts + `*ExcludeTraceFail` variants); there is no `sei2` transaction or filter API.

Legacy **`sei_*` and `sei2_*`** JSON-RPC (EVM HTTP only) are **gated** by the same `[evm].enabled_legacy_sei_apis` list in `app.toml` (after `deny_list`). Enforcement is **centralized** in `wrapSeiLegacyHTTP` (see `sei_legacy_http.go`): it inspects the JSON-RPC `method` field only. Wired from `HTTPServer.EnableRPC` via `HTTPConfig.SeiLegacyAllowlist` — handlers do not duplicate gate logic. Both surfaces are **deprecated** and scheduled for removal; **only methods named in that array** are allowed. `seid init` / `DefaultConfig` pre-fill the three `sei_*` address/Cosmos helpers; other gated methods (including `sei2_*`) appear **commented** in the generated template. **Docker localnet** (`docker/localnode/config/app.toml`) enables **all** gated methods except **`sei_sign`**. **HTTP 200** for all responses. **Disabled** methods return JSON-RPC `error` code `-32601`, `message` explains not enabled + deprecated, `data` `"legacy_sei_deprecated"`. **Allowed** single-object bodies pass through **unchanged**; JSON **batches** may be subset-forwarded with responses merged by `id`. Optional deprecation signal: HTTP header `Sei-Legacy-RPC-Deprecation` (`SeiLegacyDeprecationHTTPHeader` in `sei_legacy.go`). Coverage: `evmrpc/sei_legacy_test.go` and `integration_test/evm_module/rpc_io_test/testdata/sei_legacy_deprecation/*.iox`.
Legacy **`sei_*` and `sei2_*`** JSON-RPC (EVM HTTP only) are **gated** by the same `[evm].enabled_legacy_sei_apis` list in `app.toml` (after `deny_list`). Enforcement is **centralized** in `wrapSeiLegacyHTTP` (see `sei_legacy_http.go`): it inspects the JSON-RPC `method` field only. Wired from `HTTPServer.EnableRPC` via `HTTPConfig.SeiLegacyAllowlist` — handlers do not duplicate gate logic. Both surfaces are **deprecated** and scheduled for removal; **only methods named in that array** are allowed. `seid init` / `DefaultConfig` pre-fill the three `sei_*` address/Cosmos helpers; other gated methods (including `sei2_*`) appear **commented** in the generated template. **Docker localnet** (`docker/localnode/config/app.toml`) enables **all** gated methods except **`sei_sign`**. **HTTP 200** for all responses. **Disabled** methods return JSON-RPC `error` code `-32601`, `message` explains not enabled + deprecated, `data` `"legacy_sei_deprecated"`. **Allowed** single-object bodies pass through **unchanged**; JSON **batches** may be subset-forwarded with responses merged by `id` (for requests that include `id`). Per JSON-RPC 2.0, **notifications** (no `id` in the request) do not produce entries in the batch response array, so the merged array is **not** 1:1 with the request batch when notifications are present; if nothing would be returned, the gateway sends an **empty HTTP body** (not `[]`). Optional deprecation signal: HTTP header `Sei-Legacy-RPC-Deprecation` (`SeiLegacyDeprecationHTTPHeader` in `sei_legacy.go`). Coverage: `evmrpc/sei_legacy_test.go` and `integration_test/evm_module/rpc_io_test/testdata/sei_legacy_deprecation/*.iox`.

## `debug_` prefixed endpoints
`debug_trace*` endpoints should faithfully replay historical execution. If a transaction encountered an error during its actual execution, a `debug_trace*` call for it should reflect so. If a transction consumed X amount of gas during its actual execution, a `debug_trace*` call should show that exact amount as well.
Expand Down
149 changes: 70 additions & 79 deletions evmrpc/sei_legacy_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import (
// read more than the inner JSON-RPC stack will accept (see rpc.Server.SetHTTPBodyLimit).
const seiLegacyHTTPMaxBody = 5 * 1024 * 1024

const (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 👍🏻

invalidRequestCode = -32600
internalErrorCode = -32603
)

// wrapSeiLegacyHTTP wraps the EVM JSON-RPC HTTP handler to enforce [evm].enabled_legacy_sei_apis for
// gated sei_* and sei2_* methods. Disallowed calls get a JSON-RPC error without invoking the inner handler.
// Single-object allowed calls pass through unchanged; batches forward a filtered subset and merge inner
Expand Down Expand Up @@ -152,15 +157,8 @@ func (g *seiLegacyHTTPGate) handleBatch(w http.ResponseWriter, r *http.Request,
}
}
if len(forward) == 0 {
outArr := make([]json.RawMessage, len(msgs))
for i := range msgs {
if invalidReq[i] {
outArr[i] = json.RawMessage(marshalJSONRPCError(orNullID(ids[i]), -32600, seiLegacyBatchInvalidReqMsg))
continue
}
outArr[i] = json.RawMessage(marshalBlockedResponse(orNullID(ids[i]), blockedErr[i]))
}
writeJSONArrayResponse(w, http.StatusOK, outArr)
outArr := seiLegacyBatchResponsesNoForward(invalidReq, blockedErr, ids, len(msgs))
writeJSONRPCBatchResponse(w, http.StatusOK, outArr)
return
}
forwardBody, err := json.Marshal(forward)
Expand Down Expand Up @@ -194,15 +192,38 @@ func (g *seiLegacyHTTPGate) handleBatch(w http.ResponseWriter, r *http.Request,
if forwardLegacy {
w.Header().Set(SeiLegacyDeprecationHTTPHeader, SeiLegacyDeprecationMessage)
}
writeJSONArrayResponse(w, rec.Code, outArr)
writeJSONRPCBatchResponse(w, rec.Code, outArr)
}

const seiLegacyBatchInternalErr = "invalid or incomplete JSON-RPC batch response from server"

const seiLegacyBatchInvalidReqMsg = "Invalid Request"

// mergeSeiLegacyHTTPBatch builds one response per original batch index: gate errors, -32600 invalid slots,
// then inner results matched by id (notifications use leftover inner items in order).
// seiLegacyBatchResponsesNoForward builds the batch JSON-RPC response when nothing is forwarded to the
// inner server: invalid slots yield -32600, blocked gated methods yield gate errors, and notifications
// are omitted (JSON-RPC 2.0: no response for notifications, including in batches).
func seiLegacyBatchResponsesNoForward(
invalidReq []bool,
blockedErr []error,
ids []json.RawMessage,
lenMsgs int,
) []json.RawMessage {
out := make([]json.RawMessage, 0, lenMsgs)
for i := range lenMsgs {
if invalidReq[i] {
out = append(out, marshalJSONRPCError(orNullID(ids[i]), invalidRequestCode, seiLegacyBatchInvalidReqMsg))
continue
}
if isJSONRPCNotificationID(ids[i]) {
continue
}
out = append(out, marshalBlockedResponse(orNullID(ids[i]), blockedErr[i]))
}
return out
}

// mergeSeiLegacyHTTPBatch merges inner batch results with gate/invalid slots. Output is ordered like the
// original batch but omits entries for JSON-RPC notifications (no id), per JSON-RPC 2.0 batch rules.
func mergeSeiLegacyHTTPBatch(
invalidReq []bool,
blocked []bool,
Expand All @@ -211,38 +232,34 @@ func mergeSeiLegacyHTTPBatch(
lenMsgs int,
innerBody []byte,
) []json.RawMessage {
outArr := make([]json.RawMessage, lenMsgs)
var innerArr []json.RawMessage
if err := json.Unmarshal(innerBody, &innerArr); err != nil {
appendMergeFailure := func() []json.RawMessage {
out := make([]json.RawMessage, 0, lenMsgs)
for i := 0; i < lenMsgs; i++ {
switch {
case invalidReq[i]:
outArr[i] = json.RawMessage(marshalJSONRPCError(orNullID(ids[i]), -32600, seiLegacyBatchInvalidReqMsg))
out = append(out, json.RawMessage(marshalJSONRPCError(orNullID(ids[i]), invalidRequestCode, seiLegacyBatchInvalidReqMsg)))
case isJSONRPCNotificationID(ids[i]):
continue
case blocked[i]:
outArr[i] = json.RawMessage(marshalBlockedResponse(orNullID(ids[i]), blockedErr[i]))
out = append(out, json.RawMessage(marshalBlockedResponse(orNullID(ids[i]), blockedErr[i])))
default:
outArr[i] = json.RawMessage(marshalJSONRPCError(orNullID(ids[i]), -32603, seiLegacyBatchInternalErr))
out = append(out, json.RawMessage(marshalJSONRPCError(orNullID(ids[i]), internalErrorCode, seiLegacyBatchInternalErr)))
}
}
return outArr
return out
}

var innerArr []json.RawMessage
if err := json.Unmarshal(innerBody, &innerArr); err != nil {
return appendMergeFailure()
}

entries := make([]json.RawMessage, len(innerArr))
idToIdx := make(map[string]int, len(innerArr))
for j, raw := range innerArr {
idRaw, hasKey, err := jsonRPCObjectIDKey(raw)
if err != nil {
for i := range lenMsgs {
switch {
case invalidReq[i]:
outArr[i] = json.RawMessage(marshalJSONRPCError(orNullID(ids[i]), -32600, seiLegacyBatchInvalidReqMsg))
case blocked[i]:
outArr[i] = json.RawMessage(marshalBlockedResponse(orNullID(ids[i]), blockedErr[i]))
default:
outArr[i] = json.RawMessage(marshalJSONRPCError(orNullID(ids[i]), -32603, seiLegacyBatchInternalErr))
}
}
return outArr
return appendMergeFailure()
}
entries[j] = raw
if !hasKey || isJSONRPCNotificationID(idRaw) {
Expand All @@ -252,74 +269,38 @@ func mergeSeiLegacyHTTPBatch(
if firstIdx, ok := idToIdx[k]; ok {
// Duplicate id with different bodies: fail the merge.
if !bytes.Equal(entries[firstIdx], raw) {
for i := 0; i < lenMsgs; i++ {
switch {
case invalidReq[i]:
outArr[i] = json.RawMessage(marshalJSONRPCError(orNullID(ids[i]), -32600, seiLegacyBatchInvalidReqMsg))
case blocked[i]:
outArr[i] = json.RawMessage(marshalBlockedResponse(orNullID(ids[i]), blockedErr[i]))
default:
outArr[i] = json.RawMessage(marshalJSONRPCError(orNullID(ids[i]), -32603, seiLegacyBatchInternalErr))
}
}
return outArr
return appendMergeFailure()
}
continue
}
idToIdx[k] = j
}

out := make([]json.RawMessage, 0, lenMsgs)
used := make([]bool, len(innerArr))
for i := 0; i < lenMsgs; i++ {
if invalidReq[i] {
outArr[i] = json.RawMessage(marshalJSONRPCError(orNullID(ids[i]), -32600, seiLegacyBatchInvalidReqMsg))
continue
}
if blocked[i] {
outArr[i] = json.RawMessage(marshalBlockedResponse(orNullID(ids[i]), blockedErr[i]))
continue
}
if !isJSONRPCNotificationID(ids[i]) {
k := rpcIDKey(ids[i])
idx, ok := idToIdx[k]
if !ok || used[idx] {
outArr[i] = json.RawMessage(marshalJSONRPCError(orNullID(ids[i]), -32603, seiLegacyBatchInternalErr))
continue
}
used[idx] = true
outArr[i] = json.RawMessage(patchJSONRPCResponseIDIfNeeded(entries[idx], ids[i]))
out = append(out, json.RawMessage(marshalJSONRPCError(orNullID(ids[i]), invalidRequestCode, seiLegacyBatchInvalidReqMsg)))
continue
}
}

var fifo []int
for j := range entries {
if used[j] {
if isJSONRPCNotificationID(ids[i]) {
continue
}
fifo = append(fifo, j)
}

notifSlots := make([]int, 0)
for i := 0; i < lenMsgs; i++ {
if blocked[i] || invalidReq[i] || !isJSONRPCNotificationID(ids[i]) {
if blocked[i] {
out = append(out, json.RawMessage(marshalBlockedResponse(orNullID(ids[i]), blockedErr[i])))
continue
}
notifSlots = append(notifSlots, i)
}
fifoPos := 0
for _, i := range notifSlots {
if fifoPos >= len(fifo) {
outArr[i] = json.RawMessage(marshalJSONRPCError(orNullID(ids[i]), -32603, seiLegacyBatchInternalErr))
k := rpcIDKey(ids[i])
idx, ok := idToIdx[k]
if !ok || used[idx] {
out = append(out, json.RawMessage(marshalJSONRPCError(orNullID(ids[i]), internalErrorCode, seiLegacyBatchInternalErr)))
continue
}
idx := fifo[fifoPos]
fifoPos++
used[idx] = true
outArr[i] = json.RawMessage(patchJSONRPCResponseIDIfNeeded(entries[idx], ids[i]))
out = append(out, json.RawMessage(patchJSONRPCResponseIDIfNeeded(entries[idx], ids[i])))
}

return outArr
return out
}

func rpcIDKey(id json.RawMessage) string {
Expand Down Expand Up @@ -388,6 +369,16 @@ func copyHTTPHeader(dst, src http.Header) {
}
}

// writeJSONRPCBatchResponse writes a JSON-RPC batch response. Per JSON-RPC 2.0, if there are no
// response objects, the server must not return an empty JSON array — use an empty HTTP body instead.
func writeJSONRPCBatchResponse(w http.ResponseWriter, code int, arr []json.RawMessage) {
if len(arr) == 0 {
w.WriteHeader(code)
return
}
writeJSONArrayResponse(w, code, arr)
}

func writeJSONArrayResponse(w http.ResponseWriter, code int, arr []json.RawMessage) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
Expand All @@ -407,7 +398,7 @@ func marshalBlockedResponse(id json.RawMessage, gateErr error) []byte {
"jsonrpc": "2.0",
"id": id,
"error": map[string]interface{}{
"code": -32603,
"code": internalErrorCode,
"message": gateErr.Error(),
},
})
Expand Down
Loading
Loading