Repository navigation
Expand file tree
/
Copy pathconfig.go
More file actions
4046 lines (3650 loc) · 186 KB
/
Copy pathconfig.go
File metadata and controls
4046 lines (3650 loc) · 186 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package common
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"maps"
"os"
"slices"
"time"
"strings"
"github.com/bytedance/sonic"
pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v3"
"github.com/erpc/erpc/telemetry"
"github.com/erpc/erpc/util"
"github.com/grafana/sobek"
"github.com/rs/zerolog"
"github.com/spf13/afero"
"gopkg.in/yaml.v3"
)
var (
// ErpcVersion is the version of eRPC, overridden at build time via ldflags.
// Default: "dev" for development builds.
// Build example: -ldflags="-X github.com/erpc/erpc/common.ErpcVersion=v1.0.0"
ErpcVersion = "dev"
// ErpcCommitSha is the git commit SHA, overridden at build time via ldflags.
// Default: "none" for development builds.
// Build example: -ldflags="-X github.com/erpc/erpc/common.ErpcCommitSha=abc123def"
ErpcCommitSha = "none"
TRUE = true
FALSE = false
)
// Config represents the configuration of the application.
type Config struct {
LogLevel string `yaml:"logLevel,omitempty" json:"logLevel" tstype:"LogLevel"`
ClusterKey string `yaml:"clusterKey,omitempty" json:"clusterKey"`
Server *ServerConfig `yaml:"server,omitempty" json:"server"`
HealthCheck *HealthCheckConfig `yaml:"healthCheck,omitempty" json:"healthCheck"`
Admin *AdminConfig `yaml:"admin,omitempty" json:"admin"`
Database *DatabaseConfig `yaml:"database,omitempty" json:"database"`
Projects []*ProjectConfig `yaml:"projects,omitempty" json:"projects"`
RateLimiters *RateLimiterConfig `yaml:"rateLimiters,omitempty" json:"rateLimiters"`
Metrics *MetricsConfig `yaml:"metrics,omitempty" json:"metrics"`
ProxyPools []*ProxyPoolConfig `yaml:"proxyPools,omitempty" json:"proxyPools"`
Tracing *TracingConfig `yaml:"tracing,omitempty" json:"tracing"`
// UserScript is the compiled program of the user's TS/JS config file
// (the WHOLE thing — imports, helpers, the createConfig call). Set
// by `loadConfigFromTypescript`; nil for YAML configs.
//
// Each policy-engine pool runtime runs this program once on primer
// (via the pool's primer hook), which:
// * Evaluates the user's helpers and imports natively in the
// runtime so closures referenced by `evalFunc` actually exist.
// * Populates `globalThis.__erpcFns` with the runtime-native
// function values discovered in the default export. The
// SelectionPolicy's `EvalFunc` carries the lookup ID
// (`__ts_fn__:fn_<n>`) rather than a stringified function
// source — so the function never round-trips through
// `.toString()` + recompile.
//
// Never serialized; opaque to YAML/JSON.
UserScript *sobek.Program `yaml:"-" json:"-"`
}
// LegacyTranslateFn is the post-decode migration hook invoked by
// LoadConfig before SetDefaults. nil = no migration (canonical configs
// only). cmd/erpc/main.go wires this to legacy.TranslateFromConfig
// during init; tests that exercise legacy YAML must set it explicitly.
//
// Decoupled via a package var to avoid a circular import: this package
// owns the runtime types, and common/legacy depends on those types to
// describe the legacy shape — so legacy imports common, not the other
// way around. The hook lets common stay free of the legacy package.
var LegacyTranslateFn func(*Config) ([]string, error)
// LegacyTranslateLogger lets the caller observe deprecation warnings
// emitted by LegacyTranslateFn. If nil, warnings are dropped silently.
var LegacyTranslateLogger func(warning string)
// LoadConfig loads the configuration from the specified file.
// It supports both YAML and TypeScript (.ts) files.
func LoadConfig(fs afero.Fs, filename string, opts *DefaultOptions) (*Config, error) {
data, err := afero.ReadFile(fs, filename)
if err != nil {
return nil, err
}
var cfg Config
if strings.HasSuffix(filename, ".ts") || strings.HasSuffix(filename, ".js") {
cfgPtr, err := loadConfigFromTypescript(filename)
if err != nil {
return nil, err
}
cfg = *cfgPtr
} else {
expandedData := []byte(os.ExpandEnv(string(data)))
decoder := yaml.NewDecoder(bytes.NewReader(expandedData))
decoder.KnownFields(true)
err = decoder.Decode(&cfg)
if err != nil {
return nil, err
}
}
if LegacyTranslateFn != nil {
warnings, err := LegacyTranslateFn(&cfg)
if err != nil {
return nil, fmt.Errorf("legacy config migration: %w", err)
}
if LegacyTranslateLogger != nil {
for _, w := range warnings {
LegacyTranslateLogger(w)
}
}
}
if err := cfg.SetDefaults(opts); err != nil {
return nil, err
}
if err := cfg.Validate(); err != nil {
return nil, err
}
return &cfg, nil
}
type ServerConfig struct {
ListenV4 *bool `yaml:"listenV4,omitempty" json:"listenV4"`
HttpHostV4 *string `yaml:"httpHostV4,omitempty" json:"httpHostV4"`
ListenV6 *bool `yaml:"listenV6,omitempty" json:"listenV6"`
HttpHostV6 *string `yaml:"httpHostV6,omitempty" json:"httpHostV6"`
HttpPort *int `yaml:"httpPort,omitempty" json:"httpPort"` // Deprecated: use HttpPortV4
HttpPortV4 *int `yaml:"httpPortV4,omitempty" json:"httpPortV4"`
HttpPortV6 *int `yaml:"httpPortV6,omitempty" json:"httpPortV6"`
GrpcEnabled *bool `yaml:"grpcEnabled,omitempty" json:"grpcEnabled"`
GrpcHostV4 *string `yaml:"grpcHostV4,omitempty" json:"grpcHostV4"`
GrpcPortV4 *int `yaml:"grpcPortV4,omitempty" json:"grpcPortV4"`
GrpcHostV6 *string `yaml:"grpcHostV6,omitempty" json:"grpcHostV6"`
GrpcPortV6 *int `yaml:"grpcPortV6,omitempty" json:"grpcPortV6"`
GrpcMaxRecvMsgSize *int `yaml:"grpcMaxRecvMsgSize,omitempty" json:"grpcMaxRecvMsgSize"`
GrpcMaxSendMsgSize *int `yaml:"grpcMaxSendMsgSize,omitempty" json:"grpcMaxSendMsgSize"`
GrpcReflection *bool `yaml:"grpcReflection,omitempty" json:"grpcReflection"`
MaxTimeout *Duration `yaml:"maxTimeout,omitempty" json:"maxTimeout" tstype:"Duration"`
ReadTimeout *Duration `yaml:"readTimeout,omitempty" json:"readTimeout" tstype:"Duration"`
WriteTimeout *Duration `yaml:"writeTimeout,omitempty" json:"writeTimeout" tstype:"Duration"`
EnableGzip *bool `yaml:"enableGzip,omitempty" json:"enableGzip"`
TLS *TLSConfig `yaml:"tls,omitempty" json:"tls"`
Aliasing *AliasingConfig `yaml:"aliasing" json:"aliasing"`
WaitBeforeShutdown *Duration `yaml:"waitBeforeShutdown,omitempty" json:"waitBeforeShutdown" tstype:"Duration"`
WaitAfterShutdown *Duration `yaml:"waitAfterShutdown,omitempty" json:"waitAfterShutdown" tstype:"Duration"`
IncludeErrorDetails *bool `yaml:"includeErrorDetails,omitempty" json:"includeErrorDetails"`
TrustedIPForwarders []string `yaml:"trustedIPForwarders,omitempty" json:"trustedIPForwarders"`
TrustedIPHeaders []string `yaml:"trustedIPHeaders,omitempty" json:"trustedIPHeaders"`
ResponseHeaders map[string]string `yaml:"responseHeaders,omitempty" json:"responseHeaders"`
// MaxRequestBodySize caps one HTTP request body in bytes, counted after
// gzip decompression (0 = unlimited). An over-limit body gets HTTP 413
// and none of it runs.
MaxRequestBodySize *int64 `yaml:"maxRequestBodySize,omitempty" json:"maxRequestBodySize"`
WebSocket *WebSocketServerConfig `yaml:"webSocket,omitempty" json:"webSocket"`
// ExecutionHeaders controls the per-request diagnostic headers
// (X-ERPC-Attempts, X-ERPC-Upstreams-Tried, etc.) that expose how
// eRPC routed and resolved each request. Defaults to "all" — set
// "summary" to keep only counters, or "off" to disable entirely
// (useful for low-latency / bandwidth-constrained clients).
ExecutionHeaders *ExecutionHeadersMode `yaml:"executionHeaders,omitempty" json:"executionHeaders" tstype:"ExecutionHeadersMode"`
// CostHeaders opts into the cost/billing response headers
// (X-ERPC-Calls, X-ERPC-Billable, X-ERPC-Methods, X-ERPC-Credits,
// X-ERPC-Credits-Version) on single and batch responses. Off by
// default. Credit-unit pricing is vendor-level configuration — see
// CreditUnitsProvider and UpstreamConfig.CreditUnits.
CostHeaders *bool `yaml:"costHeaders,omitempty" json:"costHeaders"`
}
type WebSocketServerConfig struct {
// Enabled accepts WebSocket upgrades on the HTTP port. Off by default:
// with it off an upgrade request is ordinary HTTP.
Enabled *bool `yaml:"enabled,omitempty" json:"enabled"`
ReadBufferSize int `yaml:"readBufferSize,omitempty" json:"readBufferSize"`
WriteBufferSize int `yaml:"writeBufferSize,omitempty" json:"writeBufferSize"`
MaxMessageSize int64 `yaml:"maxMessageSize,omitempty" json:"maxMessageSize"`
PingInterval *Duration `yaml:"pingInterval,omitempty" json:"pingInterval" tstype:"Duration"`
MaxSubscriptionsPerConnection int `yaml:"maxSubscriptionsPerConnection,omitempty" json:"maxSubscriptionsPerConnection"`
// MaxConcurrentRequestsPerConnection bounds the messages a connection has
// in flight; further frames wait (backpressure) instead of being rejected.
// A batch is one message; its requests run concurrently, as over HTTP.
MaxConcurrentRequestsPerConnection int `yaml:"maxConcurrentRequestsPerConnection,omitempty" json:"maxConcurrentRequestsPerConnection"`
// MaxConnections caps concurrent client connections on this instance
// (0 = unlimited). Unlike the per-IP cap it holds when client IPs are
// unknown or spoofed.
MaxConnections int `yaml:"maxConnections,omitempty" json:"maxConnections"`
// MaxConnectionsPerIP caps concurrent client connections from one
// resolved client IP (0 = unlimited); see trustedIPForwarders.
MaxConnectionsPerIP int `yaml:"maxConnectionsPerIP,omitempty" json:"maxConnectionsPerIP"`
// MaxSubscriptions caps active subscriptions across all connections on
// this instance (0 = unlimited).
MaxSubscriptions int `yaml:"maxSubscriptions,omitempty" json:"maxSubscriptions"`
}
// ExecutionHeadersMode controls how much per-request execution detail is
// exposed in HTTP response headers.
type ExecutionHeadersMode string
const (
// ExecutionHeadersAll emits the full set: counters + per-upstream
// trace (upstream IDs, outcomes, reasons, durations). Default.
ExecutionHeadersAll ExecutionHeadersMode = "all"
// ExecutionHeadersSummary emits only the counter triplet
// (X-ERPC-Attempts/Retries/Hedges) + the cache-hit / final-upstream
// markers. Skips the (potentially large) per-attempt slice headers.
ExecutionHeadersSummary ExecutionHeadersMode = "summary"
// ExecutionHeadersOff disables all X-ERPC-* diagnostic headers.
ExecutionHeadersOff ExecutionHeadersMode = "off"
)
type HealthCheckConfig struct {
Mode HealthCheckMode `yaml:"mode,omitempty" json:"mode"`
Auth *AuthConfig `yaml:"auth,omitempty" json:"auth"`
DefaultEval string `yaml:"defaultEval,omitempty" json:"defaultEval"`
}
type HealthCheckMode string
const (
HealthCheckModeSimple HealthCheckMode = "simple"
HealthCheckModeNetworks HealthCheckMode = "networks"
HealthCheckModeVerbose HealthCheckMode = "verbose"
)
const (
EvalAnyInitializedUpstreams = "any:initializedUpstreams"
EvalAnyErrorRateBelow90 = "any:errorRateBelow90"
EvalAllErrorRateBelow90 = "all:errorRateBelow90"
EvalAnyErrorRateBelow100 = "any:errorRateBelow100"
EvalAllErrorRateBelow100 = "all:errorRateBelow100"
EvalEvmAnyChainId = "any:evm:eth_chainId"
EvalEvmAllChainId = "all:evm:eth_chainId"
EvalAllActiveUpstreams = "all:activeUpstreams"
)
type TracingProtocol string
const (
TracingProtocolHttp TracingProtocol = "http"
TracingProtocolGrpc TracingProtocol = "grpc"
)
type TracingConfig struct {
Enabled bool `yaml:"enabled,omitempty" json:"enabled"`
Endpoint string `yaml:"endpoint,omitempty" json:"endpoint"`
Protocol TracingProtocol `yaml:"protocol,omitempty" json:"protocol"`
SampleRate float64 `yaml:"sampleRate,omitempty" json:"sampleRate"`
Detailed bool `yaml:"detailed,omitempty" json:"detailed"`
ServiceName string `yaml:"serviceName,omitempty" json:"serviceName"`
Headers map[string]string `yaml:"headers,omitempty" json:"headers"`
TLS *TLSConfig `yaml:"tls,omitempty" json:"tls"`
ResourceAttributes map[string]string `yaml:"resourceAttributes,omitempty" json:"resourceAttributes"`
// ForceTraceMatchers defines conditions for force-tracing requests.
// Each matcher can specify network and/or method patterns.
// Multiple patterns can be separated by "|" (OR within field).
// Both network and method must match if both are specified (AND between fields).
// If only one field is specified, only that field is checked.
ForceTraceMatchers []*ForceTraceMatcher `yaml:"forceTraceMatchers,omitempty" json:"forceTraceMatchers"`
}
// ForceTraceMatcher defines a condition for force-tracing requests.
type ForceTraceMatcher struct {
// Network patterns to match (e.g., "evm:1", "evm:1|evm:42161", "evm:*")
// Multiple patterns separated by "|" act as OR conditions.
Network string `yaml:"network,omitempty" json:"network"`
// Method patterns to match (e.g., "eth_call", "debug_*|trace_*")
// Multiple patterns separated by "|" act as OR conditions.
Method string `yaml:"method,omitempty" json:"method"`
}
type AdminConfig struct {
Auth *AuthConfig `yaml:"auth" json:"auth"`
CORS *CORSConfig `yaml:"cors" json:"cors"`
AllowMethods []string `yaml:"allowMethods,omitempty" json:"allowMethods,omitempty"`
DenyMethods []string `yaml:"denyMethods,omitempty" json:"denyMethods,omitempty"`
}
type AliasingConfig struct {
Rules []*AliasingRuleConfig `yaml:"rules" json:"rules"`
}
type AliasingRuleConfig struct {
MatchDomain string `yaml:"matchDomain" json:"matchDomain"`
ServeProject string `yaml:"serveProject" json:"serveProject"`
ServeArchitecture string `yaml:"serveArchitecture" json:"serveArchitecture"`
ServeChain string `yaml:"serveChain" json:"serveChain"`
}
type DatabaseConfig struct {
EvmJsonRpcCache *CacheConfig `yaml:"evmJsonRpcCache,omitempty" json:"evmJsonRpcCache"`
SvmJsonRpcCache *CacheConfig `yaml:"svmJsonRpcCache,omitempty" json:"svmJsonRpcCache"`
SharedState *SharedStateConfig `yaml:"sharedState,omitempty" json:"sharedState"`
}
type SharedStateConfig struct {
// ClusterKey identifies the logical group for shared counters across replicas (multi-tenant friendly)
ClusterKey string `yaml:"clusterKey,omitempty" json:"clusterKey"`
// Connector contains the storage driver configuration (redis, postgresql, dynamodb, memory)
Connector *ConnectorConfig `yaml:"connector,omitempty" json:"connector"`
// FallbackTimeout is the timeout for remote storage operations (get/set/publish).
// It is a seconds-scale network timeout and NOT a foreground latency budget.
FallbackTimeout Duration `yaml:"fallbackTimeout,omitempty" json:"fallbackTimeout" tstype:"Duration"`
// LockTtl is the expiration for the distributed lock key in the backing store.
// Should comfortably exceed the expected duration of remote writes.
LockTtl Duration `yaml:"lockTtl,omitempty" json:"lockTtl" tstype:"Duration"`
// LockMaxWait caps how long the foreground path will wait to acquire the lock
// before proceeding locally and deferring the remote write to background.
LockMaxWait Duration `yaml:"lockMaxWait,omitempty" json:"lockMaxWait" tstype:"Duration"`
// UpdateMaxWait caps how long the foreground path will spend computing a new value
// (e.g., polling latest block) before returning the current local value.
UpdateMaxWait Duration `yaml:"updateMaxWait,omitempty" json:"updateMaxWait" tstype:"Duration"`
}
// CachingStrategy controls write-side admission when combined with a response
// cacheability mark (see AllowsCacheWrite). Cache hits are served with zero
// re-validation, so modules that decide grades (consensus in v1) mark the
// response instead of teaching the cache policy names.
type CachingStrategy string
const (
CachingStrategyDefaultAllow CachingStrategy = "defaultAllow"
CachingStrategyDefaultIgnore CachingStrategy = "defaultIgnore"
)
type CacheConfig struct {
Connectors []*ConnectorConfig `yaml:"connectors,omitempty" json:"connectors" tstype:"TsConnectorConfig[]"`
Policies []*CachePolicyConfig `yaml:"policies,omitempty" json:"policies"`
Compression *CompressionConfig `yaml:"compression,omitempty" json:"compression"`
// CachingStrategy is the default write posture for unmarked responses.
// Omit → defaultAllow (today's open write). defaultIgnore writes only when
// a module marks the response cacheable (true).
CachingStrategy CachingStrategy `yaml:"cachingStrategy,omitempty" json:"cachingStrategy,omitempty" tstype:"'defaultAllow' | 'defaultIgnore'"`
}
// AllowsCacheWrite applies the strategy × mark decision table for Cache.Set.
// mark nil = undecided (no module voted); true/false = explicit vote.
func AllowsCacheWrite(strategy CachingStrategy, mark *bool) bool {
if strategy == "" {
strategy = CachingStrategyDefaultAllow
}
switch strategy {
case CachingStrategyDefaultIgnore:
return mark != nil && *mark
default: // defaultAllow and any unrecognized value treated as allow
return mark == nil || *mark
}
}
type CompressionConfig struct {
Enabled *bool `yaml:"enabled,omitempty" json:"enabled"`
Algorithm string `yaml:"algorithm,omitempty" json:"algorithm"` // "zstd" for now, can be extended
ZstdLevel string `yaml:"zstdLevel,omitempty" json:"zstdLevel"` // "fastest", "default", "better", "best"
Threshold int `yaml:"threshold,omitempty" json:"threshold"` // Minimum size in bytes to compress
}
type CacheMethodConfig struct {
ReqRefs [][]interface{} `yaml:"reqRefs" json:"reqRefs"`
RespRefs [][]interface{} `yaml:"respRefs" json:"respRefs"`
Finalized bool `yaml:"finalized" json:"finalized"`
Realtime bool `yaml:"realtime" json:"realtime"`
Stateful bool `yaml:"stateful,omitempty" json:"stateful"`
// TranslateLatestTag controls whether the method-level tag translation should convert "latest" to a concrete hex block number.
// When nil or true, translation is enabled by default.
TranslateLatestTag *bool `yaml:"translateLatestTag,omitempty" json:"translateLatestTag,omitempty"`
// TranslateFinalizedTag controls whether the method-level tag translation should convert "finalized" to a concrete hex block number.
// When nil or true, translation is enabled by default.
TranslateFinalizedTag *bool `yaml:"translateFinalizedTag,omitempty" json:"translateFinalizedTag,omitempty"`
// EnforceBlockAvailability controls whether per-upstream block availability bounds (upper/lower)
// are enforced for this method at the network level. When nil or true, enforcement is enabled.
EnforceBlockAvailability *bool `yaml:"enforceBlockAvailability,omitempty" json:"enforceBlockAvailability,omitempty"`
}
type CachePolicyConfig struct {
Connector string `yaml:"connector" json:"connector"`
Network string `yaml:"network,omitempty" json:"network"`
Method string `yaml:"method,omitempty" json:"method"`
Params []interface{} `yaml:"params,omitempty" json:"params"`
Finality DataFinalityState `yaml:"finality,omitempty" json:"finality" tstype:"DataFinalityState"`
Empty CacheEmptyBehavior `yaml:"empty,omitempty" json:"empty" tstype:"CacheEmptyBehavior"`
AppliesTo CachePolicyAppliesTo `yaml:"appliesTo,omitempty" json:"appliesTo" tstype:"'get' | 'set' | 'both'"`
MinItemSize *string `yaml:"minItemSize,omitempty" json:"minItemSize" tstype:"ByteSize"`
MaxItemSize *string `yaml:"maxItemSize,omitempty" json:"maxItemSize" tstype:"ByteSize"`
// TTL is either a fixed duration ("2s") or, in object form, derived from the
// network's estimated block time ({ blockTimeMultiplier: 1, fallback: 2s }).
// For realtime finality the resolved value is the age limit; the fixed/
// fallback component is also used as the cache storage expiry. See
// BlockTimeAdaptiveDuration.
TTL *BlockTimeAdaptiveDuration `yaml:"ttl,omitempty" json:"ttl,omitempty" tstype:"Duration | BlockTimeAdaptiveDuration"`
}
type ConnectorDriverType string
const (
DriverMemory ConnectorDriverType = "memory"
DriverRedis ConnectorDriverType = "redis"
DriverPostgreSQL ConnectorDriverType = "postgresql"
DriverDynamoDB ConnectorDriverType = "dynamodb"
DriverGrpc ConnectorDriverType = "grpc"
)
type ConnectorConfig struct {
Id string `yaml:"id,omitempty" json:"id"`
Driver ConnectorDriverType `yaml:"driver" json:"driver" tstype:"TsConnectorDriverType"`
// Tags label the data source a connector caches, so the `use-upstream`
// directive can gate which connector is read/written — e.g. a connector
// fed by a system-transaction node tagged `systx` is only used when the
// request's selector admits it. Same glob/`!negation` grammar as upstream
// tags. Untagged connectors are always eligible (a use-upstream pin meant
// for upstreams must not disable a normal cache).
Tags []string `yaml:"tags,omitempty" json:"tags,omitempty"`
Memory *MemoryConnectorConfig `yaml:"memory,omitempty" json:"memory"`
Redis *RedisConnectorConfig `yaml:"redis,omitempty" json:"redis"`
DynamoDB *DynamoDBConnectorConfig `yaml:"dynamodb,omitempty" json:"dynamodb"`
PostgreSQL *PostgreSQLConnectorConfig `yaml:"postgresql,omitempty" json:"postgresql"`
Grpc *GrpcConnectorConfig `yaml:"grpc,omitempty" json:"grpc"`
FailsafeForGets []*FailsafeConfig `yaml:"failsafeForGets,omitempty" json:"failsafeForGets"`
FailsafeForSets []*FailsafeConfig `yaml:"failsafeForSets,omitempty" json:"failsafeForSets"`
Mock *MockConnectorConfig `yaml:"-" json:"-"`
}
type GrpcConnectorConfig struct {
Bootstrap string `yaml:"bootstrap,omitempty" json:"bootstrap"`
Servers []string `yaml:"servers,omitempty" json:"servers"`
Headers map[string]string `yaml:"headers,omitempty" json:"headers"`
GetTimeout Duration `yaml:"getTimeout,omitempty" json:"getTimeout" tstype:"Duration"`
// NetworkId pins every server in this connector to one network and skips
// the eth_chainId bootstrap probe.
//
// Required for SVM. Solana has no numeric chain id — cluster identity is
// the genesis hash — and eth_chainId returns Unimplemented, so the probe
// fails and no client is ever registered. Probing GetGenesisHash instead
// is not an option either: the BDS reader deliberately does not publish
// one until it can verify its own, so identity here has to be asserted by
// configuration rather than discovered.
//
// Leave unset for EVM to keep the probe, which also arms the per-request
// chain-identity assertion that catches an endpoint cross-wired LATER — a
// static binding cannot.
NetworkId string `yaml:"networkId,omitempty" json:"networkId"`
// PoolSize is the number of independent gRPC connections opened to each
// backing server, selected round-robin per request. Larger values raise the
// concurrent-stream ceiling and shrink the blast radius of a single wedged
// connection, at the cost of more open connections per server. When unset
// (0) a built-in default is used.
PoolSize int `yaml:"poolSize,omitempty" json:"poolSize"`
// HealthCheckService is a grpc.health.v1 service name. When set, each
// connection watches it on every resolved address and sends requests only
// to addresses reporting SERVING. Empty (default) disables health checking.
HealthCheckService string `yaml:"healthCheckService,omitempty" json:"healthCheckService"`
}
type MemoryConnectorConfig struct {
MaxItems int `yaml:"maxItems" json:"maxItems"`
MaxTotalSize string `yaml:"maxTotalSize" json:"maxTotalSize"`
EmitMetrics *bool `yaml:"emitMetrics,omitempty" json:"emitMetrics,omitempty"`
}
type MockConnectorConfig struct {
MemoryConnectorConfig
GetDelay time.Duration
SetDelay time.Duration
GetErrorRate float64
SetErrorRate float64
}
type TLSConfig struct {
Enabled bool `yaml:"enabled" json:"enabled"`
CertFile string `yaml:"certFile" json:"certFile"`
KeyFile string `yaml:"keyFile" json:"keyFile"`
CAFile string `yaml:"caFile,omitempty" json:"caFile"`
InsecureSkipVerify bool `yaml:"insecureSkipVerify,omitempty" json:"insecureSkipVerify"`
}
type RedisConnectorConfig struct {
Addr string `yaml:"addr,omitempty" json:"addr"`
Username string `yaml:"username,omitempty" json:"username"`
Password string `yaml:"password,omitempty" json:"-"`
DB int `yaml:"db,omitempty" json:"db"`
TLS *TLSConfig `yaml:"tls,omitempty" json:"tls"`
ConnPoolSize int `yaml:"connPoolSize,omitempty" json:"connPoolSize"`
URI string `yaml:"uri" json:"uri"`
InitTimeout Duration `yaml:"initTimeout,omitempty" json:"initTimeout" tstype:"Duration"`
GetTimeout Duration `yaml:"getTimeout,omitempty" json:"getTimeout" tstype:"Duration"`
SetTimeout Duration `yaml:"setTimeout,omitempty" json:"setTimeout" tstype:"Duration"`
LockRetryInterval Duration `yaml:"lockRetryInterval,omitempty" json:"lockRetryInterval" tstype:"Duration"`
IAMAuth *RedisIAMAuthConfig `yaml:"iamAuth,omitempty" json:"iamAuth,omitempty"`
}
func (r *RedisConnectorConfig) MarshalJSON() ([]byte, error) {
return sonic.Marshal(map[string]interface{}{
"addr": r.Addr,
"username": r.Username,
"password": "REDACTED",
"db": r.DB,
"connPoolSize": r.ConnPoolSize,
"uri": util.RedactEndpoint(r.URI),
"tls": r.TLS,
"initTimeout": r.InitTimeout.String(),
"getTimeout": r.GetTimeout.String(),
"setTimeout": r.SetTimeout.String(),
"iamAuth": r.IAMAuth,
})
}
func (r *RedisConnectorConfig) MarshalYAML() (interface{}, error) {
return map[string]interface{}{
"addr": r.Addr,
"username": r.Username,
"password": "REDACTED",
"db": r.DB,
"connPoolSize": r.ConnPoolSize,
"uri": util.RedactEndpoint(r.URI),
"tls": r.TLS,
"initTimeout": r.InitTimeout.String(),
"getTimeout": r.GetTimeout.String(),
"setTimeout": r.SetTimeout.String(),
"lockRetryInterval": r.LockRetryInterval.String(),
"iamAuth": r.IAMAuth,
}, nil
}
type DynamoDBConnectorConfig struct {
Table string `yaml:"table,omitempty" json:"table"`
Region string `yaml:"region,omitempty" json:"region"`
Endpoint string `yaml:"endpoint,omitempty" json:"endpoint"`
Auth *AwsAuthConfig `yaml:"auth,omitempty" json:"auth"`
PartitionKeyName string `yaml:"partitionKeyName,omitempty" json:"partitionKeyName"`
RangeKeyName string `yaml:"rangeKeyName,omitempty" json:"rangeKeyName"`
ReverseIndexName string `yaml:"reverseIndexName,omitempty" json:"reverseIndexName"`
TTLAttributeName string `yaml:"ttlAttributeName,omitempty" json:"ttlAttributeName"`
InitTimeout Duration `yaml:"initTimeout,omitempty" json:"initTimeout" tstype:"Duration"`
GetTimeout Duration `yaml:"getTimeout,omitempty" json:"getTimeout" tstype:"Duration"`
SetTimeout Duration `yaml:"setTimeout,omitempty" json:"setTimeout" tstype:"Duration"`
MaxRetries int `yaml:"maxRetries,omitempty" json:"maxRetries"`
StatePollInterval Duration `yaml:"statePollInterval,omitempty" json:"statePollInterval" tstype:"Duration"`
LockRetryInterval Duration `yaml:"lockRetryInterval,omitempty" json:"lockRetryInterval" tstype:"Duration"`
}
type PostgreSQLConnectorConfig struct {
ConnectionUri string `yaml:"connectionUri" json:"connectionUri"`
Table string `yaml:"table" json:"table"`
MinConns int32 `yaml:"minConns,omitempty" json:"minConns"`
MaxConns int32 `yaml:"maxConns,omitempty" json:"maxConns"`
InitTimeout Duration `yaml:"initTimeout,omitempty" json:"initTimeout" tstype:"Duration"`
GetTimeout Duration `yaml:"getTimeout,omitempty" json:"getTimeout" tstype:"Duration"`
SetTimeout Duration `yaml:"setTimeout,omitempty" json:"setTimeout" tstype:"Duration"`
IAMAuth *PostgreSQLIAMAuthConfig `yaml:"iamAuth,omitempty" json:"iamAuth,omitempty"`
// SkipSchemaSetup skips all startup DDL (CREATE TABLE/INDEX, column
// migrations, pg_cron) and the local expired-row cleanup DELETE loop. Set
// it for connectors whose ConnectionUri targets a read-only replica (e.g.
// an Aurora global-database secondary): DDL cannot execute there (SQLSTATE
// 25006) and is not write-forwarded, so the writer-region connector owns
// the schema and the replica receives it via storage replication.
SkipSchemaSetup bool `yaml:"skipSchemaSetup,omitempty" json:"skipSchemaSetup"`
}
func (p *PostgreSQLConnectorConfig) MarshalJSON() ([]byte, error) {
return sonic.Marshal(map[string]interface{}{
"connectionUri": util.RedactEndpoint(p.ConnectionUri),
"table": p.Table,
"minConns": fmt.Sprintf("%d", p.MinConns),
"maxConns": fmt.Sprintf("%d", p.MaxConns),
"initTimeout": p.InitTimeout.String(),
"getTimeout": p.GetTimeout.String(),
"setTimeout": p.SetTimeout.String(),
"iamAuth": p.IAMAuth,
"skipSchemaSetup": p.SkipSchemaSetup,
})
}
func (p *PostgreSQLConnectorConfig) MarshalYAML() (interface{}, error) {
return map[string]interface{}{
"connectionUri": util.RedactEndpoint(p.ConnectionUri),
"table": p.Table,
"minConns": p.MinConns,
"maxConns": p.MaxConns,
"initTimeout": p.InitTimeout.String(),
"getTimeout": p.GetTimeout.String(),
"setTimeout": p.SetTimeout.String(),
"iamAuth": p.IAMAuth,
"skipSchemaSetup": p.SkipSchemaSetup,
}, nil
}
type AwsAuthConfig struct {
Mode string `yaml:"mode" json:"mode" tstype:"'file' | 'env' | 'secret'"` // "file", "env", "secret"
CredentialsFile string `yaml:"credentialsFile" json:"credentialsFile"`
Profile string `yaml:"profile" json:"profile"`
AccessKeyID string `yaml:"accessKeyID" json:"accessKeyID"`
SecretAccessKey string `yaml:"secretAccessKey" json:"secretAccessKey"`
}
func (a *AwsAuthConfig) MarshalJSON() ([]byte, error) {
return sonic.Marshal(map[string]interface{}{
"mode": a.Mode,
"credentialsFile": a.CredentialsFile,
"profile": a.Profile,
"accessKeyID": a.AccessKeyID,
"secretAccessKey": "REDACTED",
})
}
func (a *AwsAuthConfig) MarshalYAML() (interface{}, error) {
return map[string]interface{}{
"mode": a.Mode,
"credentialsFile": a.CredentialsFile,
"profile": a.Profile,
"accessKeyID": a.AccessKeyID,
"secretAccessKey": "REDACTED",
}, nil
}
// RedisIAMAuthConfig enables AWS IAM authentication for ElastiCache (Valkey ≥7.2
// or Redis OSS ≥7.0). When enabled, eRPC mints SigV4-presigned auth tokens via
// go-redis's CredentialsProviderContext on every new connection. TLS is required
// (auto-enabled by SetDefaults). For IAM-enabled ElastiCache users, the user
// name and user ID must be identical — supply that single value as UserID.
type RedisIAMAuthConfig struct {
Enabled bool `yaml:"enabled" json:"enabled"`
// CacheName is the ElastiCache replication-group ID.
// Will be lowercased automatically (AWS lowercases cache names at creation time).
CacheName string `yaml:"cacheName" json:"cacheName"`
// Region is optional — derived from AWS_REGION / instance metadata when omitted.
Region string `yaml:"region,omitempty" json:"region,omitempty"`
UserID string `yaml:"userID" json:"userID"`
// Auth selects the AWS credential source (same shape as DynamoDB's auth).
// Omit to use the default credential chain (instance role, env vars, …).
Auth *AwsAuthConfig `yaml:"auth,omitempty" json:"auth,omitempty"`
}
// PostgreSQLIAMAuthConfig enables AWS IAM authentication for RDS PostgreSQL.
// eRPC mints SigV4-presigned tokens via pgxpool.BeforeConnect on each new pool
// connection. SSL is required (auto-enforced via sslmode=require by SetDefaults).
type PostgreSQLIAMAuthConfig struct {
Enabled bool `yaml:"enabled" json:"enabled"`
// Endpoint is host:port of the RDS instance. If empty, derived from
// ConnectionUri at SetDefaults time.
Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty"`
// Region is optional — derived from AWS_REGION / instance metadata when omitted.
Region string `yaml:"region,omitempty" json:"region,omitempty"`
// DBUser is the database user mapped to the IAM role (must be granted rds_iam
// in PostgreSQL). If empty, derived from the user in ConnectionUri.
DBUser string `yaml:"dbUser,omitempty" json:"dbUser,omitempty"`
Auth *AwsAuthConfig `yaml:"auth,omitempty" json:"auth,omitempty"`
}
type ProjectConfig struct {
Id string `yaml:"id" json:"id"`
Auth *AuthConfig `yaml:"auth,omitempty" json:"auth"`
CORS *CORSConfig `yaml:"cors,omitempty" json:"cors"`
Providers []*ProviderConfig `yaml:"providers,omitempty" json:"providers"`
UpstreamDefaults *UpstreamConfig `yaml:"upstreamDefaults,omitempty" json:"upstreamDefaults"`
Upstreams []*UpstreamConfig `yaml:"upstreams,omitempty" json:"upstreams"`
NetworkDefaults *NetworkDefaults `yaml:"networkDefaults,omitempty" json:"networkDefaults"`
Networks []*NetworkConfig `yaml:"networks,omitempty" json:"networks"`
RateLimitBudget string `yaml:"rateLimitBudget,omitempty" json:"rateLimitBudget"`
// Configure user agent tracking at the project level
UserAgentMode UserAgentTrackingMode `yaml:"userAgentMode,omitempty" json:"userAgentMode"`
// TrustUserIdHeader makes erpc read the caller's user identity from the
// X-ERPC-User-Id request header (see common.HeaderUserId) and use it for the
// `user` metric/log label — but only when no auth strategy resolved a user
// (auth wins) and only for attribution (no rate-limit budget is derived).
// This is for deployments that authenticate callers in front of erpc (e.g. a
// gateway) and want per-user erpc telemetry without erpc performing auth.
// erpc does NOT validate the header, so enable this ONLY when erpc is reachable
// solely by a trusted proxy that sets the header and strips any client copy —
// otherwise callers can spoof their own attribution. Default false.
TrustUserIdHeader bool `yaml:"trustUserIdHeader,omitempty" json:"trustUserIdHeader"`
ForwardHeaders []string `yaml:"forwardHeaders,omitempty" json:"forwardHeaders"`
AllowClientDirectives *string `yaml:"allowClientDirectives,omitempty" json:"allowClientDirectives"`
IgnoreMethods []string `yaml:"ignoreMethods,omitempty" json:"ignoreMethods"`
AllowMethods []string `yaml:"allowMethods,omitempty" json:"allowMethods"`
// Integrity is the project-wide data-integrity configuration. It applies to
// all networks; each network may override it with its own integrity block.
Integrity *IntegrityConfig `yaml:"integrity,omitempty" json:"integrity,omitempty"`
// ScoreMetricsWindowSize is the tumbling window the per-upstream
// health tracker uses for its rolling counters (errorRate, p50/p70/
// p95 latency, throttledRate, misbehaviorRate). At each tick the
// counters reset and start re-accumulating, so this knob effectively
// controls how fast a degraded upstream's metrics start reflecting
// the new reality. Short windows (e.g. 30s) react quickly but give
// noisier ranking; long windows (5–10m) give stable averages but
// hide spikes for longer. Defaults to 10m when zero — production
// systems typically leave it default; the eRPC simulator overrides
// to 30s so knob changes show up in the UI within seconds.
ScoreMetricsWindowSize Duration `yaml:"scoreMetricsWindowSize,omitempty" json:"scoreMetricsWindowSize"`
// LegacyProject captures deprecated project-level scoring / routing
// keys that used to live directly on ProjectConfig. Consumed by the
// legacy translator hook in LoadConfig, then cleared. Never serialized.
LegacyProject *LegacyProjectFields `yaml:"-" json:"-"`
}
// LegacyProjectFields collects the deprecated project-level scoring +
// routing keys. The translator inspects these to synthesize a
// `selectionPolicy.eval` for each network and to emit deprecation
// warnings. All fields are zero-valued for a clean modern config.
//
// `scoreMetricsWindowSize` was previously listed here as inert; it is
// now a first-class field on ProjectConfig (above) — operators
// control the health tracker window directly.
type LegacyProjectFields struct {
RoutingStrategy string `yaml:"routingStrategy,omitempty"`
ScoreGranularity string `yaml:"scoreGranularity,omitempty"`
ScorePenaltyDecayRate float64 `yaml:"scorePenaltyDecayRate,omitempty"`
ScoreSwitchHysteresis float64 `yaml:"scoreSwitchHysteresis,omitempty"`
ScoreMinSwitchInterval Duration `yaml:"scoreMinSwitchInterval,omitempty"`
ScoreMetricsMode string `yaml:"scoreMetricsMode,omitempty"`
ScoreRefreshInterval Duration `yaml:"scoreRefreshInterval,omitempty"`
}
// UnmarshalYAML captures legacy project-level scoring / routing keys
// alongside the canonical schema. The legacy fields are stashed in
// LegacyProject; the load-time translator hook reads them and
// synthesizes the equivalent selectionPolicy.eval per network.
func (p *ProjectConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
type canonicalProjectConfig ProjectConfig
type shadow struct {
canonicalProjectConfig `yaml:",inline"`
LegacyProjectFields `yaml:",inline"`
}
var s shadow
if err := unmarshal(&s); err != nil {
return err
}
*p = ProjectConfig(s.canonicalProjectConfig)
if s.LegacyProjectFields != (LegacyProjectFields{}) {
lf := s.LegacyProjectFields
p.LegacyProject = &lf
}
return nil
}
// UserAgentTrackingMode controls how user agents are recorded for metrics/labels
type UserAgentTrackingMode string
const (
// UserAgentTrackingModeSimplified lowers cardinality by bucketing common user agents
UserAgentTrackingModeSimplified UserAgentTrackingMode = "simplified"
// UserAgentTrackingModeRaw records the user agent string as-is (high cardinality)
UserAgentTrackingModeRaw UserAgentTrackingMode = "raw"
)
// Removed legacy nested UserAgentConfig; use ProjectConfig.UserAgentMode
type NetworkDefaults struct {
RateLimitBudget string `yaml:"rateLimitBudget,omitempty" json:"rateLimitBudget"`
Failsafe []*FailsafeConfig `yaml:"failsafe,omitempty" json:"failsafe"`
SelectionPolicy *SelectionPolicyConfig `yaml:"selectionPolicy,omitempty" json:"selectionPolicy"`
DirectiveDefaults *DirectiveDefaultsConfig `yaml:"directiveDefaults,omitempty" json:"directiveDefaults"`
Evm *EvmNetworkConfig `yaml:"evm,omitempty" json:"evm" tstype:"TsEvmNetworkConfigForDefaults"`
Svm *SvmNetworkConfig `yaml:"svm,omitempty" json:"svm" tstype:"TsSvmNetworkConfigForDefaults"`
Multiplexing *bool `yaml:"multiplexing,omitempty" json:"multiplexing"`
CacheKeySuffix string `yaml:"cacheKeySuffix,omitempty" json:"cacheKeySuffix"`
Failover *FailoverConfig `yaml:"failover,omitempty" json:"failover"`
MaxClientBatchSize *int `yaml:"maxClientBatchSize,omitempty" json:"maxClientBatchSize"`
}
// FailoverConfig controls per-request escalation to fallback-tier upstreams,
// independent of the selection policy.
type FailoverConfig struct {
// OnDefaultsExhausted tries upstreams not tagged `tier:fallback` first and
// advances to the fallback tier within the same request only once every
// other upstream failed with an error retryable toward the network.
OnDefaultsExhausted *bool `yaml:"onDefaultsExhausted,omitempty" json:"onDefaultsExhausted"`
}
// Enabled reports whether any failover behaviour is configured.
func (f *FailoverConfig) Enabled() bool {
if f == nil {
return false
}
return f.OnDefaultsExhausted != nil && *f.OnDefaultsExhausted
}
// UnmarshalYAML provides backward compatibility for old single failsafe object format
func (n *NetworkDefaults) UnmarshalYAML(unmarshal func(interface{}) error) error {
// Define a type alias to avoid recursion
type rawNetworkDefaults NetworkDefaults
raw := (*rawNetworkDefaults)(n)
// Try unmarshaling normally first
err := unmarshal(raw)
if err == nil {
return nil
}
// Save the original error - it might be more informative
originalErr := err
// Check if the error is about unknown fields - if so, return it as is
// This preserves errors like "field maxCount not found in type"
errStr := err.Error()
if strings.Contains(errStr, "not found in type") ||
strings.Contains(errStr, "unknown field") {
return originalErr
}
// If that fails, try the old format with single failsafe object
type oldNetworkDefaults struct {
RateLimitBudget string `yaml:"rateLimitBudget,omitempty"`
Failsafe *FailsafeConfig `yaml:"failsafe,omitempty"`
SelectionPolicy *SelectionPolicyConfig `yaml:"selectionPolicy,omitempty"`
DirectiveDefaults *DirectiveDefaultsConfig `yaml:"directiveDefaults,omitempty"`
Evm *EvmNetworkConfig `yaml:"evm,omitempty"`
Svm *SvmNetworkConfig `yaml:"svm,omitempty"`
CacheKeySuffix string `yaml:"cacheKeySuffix,omitempty"`
Failover *FailoverConfig `yaml:"failover,omitempty"`
MaxClientBatchSize *int `yaml:"maxClientBatchSize,omitempty"`
}
var old oldNetworkDefaults
if err := unmarshal(&old); err != nil {
// If both formats fail, return the original error as it's likely more informative
// about the actual problem (like invalid field names)
return originalErr
}
// Convert old format to new format
n.RateLimitBudget = old.RateLimitBudget
n.CacheKeySuffix = old.CacheKeySuffix
n.SelectionPolicy = old.SelectionPolicy
n.DirectiveDefaults = old.DirectiveDefaults
n.Evm = old.Evm
n.Svm = old.Svm
n.Failover = old.Failover
n.MaxClientBatchSize = old.MaxClientBatchSize
if old.Failsafe != nil {
// Ensure MatchMethod has a default value for backward compatibility
if old.Failsafe.MatchMethod == "" {
old.Failsafe.MatchMethod = "*"
}
n.Failsafe = []*FailsafeConfig{old.Failsafe}
}
return nil
}
type CORSConfig struct {
AllowedOrigins []string `yaml:"allowedOrigins" json:"allowedOrigins"`
AllowedMethods []string `yaml:"allowedMethods" json:"allowedMethods"`
AllowedHeaders []string `yaml:"allowedHeaders" json:"allowedHeaders"`
ExposedHeaders []string `yaml:"exposedHeaders" json:"exposedHeaders"`
AllowCredentials *bool `yaml:"allowCredentials" json:"allowCredentials"`
MaxAge int `yaml:"maxAge" json:"maxAge"`
}
type VendorSettings map[string]interface{}
// CreditUnits extracts the `creditUnits` override dictionary from vendor
// settings (`providers[].settings.creditUnits`): JSON-RPC method → credit
// units, with "*" as the vendor's fallback for unlisted methods. YAML
// decodes numbers as int or float64 — both normalize to int64. Nil when
// absent or empty.
func (s VendorSettings) CreditUnits() map[string]int64 {
raw, ok := s["creditUnits"].(map[string]interface{})
if !ok {
return nil
}
out := make(map[string]int64, len(raw))
for method, v := range raw {
switch n := v.(type) {
case int:
out[method] = int64(n)
case int64:
out[method] = n
case float64:
out[method] = int64(n)
}
}
if len(out) == 0 {
return nil
}
return out
}
type ProviderConfig struct {
Id string `yaml:"id,omitempty" json:"id"`
Vendor string `yaml:"vendor" json:"vendor"`
Settings VendorSettings `yaml:"settings,omitempty" json:"settings"`
OnlyNetworks []string `yaml:"onlyNetworks,omitempty" json:"onlyNetworks"`
IgnoreNetworks []string `yaml:"ignoreNetworks,omitempty" json:"ignoreNetworks"`
UpstreamIdTemplate string `yaml:"upstreamIdTemplate,omitempty" json:"upstreamIdTemplate"`
Overrides map[string]*UpstreamConfig `yaml:"overrides,omitempty" json:"overrides"`
}
func (p *ProviderConfig) MarshalJSON() ([]byte, error) {
return sonic.Marshal(map[string]interface{}{
"id": p.Id,
"vendor": p.Vendor,
"settings": "REDACTED",
"onlyNetworks": p.OnlyNetworks,
"upstreamIdTemplate": p.UpstreamIdTemplate,
"overrides": p.Overrides,
})
}
func (p *ProviderConfig) MarshalYAML() (interface{}, error) {
return map[string]interface{}{
"id": p.Id,
"vendor": p.Vendor,
"settings": "REDACTED",
"onlyNetworks": p.OnlyNetworks,
"ignoreNetworks": p.IgnoreNetworks,
"upstreamIdTemplate": p.UpstreamIdTemplate,
"overrides": p.Overrides,
}, nil
}
// TagTierFallback marks an upstream as part of the fallback tier, used only
// when the other upstreams are unavailable.
const TagTierFallback = "tier:fallback"
// RateLimitCountMode selects the accounting unit an upstream's rate-limit
// budget charges per call.
type RateLimitCountMode string
const (
// RateLimitCountModeRequest (default) charges a flat 1 hit per call,
// regardless of method — the historical eRPC behavior.
RateLimitCountModeRequest RateLimitCountMode = "request"
// RateLimitCountModeCredit charges the request's resolved vendor
// credit-unit cost (the same table used for cost accounting), so a
// heavy eth_getLogs consumes more budget than a cheap eth_blockNumber.
// The pre-flight table estimate is used (the real cost is not known
// until after the call); a 0-CU method consumes nothing.
RateLimitCountModeCredit RateLimitCountMode = "credit"
)
type UpstreamConfig struct {
Id string `yaml:"id,omitempty" json:"id"`
Type UpstreamType `yaml:"type,omitempty" json:"type" tstype:"TsUpstreamType"`
// Tags is the single canonical user-applied label set. Convention is
// `<dimension>:<value>` so a single upstream can carry orthogonal labels:
//
// tags:
// - tier:main # used by .preferTag for tiering
// - region:us-east # used by .spreadAcrossTags('region:')
// - sequencer:op-base # shared-fate label
//
// Bare strings (no prefix) work too. Patterns supported by every
// stdlib tag method: glob (`*`, `?`) and `!negation`.
//
// Tier convention: `tier:fallback` declares a fallback-tier upstream
// (matches the long-standing eRPC convention; the default policy
// looks for `!tier:fallback` as the primary tier).
//
// The legacy `group: X` / `cohort: Y` YAML keys are accepted at
// load time only — UnmarshalYAML rewrites them as `tier:X` /
// `cohort:Y` tags and forgets them. There is no Go-level Group or
// Cohort field; programmatic code uses Tags directly.
Tags []string `yaml:"tags,omitempty" json:"tags,omitempty"`
VendorName string `yaml:"vendorName,omitempty" json:"vendorName"`
Endpoint string `yaml:"endpoint,omitempty" json:"endpoint"`
Evm *EvmUpstreamConfig `yaml:"evm,omitempty" json:"evm"`
Svm *SvmUpstreamConfig `yaml:"svm,omitempty" json:"svm"`
JsonRpc *JsonRpcUpstreamConfig `yaml:"jsonRpc,omitempty" json:"jsonRpc"`
Grpc *GrpcUpstreamConfig `yaml:"grpc,omitempty" json:"grpc"`
IgnoreMethods []string `yaml:"ignoreMethods,omitempty" json:"ignoreMethods"`
AllowMethods []string `yaml:"allowMethods,omitempty" json:"allowMethods"`
AutoIgnoreUnsupportedMethods *bool `yaml:"autoIgnoreUnsupportedMethods,omitempty" json:"autoIgnoreUnsupportedMethods"`
Failsafe []*FailsafeConfig `yaml:"failsafe,omitempty" json:"failsafe"`
RateLimitBudget string `yaml:"rateLimitBudget,omitempty" json:"rateLimitBudget"`
RateLimitAutoTune *RateLimitAutoTuneConfig `yaml:"rateLimitAutoTune,omitempty" json:"rateLimitAutoTune"`
// RateLimitCountMode selects how this upstream's rate-limit budget
// counts a call: "request" (default) charges 1 hit, "credit" charges the
// request's resolved vendor credit-unit cost. Empty resolves to
// "request". Applies to the upstream-level budget only.
RateLimitCountMode RateLimitCountMode `yaml:"rateLimitCountMode,omitempty" json:"rateLimitCountMode,omitempty"`
// CreditUnits overrides the vendor's built-in per-method credit table
// (CreditUnitsProvider) for this upstream, merged per method over the
// vendor defaults ("*" = fallback for unlisted methods). Normally set
// once per provider via `providers[].settings.creditUnits`, which is