Skip to content
Open
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
8 changes: 7 additions & 1 deletion clients/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ func (manager *ClientRegistry) CreateClient(appCtx context.Context, ups common.U
var c ClientInterface
var cerr error
switch cfg.Type {
case common.UpstreamTypeEvm:
case common.UpstreamTypeEvm, common.UpstreamTypeJsonRpc:
// jsonrpc architecture reuses the generic HTTP/WS JSON-RPC clients
// (passthrough; no EVM chainId/state poller). gRPC BDS remains EVM-only.
switch parsedUrl.Scheme {
case "http", "https":
c, cerr = NewGenericHttpJsonRpcClient(
Expand Down Expand Up @@ -126,6 +128,10 @@ func (manager *ClientRegistry) CreateClient(appCtx context.Context, ups common.U
cerr = fmt.Errorf("failed to create WebSocket client for upstream %v: %w", cfg.Id, cerr)
}
case "grpc", "grpc+bds":
if cfg.Type == common.UpstreamTypeJsonRpc {
cerr = fmt.Errorf("unsupported endpoint scheme: %v for upstream type jsonrpc: %v", parsedUrl.Scheme, cfg.Id)
break
}
c, cerr = NewGrpcBdsClient(
appCtx,
&lg,
Expand Down
12 changes: 12 additions & 0 deletions common/architecture_jsonrpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package common

const (
UpstreamTypeJsonRpc UpstreamType = "jsonrpc"
)

// JsonRpcNetworkConfig identifies a non-EVM JSON-RPC network (Solana, Starknet, …).
// Network id becomes jsonrpc:<id>. No chainId / state poller / EVM method hooks.
type JsonRpcNetworkConfig struct {
// Id is a stable slug (usually the CLL alias), e.g. solana-mainnet.
Id string `yaml:"id" json:"id"`
}
63 changes: 63 additions & 0 deletions common/architecture_jsonrpc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package common

import (
"testing"

"gopkg.in/yaml.v3"
)

func TestJsonRpcNetworkId(t *testing.T) {
n := &NetworkConfig{
Architecture: ArchitectureJsonRpc,
Alias: "solana-mainnet",
JsonRpc: &JsonRpcNetworkConfig{Id: "solana-mainnet"},
}
if got := n.NetworkId(); got != "jsonrpc:solana-mainnet" {
t.Fatalf("NetworkId()=%q", got)
}
if !IsValidArchitecture(string(ArchitectureJsonRpc)) {
t.Fatal("ArchitectureJsonRpc should be valid")
}
if !IsValidNetwork("jsonrpc:solana-mainnet") {
t.Fatal("jsonrpc:solana-mainnet should be valid")
}
if IsValidNetwork("jsonrpc:") || IsValidNetwork("jsonrpc:a:b") {
t.Fatal("invalid jsonrpc ids accepted")
}
}

func TestJsonRpcNetworkConfig_UnmarshalYAML_FailsafeObject(t *testing.T) {
// Chart emits failsafe as a single object (not a list). NetworkConfig must
// still accept architecture/jsonRpc via the oldNetworkConfig fallback.
const raw = `
architecture: jsonrpc
alias: solana-mainnet
jsonRpc:
id: solana-mainnet
failsafe:
timeout:
duration: 30s
retry:
maxAttempts: 5
delay: 50ms
`
var n NetworkConfig
if err := yaml.Unmarshal([]byte(raw), &n); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if n.Architecture != ArchitectureJsonRpc {
t.Fatalf("architecture=%q", n.Architecture)
}
if n.JsonRpc == nil || n.JsonRpc.Id != "solana-mainnet" {
t.Fatalf("jsonRpc=%v", n.JsonRpc)
}
if n.Alias != "solana-mainnet" {
t.Fatalf("alias=%q", n.Alias)
}
if len(n.Failsafe) != 1 || n.Failsafe[0].Timeout == nil {
t.Fatalf("failsafe not converted from object: %+v", n.Failsafe)
}
if got := n.NetworkId(); got != "jsonrpc:solana-mainnet" {
t.Fatalf("NetworkId()=%q", got)
}
}
18 changes: 16 additions & 2 deletions common/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -1123,6 +1123,9 @@ type JsonRpcUpstreamConfig struct {
EnableGzip *bool `yaml:"enableGzip,omitempty" json:"enableGzip"`
Headers map[string]string `yaml:"headers,omitempty" json:"headers"`
ProxyPool string `yaml:"proxyPool,omitempty" json:"proxyPool"`
// NetworkId binds this upstream to architecture jsonrpc (slug only, e.g. solana-mainnet).
// Required when type is jsonrpc; skipped for EVM upstreams that already use jsonRpc for batch/headers.
NetworkId string `yaml:"networkId,omitempty" json:"networkId,omitempty"`
}

func (c *JsonRpcUpstreamConfig) Copy() *JsonRpcUpstreamConfig {
Expand Down Expand Up @@ -2013,6 +2016,7 @@ type NetworkConfig struct {
RateLimitBudget string `yaml:"rateLimitBudget,omitempty" json:"rateLimitBudget"`
Failsafe []*FailsafeConfig `yaml:"failsafe,omitempty" json:"failsafe"`
Evm *EvmNetworkConfig `yaml:"evm,omitempty" json:"evm"`
JsonRpc *JsonRpcNetworkConfig `yaml:"jsonRpc,omitempty" json:"jsonRpc"`
SelectionPolicy *SelectionPolicyConfig `yaml:"selectionPolicy,omitempty" json:"selectionPolicy"`
DirectiveDefaults *DirectiveDefaultsConfig `yaml:"directiveDefaults,omitempty" json:"directiveDefaults"`
Alias string `yaml:"alias,omitempty" json:"alias"`
Expand Down Expand Up @@ -2084,6 +2088,7 @@ func (n *NetworkConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
RateLimitBudget string `yaml:"rateLimitBudget,omitempty"`
Failsafe *FailsafeConfig `yaml:"failsafe,omitempty"`
Evm *EvmNetworkConfig `yaml:"evm,omitempty"`
JsonRpc *JsonRpcNetworkConfig `yaml:"jsonRpc,omitempty"`
SelectionPolicy *SelectionPolicyConfig `yaml:"selectionPolicy,omitempty"`
DirectiveDefaults *DirectiveDefaultsConfig `yaml:"directiveDefaults,omitempty"`
Alias string `yaml:"alias,omitempty"`
Expand All @@ -2102,6 +2107,7 @@ func (n *NetworkConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
n.Architecture = old.Architecture
n.RateLimitBudget = old.RateLimitBudget
n.Evm = old.Evm
n.JsonRpc = old.JsonRpc
n.SelectionPolicy = old.SelectionPolicy
n.DirectiveDefaults = old.DirectiveDefaults
n.Alias = old.Alias
Expand Down Expand Up @@ -2562,13 +2568,21 @@ type RateLimitStoreConfig struct {
}

func (c *NetworkConfig) NetworkId() string {
if c.Architecture == "" || c.Evm == nil {
if c.Architecture == "" {
return ""
}

switch c.Architecture {
case "evm":
case ArchitectureEvm:
if c.Evm == nil {
return ""
}
return util.EvmNetworkId(c.Evm.ChainId)
case ArchitectureJsonRpc:
if c.JsonRpc == nil || c.JsonRpc.Id == "" {
return ""
}
return util.JsonRpcNetworkId(c.JsonRpc.Id)
default:
return ""
}
Expand Down
25 changes: 21 additions & 4 deletions common/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -1646,8 +1646,17 @@ func (u *UpstreamConfig) SetDefaults(defaults *UpstreamConfig) error {
}
}
if u.Type == "" {
// TODO make actual calls to detect other types (solana, btc, etc)?
u.Type = UpstreamTypeEvm
if u.JsonRpc != nil && u.JsonRpc.NetworkId != "" {
u.Type = UpstreamTypeJsonRpc
} else {
// TODO make actual calls to detect other types (solana, btc, etc)?
u.Type = UpstreamTypeEvm
}
}
if u.Type == UpstreamTypeJsonRpc {
if u.JsonRpc == nil {
u.JsonRpc = &JsonRpcUpstreamConfig{}
}
}

if len(u.Failsafe) > 0 {
Expand Down Expand Up @@ -1940,13 +1949,21 @@ func (n *NetworkConfig) SetDefaults(upstreams []*UpstreamConfig, defaults *Netwo

if n.Architecture == "" {
if n.Evm != nil {
n.Architecture = "evm"
n.Architecture = ArchitectureEvm
} else if n.JsonRpc != nil && n.JsonRpc.Id != "" {
n.Architecture = ArchitectureJsonRpc
}
}

if n.Architecture == "evm" && n.Evm == nil {
if n.Architecture == ArchitectureEvm && n.Evm == nil {
n.Evm = &EvmNetworkConfig{}
}
if n.Architecture == ArchitectureJsonRpc && n.JsonRpc == nil {
n.JsonRpc = &JsonRpcNetworkConfig{}
}
if n.Architecture == ArchitectureJsonRpc && n.JsonRpc.Id == "" && n.Alias != "" {
n.JsonRpc.Id = n.Alias
}

// Apply methods defaults
if n.Methods == nil {
Expand Down
14 changes: 12 additions & 2 deletions common/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import (
type NetworkArchitecture string

const (
ArchitectureEvm NetworkArchitecture = "evm"
ArchitectureEvm NetworkArchitecture = "evm"
ArchitectureJsonRpc NetworkArchitecture = "jsonrpc"
)

type Network interface {
Expand All @@ -33,7 +34,12 @@ type Network interface {
}

func IsValidArchitecture(architecture string) bool {
return architecture == string(ArchitectureEvm) // TODO add more architectures when they are supported
switch NetworkArchitecture(architecture) {
case ArchitectureEvm, ArchitectureJsonRpc:
return true
default:
return false
}
}

func IsValidNetwork(network string) bool {
Expand All @@ -44,6 +50,10 @@ func IsValidNetwork(network string) bool {
}
return chainId > 0
}
if strings.HasPrefix(network, "jsonrpc:") {
id := strings.TrimPrefix(network, "jsonrpc:")
return id != "" && !strings.Contains(id, ":")
}

return false
}
Expand Down
18 changes: 17 additions & 1 deletion common/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,14 @@ func (u *UpstreamConfig) Validate(c *Config, skipEndpointCheck bool) error {
if !skipEndpointCheck && u.Endpoint == "" {
return fmt.Errorf("upstream.*.endpoint is required")
}
if u.Type == UpstreamTypeJsonRpc {
if u.JsonRpc == nil || u.JsonRpc.NetworkId == "" {
return fmt.Errorf("upstream.*.jsonRpc.networkId is required for type jsonrpc")
}
if !util.IsValidIdentifier(u.JsonRpc.NetworkId) {
return fmt.Errorf("upstream.*.jsonRpc.networkId '%s' is invalid", u.JsonRpc.NetworkId)
}
}
if u.Evm != nil {
if err := u.Evm.Validate(u); err != nil {
return err
Expand Down Expand Up @@ -1274,9 +1282,17 @@ func (n *NetworkConfig) Validate(c *Config) error {
if n.Architecture == "" {
return fmt.Errorf("network.*.architecture is required")
}
if n.Architecture == "evm" && n.Evm == nil {
if n.Architecture == ArchitectureEvm && n.Evm == nil {
return fmt.Errorf("network.*.evm is required for evm networks")
}
if n.Architecture == ArchitectureJsonRpc {
if n.JsonRpc == nil || n.JsonRpc.Id == "" {
return fmt.Errorf("network.*.jsonRpc.id is required for jsonrpc networks")
}
if !util.IsValidIdentifier(n.JsonRpc.Id) {
return fmt.Errorf("network.*.jsonRpc.id '%s' must contain only alphanumeric characters, dash, or underscore", n.JsonRpc.Id)
}
}
if n.Evm != nil {
if err := n.Evm.Validate(); err != nil {
return err
Expand Down
11 changes: 11 additions & 0 deletions erpc/networks.go
Original file line number Diff line number Diff line change
Expand Up @@ -1392,6 +1392,17 @@ func (n *Network) prepareRequest(ctx context.Context, nr *common.NormalizedReque
)
}
evm.NormalizeHttpJsonRpc(ctx, nr, jsonRpcReq)
case common.ArchitectureJsonRpc:
// Passthrough: parse/validate JSON-RPC envelope only — no EVM method hooks.
if _, err := nr.JsonRpcRequest(ctx); err != nil {
return common.NewErrJsonRpcExceptionInternal(
0,
common.JsonRpcErrorParseException,
"failed to unmarshal json-rpc request",
err,
nil,
)
}
default:
return common.NewErrJsonRpcExceptionInternal(
0,
Expand Down
9 changes: 8 additions & 1 deletion erpc/networks_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,10 +309,12 @@ func (nr *NetworksRegistry) prepareNetwork(nwCfg *common.NetworkConfig) (*Networ
}

switch nwCfg.Architecture {
case "evm":
case common.ArchitectureEvm:
if nr.evmJsonRpcCache != nil {
network.cacheDal = nr.evmJsonRpcCache.WithProjectId(nr.project.Config.Id)
}
case common.ArchitectureJsonRpc:
// No architecture-specific cache yet; failsafe + metrics still apply.
default:
return nil, errors.New("unknown network architecture")
}
Expand Down Expand Up @@ -363,6 +365,11 @@ func (nr *NetworksRegistry) resolveNetworkConfig(networkId string) (*common.Netw
return nil, e
}
nwCfg.Evm = &common.EvmNetworkConfig{ChainId: int64(c)}
case common.ArchitectureJsonRpc:
if !util.IsValidIdentifier(s[1]) {
return nil, fmt.Errorf("invalid jsonrpc network id: %s", networkId)
}
nwCfg.JsonRpc = &common.JsonRpcNetworkConfig{Id: s[1]}
}
if err := nwCfg.SetDefaults(prj.Config.Upstreams, prj.Config.NetworkDefaults); err != nil {
return nil, fmt.Errorf("failed to set defaults for network config: %w", err)
Expand Down
2 changes: 2 additions & 0 deletions upstream/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,8 @@ func (u *UpstreamsRegistry) buildUpstreamBootstrapTask(upsCfg *common.UpstreamCo
taskName := fmt.Sprintf("upstream/%s", cfg.Id)
if cfg.Evm != nil && cfg.Evm.ChainId > 0 {
taskName = fmt.Sprintf("network/%s/upstream/%s", util.EvmNetworkId(cfg.Evm.ChainId), cfg.Id)
} else if cfg.Type == common.UpstreamTypeJsonRpc && cfg.JsonRpc != nil && cfg.JsonRpc.NetworkId != "" {
taskName = fmt.Sprintf("network/%s/upstream/%s", util.JsonRpcNetworkId(cfg.JsonRpc.NetworkId), cfg.Id)
}
return util.NewBootstrapTask(
taskName,
Expand Down
8 changes: 8 additions & 0 deletions upstream/upstream.go
Original file line number Diff line number Diff line change
Expand Up @@ -1487,6 +1487,14 @@ func (u *Upstream) detectFeatures(ctx context.Context) error {

// TODO evm: check trace methods availability (by engine? erigon/geth/etc)
// TODO evm: detect max eth_getLogs max block range
} else if cfg.Type == common.UpstreamTypeJsonRpc {
if cfg.JsonRpc == nil || cfg.JsonRpc.NetworkId == "" {
return common.NewTaskFatal(fmt.Errorf("upstream.*.jsonRpc.networkId is required for type jsonrpc"))
}
if !util.IsValidIdentifier(cfg.JsonRpc.NetworkId) {
return common.NewTaskFatal(fmt.Errorf("upstream.*.jsonRpc.networkId '%s' is invalid", cfg.JsonRpc.NetworkId))
}
u.networkId.Store(util.JsonRpcNetworkId(cfg.JsonRpc.NetworkId))
} else {
return fmt.Errorf("upstream type not supported: %s", cfg.Type)
}
Expand Down
8 changes: 8 additions & 0 deletions util/ids.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ func EvmNetworkId(chainId interface{}) string {
return fmt.Sprintf("evm:%d", chainId)
}

func JsonRpcNetworkId(id string) string {
return fmt.Sprintf("jsonrpc:%s", id)
}

var validIdentifierRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)

func IsValidIdentifier(s string) bool {
Expand All @@ -23,6 +27,10 @@ func IsValidNetworkId(s string) bool {
_, err := strconv.Atoi(s[4:])
return err == nil
}
if strings.HasPrefix(s, "jsonrpc:") {
id := s[len("jsonrpc:"):]
return id != "" && IsValidIdentifier(id)
}
return false
}

Expand Down
Loading