From 4ab9874f1353d921745953e4613e00307461729e Mon Sep 17 00:00:00 2001 From: Florian Date: Sun, 25 Jan 2026 14:57:47 +0100 Subject: [PATCH 01/16] feat(sqd): add provider for portal wrapper --- thirdparty/sqd.go | 92 +++++++++++ thirdparty/sqd_chain_mapping.go | 72 +++++++++ thirdparty/sqd_chain_mapping_test.go | 99 ++++++++++++ thirdparty/sqd_test.go | 227 +++++++++++++++++++++++++++ thirdparty/vendors_registry.go | 1 + 5 files changed, 491 insertions(+) create mode 100644 thirdparty/sqd.go create mode 100644 thirdparty/sqd_chain_mapping.go create mode 100644 thirdparty/sqd_chain_mapping_test.go create mode 100644 thirdparty/sqd_test.go diff --git a/thirdparty/sqd.go b/thirdparty/sqd.go new file mode 100644 index 000000000..ffe484937 --- /dev/null +++ b/thirdparty/sqd.go @@ -0,0 +1,92 @@ +package thirdparty + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/erpc/erpc/common" + "github.com/rs/zerolog" +) + +type SqdVendor struct { + common.Vendor +} + +func CreateSqdVendor() common.Vendor { + return &SqdVendor{} +} + +func (v *SqdVendor) Name() string { + return "sqd" +} + +func (v *SqdVendor) OwnsUpstream(ups *common.UpstreamConfig) bool { + return ups.VendorName == v.Name() +} + +func (v *SqdVendor) SupportsNetwork(ctx context.Context, logger *zerolog.Logger, settings common.VendorSettings, networkId string) (bool, error) { + if !strings.HasPrefix(networkId, "evm:") { + return false, nil + } + + chainID, err := strconv.ParseInt(strings.TrimPrefix(networkId, "evm:"), 10, 64) + if err != nil { + return false, err + } + + if dataset, ok := sqdDatasetFromSettings(settings, chainID); ok && dataset != "" { + return true, nil + } + + _, exists := sqdChainToDataset[chainID] + return exists, nil +} + +func (v *SqdVendor) GenerateConfigs(ctx context.Context, logger *zerolog.Logger, upstream *common.UpstreamConfig, settings common.VendorSettings) ([]*common.UpstreamConfig, error) { + if upstream.JsonRpc == nil { + upstream.JsonRpc = &common.JsonRpcUpstreamConfig{} + } + + if upstream.Evm == nil { + return nil, fmt.Errorf("sqd vendor requires upstream.evm to be defined") + } + + if upstream.Evm.ChainId == 0 { + return nil, fmt.Errorf("sqd vendor requires upstream.evm.chainId to be defined") + } + + if upstream.Endpoint == "" { + return nil, fmt.Errorf("sqd vendor requires upstream.endpoint to be defined (portal wrapper)") + } + + upstream.Type = common.UpstreamTypeEvm + upstream.VendorName = v.Name() + + if upstream.IgnoreMethods == nil { + upstream.IgnoreMethods = []string{"*"} + } + if upstream.AllowMethods == nil { + upstream.AllowMethods = []string{ + "eth_chainId", + "eth_blockNumber", + "eth_getBlockByNumber", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getLogs", + "trace_block", + } + } + + if logger != nil { + logger.Debug().Int64("chainId", upstream.Evm.ChainId).Interface("upstream", upstream).Msg("generated upstream from sqd provider") + } + + return []*common.UpstreamConfig{upstream}, nil +} + +func (v *SqdVendor) GetVendorSpecificErrorIfAny(req *common.NormalizedRequest, resp *http.Response, jrr interface{}, details map[string]interface{}) error { + // Wrapper returns standard JSON-RPC errors; let generic normalization handle them. + return nil +} diff --git a/thirdparty/sqd_chain_mapping.go b/thirdparty/sqd_chain_mapping.go new file mode 100644 index 000000000..c7dfdc2f3 --- /dev/null +++ b/thirdparty/sqd_chain_mapping.go @@ -0,0 +1,72 @@ +package thirdparty + +import ( + "strconv" + + "github.com/erpc/erpc/common" +) + +// sqdChainToDataset maps EVM chainId to SQD Portal dataset name. +// Dataset slug is the last segment of the Portal gateway URL. +// Kept for supported-chain detection and vendor settings overrides. +var sqdChainToDataset = map[int64]string{ + // Mainnets + 1: "ethereum-mainnet", + 10: "optimism-mainnet", + 56: "binance-mainnet", + 100: "gnosis-mainnet", + 137: "polygon-mainnet", + 250: "fantom-mainnet", + 324: "zksync-mainnet", + 8453: "base-mainnet", + 42161: "arbitrum-one", + 42170: "arbitrum-nova", + 43114: "avalanche-mainnet", + 59144: "linea-mainnet", + 534352: "scroll-mainnet", + 81457: "blast-mainnet", + 7777777: "zora-mainnet", + + // Testnets (commonly used) + 11155111: "ethereum-sepolia", + 84532: "base-sepolia", + 421614: "arbitrum-sepolia", + 11155420: "optimism-sepolia", +} + +// SqdSupportedChainIds returns a slice of all supported chain IDs. +func SqdSupportedChainIds() []int64 { + chains := make([]int64, 0, len(sqdChainToDataset)) + for chainId := range sqdChainToDataset { + chains = append(chains, chainId) + } + return chains +} + +func sqdDatasetFromSettings(settings common.VendorSettings, chainId int64) (string, bool) { + if settings == nil { + return "", false + } + + if dataset, ok := settings["dataset"].(string); ok && dataset != "" { + return dataset, true + } + + if raw, ok := settings["datasetByChainId"]; ok { + switch m := raw.(type) { + case map[string]interface{}: + if ds, ok := m[strconv.FormatInt(chainId, 10)].(string); ok && ds != "" { + return ds, true + } + case map[interface{}]interface{}: + if ds, ok := m[chainId].(string); ok && ds != "" { + return ds, true + } + if ds, ok := m[strconv.FormatInt(chainId, 10)].(string); ok && ds != "" { + return ds, true + } + } + } + + return "", false +} diff --git a/thirdparty/sqd_chain_mapping_test.go b/thirdparty/sqd_chain_mapping_test.go new file mode 100644 index 000000000..d0cfbe1c6 --- /dev/null +++ b/thirdparty/sqd_chain_mapping_test.go @@ -0,0 +1,99 @@ +package thirdparty + +import ( + "testing" + + "github.com/erpc/erpc/common" + "github.com/erpc/erpc/util" +) + +func init() { + util.ConfigureTestLogger() +} + +func TestSqdChainToDataset(t *testing.T) { + tests := []struct { + name string + chainId int64 + expected string + }{ + {"ethereum mainnet", 1, "ethereum-mainnet"}, + {"polygon mainnet", 137, "polygon-mainnet"}, + {"arbitrum one", 42161, "arbitrum-one"}, + {"base mainnet", 8453, "base-mainnet"}, + {"optimism mainnet", 10, "optimism-mainnet"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dataset, ok := sqdChainToDataset[tt.chainId] + if !ok { + t.Fatalf("chain %d not found in sqdChainToDataset", tt.chainId) + } + if dataset != tt.expected { + t.Errorf("sqdChainToDataset[%d] = %q, want %q", tt.chainId, dataset, tt.expected) + } + }) + } +} + +func TestSqdSupportedChainIds(t *testing.T) { + chainIds := SqdSupportedChainIds() + + if len(chainIds) != len(sqdChainToDataset) { + t.Errorf("SqdSupportedChainIds() returned %d chains, want %d", len(chainIds), len(sqdChainToDataset)) + } + + // Verify all returned chain IDs exist in the map + for _, chainId := range chainIds { + if _, ok := sqdChainToDataset[chainId]; !ok { + t.Errorf("SqdSupportedChainIds() returned chainId %d which is not in sqdChainToDataset", chainId) + } + } + + // Verify all map keys are in the returned slice + chainIdSet := make(map[int64]bool) + for _, id := range chainIds { + chainIdSet[id] = true + } + for chainId := range sqdChainToDataset { + if !chainIdSet[chainId] { + t.Errorf("chainId %d from sqdChainToDataset is missing from SqdSupportedChainIds()", chainId) + } + } +} + +func TestSqdChainToDataset_UnknownChain(t *testing.T) { + unknownChainIds := []int64{999999, 0, -1, 123456789} + + for _, chainId := range unknownChainIds { + if dataset, ok := sqdChainToDataset[chainId]; ok { + t.Errorf("unexpected mapping for unknown chain %d: %q", chainId, dataset) + } + } +} + +func TestSqdDatasetFromSettings_MapInterfaceInterface(t *testing.T) { + settings := common.VendorSettings{ + "datasetByChainId": map[interface{}]interface{}{ + int64(1): "ethereum-mainnet", + "10": "optimism-mainnet", + }, + } + + dataset, ok := sqdDatasetFromSettings(settings, 1) + if !ok { + t.Fatalf("expected dataset for chain 1") + } + if dataset != "ethereum-mainnet" { + t.Errorf("dataset = %q, want ethereum-mainnet", dataset) + } + + dataset, ok = sqdDatasetFromSettings(settings, 10) + if !ok { + t.Fatalf("expected dataset for chain 10") + } + if dataset != "optimism-mainnet" { + t.Errorf("dataset = %q, want optimism-mainnet", dataset) + } +} diff --git a/thirdparty/sqd_test.go b/thirdparty/sqd_test.go new file mode 100644 index 000000000..adb1f686d --- /dev/null +++ b/thirdparty/sqd_test.go @@ -0,0 +1,227 @@ +package thirdparty + +import ( + "context" + "testing" + + "github.com/erpc/erpc/common" + "github.com/erpc/erpc/util" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func init() { + util.ConfigureTestLogger() +} + +func TestSqdVendor_Name(t *testing.T) { + v := CreateSqdVendor() + assert.Equal(t, "sqd", v.Name()) +} + +func TestSqdVendor_OwnsUpstream(t *testing.T) { + v := CreateSqdVendor() + + tests := []struct { + name string + upstream *common.UpstreamConfig + expected bool + }{ + { + name: "vendorName sqd", + upstream: &common.UpstreamConfig{Type: common.UpstreamTypeEvm, VendorName: "sqd"}, + expected: true, + }, + { + name: "endpoint with portal.sqd.dev but no vendorName", + upstream: &common.UpstreamConfig{Endpoint: "https://portal.sqd.dev/datasets/ethereum-mainnet"}, + expected: false, + }, + { + name: "other vendor", + upstream: &common.UpstreamConfig{Type: common.UpstreamTypeEvm, VendorName: "alchemy"}, + expected: false, + }, + { + name: "empty config", + upstream: &common.UpstreamConfig{}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, v.OwnsUpstream(tt.upstream)) + }) + } +} + +func TestSqdVendor_SupportsNetwork(t *testing.T) { + v := CreateSqdVendor() + ctx := context.Background() + logger := zerolog.Nop() + + tests := []struct { + name string + networkId string + settings common.VendorSettings + expected bool + wantErr bool + }{ + { + name: "ethereum mainnet supported", + networkId: "evm:1", + settings: nil, + expected: true, + }, + { + name: "polygon mainnet supported", + networkId: "evm:137", + settings: nil, + expected: true, + }, + { + name: "unsupported chainId", + networkId: "evm:999999", + settings: nil, + expected: false, + }, + { + name: "non-evm network not supported", + networkId: "solana:mainnet", + settings: nil, + expected: false, + }, + { + name: "invalid network format", + networkId: "invalid", + settings: nil, + expected: false, + }, + { + name: "unsupported chainId with dataset override", + networkId: "evm:999999", + settings: common.VendorSettings{"dataset": "custom-dataset"}, + expected: true, + }, + { + name: "unsupported chainId with datasetByChainId override", + networkId: "evm:999999", + settings: common.VendorSettings{"datasetByChainId": map[string]interface{}{"999999": "custom-dataset"}}, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + supported, err := v.SupportsNetwork(ctx, &logger, tt.settings, tt.networkId) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.expected, supported) + }) + } +} + +func TestSqdVendor_GenerateConfigs(t *testing.T) { + v := CreateSqdVendor() + ctx := context.Background() + logger := zerolog.Nop() + + t.Run("fails when endpoint missing", func(t *testing.T) { + upstream := &common.UpstreamConfig{ + Id: "test-sqd", + Type: common.UpstreamTypeEvm, + Evm: &common.EvmUpstreamConfig{ChainId: 1}, + } + + _, err := v.GenerateConfigs(ctx, &logger, upstream, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "upstream.endpoint") + }) + + t.Run("preserves pre-defined endpoint", func(t *testing.T) { + upstream := &common.UpstreamConfig{ + Id: "test-sqd", + Type: common.UpstreamTypeEvm, + Endpoint: "https://portal-wrapper.internal/v1/evm/1", + Evm: &common.EvmUpstreamConfig{ChainId: 1}, + } + + configs, err := v.GenerateConfigs(ctx, &logger, upstream, nil) + assert.NoError(t, err) + assert.Len(t, configs, 1) + assert.Equal(t, "https://portal-wrapper.internal/v1/evm/1", configs[0].Endpoint) + assert.Equal(t, "sqd", configs[0].VendorName) + assert.Equal(t, common.UpstreamTypeEvm, configs[0].Type) + }) + + t.Run("sets ignoreMethods and allowMethods", func(t *testing.T) { + upstream := &common.UpstreamConfig{ + Id: "test-sqd", + Type: common.UpstreamTypeEvm, + Endpoint: "https://portal-wrapper.internal/v1/evm/1", + Evm: &common.EvmUpstreamConfig{ChainId: 1}, + } + + configs, err := v.GenerateConfigs(ctx, &logger, upstream, nil) + assert.NoError(t, err) + assert.Equal(t, []string{"*"}, configs[0].IgnoreMethods) + assert.Contains(t, configs[0].AllowMethods, "eth_getBlockByNumber") + assert.Contains(t, configs[0].AllowMethods, "eth_getLogs") + assert.Contains(t, configs[0].AllowMethods, "trace_block") + }) + + t.Run("preserves user-defined ignoreMethods", func(t *testing.T) { + upstream := &common.UpstreamConfig{ + Id: "test-sqd", + Type: common.UpstreamTypeEvm, + Endpoint: "https://portal-wrapper.internal/v1/evm/1", + Evm: &common.EvmUpstreamConfig{ChainId: 1}, + IgnoreMethods: []string{"eth_call"}, + } + + configs, err := v.GenerateConfigs(ctx, &logger, upstream, nil) + assert.NoError(t, err) + assert.Equal(t, []string{"eth_call"}, configs[0].IgnoreMethods) + }) + + t.Run("preserves user-defined allowMethods", func(t *testing.T) { + upstream := &common.UpstreamConfig{ + Id: "test-sqd", + Type: common.UpstreamTypeEvm, + Endpoint: "https://portal-wrapper.internal/v1/evm/1", + Evm: &common.EvmUpstreamConfig{ChainId: 1}, + AllowMethods: []string{"eth_getBlockByNumber"}, + } + + configs, err := v.GenerateConfigs(ctx, &logger, upstream, nil) + assert.NoError(t, err) + assert.Equal(t, []string{"eth_getBlockByNumber"}, configs[0].AllowMethods) + }) + + t.Run("fails when evm config is nil", func(t *testing.T) { + upstream := &common.UpstreamConfig{ + Id: "test-sqd", + Type: common.UpstreamTypeEvm, + } + + _, err := v.GenerateConfigs(ctx, &logger, upstream, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "upstream.evm") + }) + + t.Run("fails when chainId is zero", func(t *testing.T) { + upstream := &common.UpstreamConfig{ + Id: "test-sqd", + Type: common.UpstreamTypeEvm, + Evm: &common.EvmUpstreamConfig{ChainId: 0}, + } + + _, err := v.GenerateConfigs(ctx, &logger, upstream, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "chainId") + }) +} diff --git a/thirdparty/vendors_registry.go b/thirdparty/vendors_registry.go index 4979e6218..42b163e13 100644 --- a/thirdparty/vendors_registry.go +++ b/thirdparty/vendors_registry.go @@ -30,6 +30,7 @@ func NewVendorsRegistry() *VendorsRegistry { r.Register(CreateBlockPiVendor()) r.Register(CreateAnkrVendor()) r.Register(CreateRoutemeshVendor()) + r.Register(CreateSqdVendor()) return r } From f56866ecbec61a241a22362bab7968af2a7104df Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 27 Jan 2026 16:33:26 +0100 Subject: [PATCH 02/16] feat: add sqd settings for default datasets and api key --- thirdparty/sqd.go | 22 +++++++++++++++ thirdparty/sqd_chain_mapping.go | 29 ++++++++++++++++++++ thirdparty/sqd_test.go | 48 +++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+) diff --git a/thirdparty/sqd.go b/thirdparty/sqd.go index ffe484937..0fbbc58cb 100644 --- a/thirdparty/sqd.go +++ b/thirdparty/sqd.go @@ -41,6 +41,10 @@ func (v *SqdVendor) SupportsNetwork(ctx context.Context, logger *zerolog.Logger, return true, nil } + if !sqdUseDefaultDatasets(settings) { + return false, nil + } + _, exists := sqdChainToDataset[chainID] return exists, nil } @@ -79,6 +83,24 @@ func (v *SqdVendor) GenerateConfigs(ctx context.Context, logger *zerolog.Logger, } } + if settings != nil { + if apiKey, ok := settings["wrapperApiKey"].(string); ok && apiKey != "" { + header := "X-API-Key" + if headerOverride, ok := settings["wrapperApiKeyHeader"].(string); ok && headerOverride != "" { + header = headerOverride + } + if upstream.JsonRpc == nil { + upstream.JsonRpc = &common.JsonRpcUpstreamConfig{} + } + if upstream.JsonRpc.Headers == nil { + upstream.JsonRpc.Headers = make(map[string]string) + } + if _, exists := upstream.JsonRpc.Headers[header]; !exists { + upstream.JsonRpc.Headers[header] = apiKey + } + } + } + if logger != nil { logger.Debug().Int64("chainId", upstream.Evm.ChainId).Interface("upstream", upstream).Msg("generated upstream from sqd provider") } diff --git a/thirdparty/sqd_chain_mapping.go b/thirdparty/sqd_chain_mapping.go index c7dfdc2f3..d80d8e3cc 100644 --- a/thirdparty/sqd_chain_mapping.go +++ b/thirdparty/sqd_chain_mapping.go @@ -70,3 +70,32 @@ func sqdDatasetFromSettings(settings common.VendorSettings, chainId int64) (stri return "", false } + +func sqdUseDefaultDatasets(settings common.VendorSettings) bool { + if settings == nil { + return true + } + + raw, ok := settings["useDefaultDatasets"] + if !ok { + return true + } + + switch val := raw.(type) { + case bool: + return val + case string: + parsed, err := strconv.ParseBool(val) + if err == nil { + return parsed + } + case int: + return val != 0 + case int64: + return val != 0 + case float64: + return val != 0 + } + + return true +} diff --git a/thirdparty/sqd_test.go b/thirdparty/sqd_test.go index adb1f686d..9bf968cfd 100644 --- a/thirdparty/sqd_test.go +++ b/thirdparty/sqd_test.go @@ -80,6 +80,12 @@ func TestSqdVendor_SupportsNetwork(t *testing.T) { settings: nil, expected: true, }, + { + name: "default datasets disabled rejects known chain", + networkId: "evm:1", + settings: common.VendorSettings{"useDefaultDatasets": false}, + expected: false, + }, { name: "unsupported chainId", networkId: "evm:999999", @@ -110,6 +116,15 @@ func TestSqdVendor_SupportsNetwork(t *testing.T) { settings: common.VendorSettings{"datasetByChainId": map[string]interface{}{"999999": "custom-dataset"}}, expected: true, }, + { + name: "default datasets disabled with datasetByChainId override", + networkId: "evm:1", + settings: common.VendorSettings{ + "useDefaultDatasets": false, + "datasetByChainId": map[string]interface{}{"1": "ethereum-mainnet"}, + }, + expected: true, + }, } for _, tt := range tests { @@ -174,6 +189,39 @@ func TestSqdVendor_GenerateConfigs(t *testing.T) { assert.Contains(t, configs[0].AllowMethods, "trace_block") }) + t.Run("sets wrapper api key header", func(t *testing.T) { + upstream := &common.UpstreamConfig{ + Id: "test-sqd", + Type: common.UpstreamTypeEvm, + Endpoint: "https://portal-wrapper.internal/v1/evm/1", + Evm: &common.EvmUpstreamConfig{ChainId: 1}, + } + + configs, err := v.GenerateConfigs(ctx, &logger, upstream, common.VendorSettings{ + "wrapperApiKey": "secret", + }) + assert.NoError(t, err) + assert.Equal(t, "secret", configs[0].JsonRpc.Headers["X-API-Key"]) + }) + + t.Run("preserves existing wrapper header", func(t *testing.T) { + upstream := &common.UpstreamConfig{ + Id: "test-sqd", + Type: common.UpstreamTypeEvm, + Endpoint: "https://portal-wrapper.internal/v1/evm/1", + Evm: &common.EvmUpstreamConfig{ChainId: 1}, + JsonRpc: &common.JsonRpcUpstreamConfig{ + Headers: map[string]string{"X-API-Key": "existing"}, + }, + } + + configs, err := v.GenerateConfigs(ctx, &logger, upstream, common.VendorSettings{ + "wrapperApiKey": "secret", + }) + assert.NoError(t, err) + assert.Equal(t, "existing", configs[0].JsonRpc.Headers["X-API-Key"]) + }) + t.Run("preserves user-defined ignoreMethods", func(t *testing.T) { upstream := &common.UpstreamConfig{ Id: "test-sqd", From 3a99d7929e625b0601a01ac7b04cf8d246ceffb2 Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 2 Feb 2026 12:30:06 +0100 Subject: [PATCH 03/16] fix: harden sqd settings parsing --- thirdparty/sqd.go | 49 +++++++- thirdparty/sqd_chain_mapping.go | 161 ++++++++++++++++++++++++++- thirdparty/sqd_chain_mapping_test.go | 63 +++++++++-- thirdparty/sqd_test.go | 51 +++++++++ 4 files changed, 308 insertions(+), 16 deletions(-) diff --git a/thirdparty/sqd.go b/thirdparty/sqd.go index 0fbbc58cb..29f846dcd 100644 --- a/thirdparty/sqd.go +++ b/thirdparty/sqd.go @@ -69,6 +69,12 @@ func (v *SqdVendor) GenerateConfigs(ctx context.Context, logger *zerolog.Logger, upstream.Type = common.UpstreamTypeEvm upstream.VendorName = v.Name() + if dataset, ok := sqdDatasetForChain(settings, upstream.Evm.ChainId); ok { + if endpoint, updated := sqdApplyDatasetToEndpoint(upstream.Endpoint, dataset); updated { + upstream.Endpoint = endpoint + } + } + if upstream.IgnoreMethods == nil { upstream.IgnoreMethods = []string{"*"} } @@ -89,9 +95,6 @@ func (v *SqdVendor) GenerateConfigs(ctx context.Context, logger *zerolog.Logger, if headerOverride, ok := settings["wrapperApiKeyHeader"].(string); ok && headerOverride != "" { header = headerOverride } - if upstream.JsonRpc == nil { - upstream.JsonRpc = &common.JsonRpcUpstreamConfig{} - } if upstream.JsonRpc.Headers == nil { upstream.JsonRpc.Headers = make(map[string]string) } @@ -109,6 +112,46 @@ func (v *SqdVendor) GenerateConfigs(ctx context.Context, logger *zerolog.Logger, } func (v *SqdVendor) GetVendorSpecificErrorIfAny(req *common.NormalizedRequest, resp *http.Response, jrr interface{}, details map[string]interface{}) error { + if resp == nil { + return nil + } + + switch resp.StatusCode { + case http.StatusUnauthorized, http.StatusForbidden: + return common.NewErrEndpointUnauthorized(fmt.Errorf("sqd portal wrapper unauthorized: %d", resp.StatusCode)) + case http.StatusTooManyRequests: + return common.NewErrEndpointCapacityExceeded(fmt.Errorf("sqd portal wrapper rate limited: %d", resp.StatusCode)) + case http.StatusPaymentRequired: + return common.NewErrEndpointBillingIssue(fmt.Errorf("sqd portal wrapper billing issue: %d", resp.StatusCode)) + } + // Wrapper returns standard JSON-RPC errors; let generic normalization handle them. return nil } + +func sqdDatasetForChain(settings common.VendorSettings, chainId int64) (string, bool) { + if dataset, ok := sqdDatasetFromSettings(settings, chainId); ok { + return dataset, true + } + if !sqdUseDefaultDatasets(settings) { + return "", false + } + if dataset, ok := sqdChainToDataset[chainId]; ok { + return dataset, true + } + return "", false +} + +func sqdApplyDatasetToEndpoint(endpoint string, dataset string) (string, bool) { + if dataset == "" { + return endpoint, false + } + if strings.Contains(endpoint, "{dataset}") { + return strings.ReplaceAll(endpoint, "{dataset}", dataset), true + } + trimmed := strings.TrimRight(endpoint, "/") + if strings.HasSuffix(strings.ToLower(trimmed), "/datasets") { + return trimmed + "/" + dataset, true + } + return endpoint, false +} diff --git a/thirdparty/sqd_chain_mapping.go b/thirdparty/sqd_chain_mapping.go index d80d8e3cc..ee6b0be16 100644 --- a/thirdparty/sqd_chain_mapping.go +++ b/thirdparty/sqd_chain_mapping.go @@ -1,9 +1,12 @@ package thirdparty import ( + "fmt" "strconv" + "strings" "github.com/erpc/erpc/common" + "github.com/rs/zerolog/log" ) // sqdChainToDataset maps EVM chainId to SQD Portal dataset name. @@ -54,17 +57,44 @@ func sqdDatasetFromSettings(settings common.VendorSettings, chainId int64) (stri if raw, ok := settings["datasetByChainId"]; ok { switch m := raw.(type) { + case map[string]string: + if ds, ok := sqdDatasetFromStringKeyMapString(m, chainId); ok { + return ds, true + } case map[string]interface{}: - if ds, ok := m[strconv.FormatInt(chainId, 10)].(string); ok && ds != "" { + if ds, ok := sqdDatasetFromStringKeyMap(m, chainId); ok { return ds, true } - case map[interface{}]interface{}: - if ds, ok := m[chainId].(string); ok && ds != "" { + case map[int]string: + if ds, ok := sqdDatasetFromIntKeyMapString(m, chainId); ok { + return ds, true + } + case map[int]interface{}: + if ds, ok := sqdDatasetFromIntKeyMap(m, chainId); ok { + return ds, true + } + case map[int64]string: + if ds, ok := sqdDatasetFromInt64KeyMapString(m, chainId); ok { + return ds, true + } + case map[int64]interface{}: + if ds, ok := sqdDatasetFromInt64KeyMap(m, chainId); ok { return ds, true } - if ds, ok := m[strconv.FormatInt(chainId, 10)].(string); ok && ds != "" { + case map[float64]string: + if ds, ok := sqdDatasetFromFloatKeyMapString(m, chainId); ok { return ds, true } + case map[float64]interface{}: + if ds, ok := sqdDatasetFromFloatKeyMap(m, chainId); ok { + return ds, true + } + case map[interface{}]interface{}: + if ds, ok := sqdDatasetFromInterfaceKeyMap(m, chainId); ok { + return ds, true + } + default: + log.Warn().Str("type", fmt.Sprintf("%T", raw)).Msg("sqd datasetByChainId unsupported type") } } @@ -85,17 +115,136 @@ func sqdUseDefaultDatasets(settings common.VendorSettings) bool { case bool: return val case string: - parsed, err := strconv.ParseBool(val) - if err == nil { + if parsed, ok := sqdParseBoolString(val); ok { return parsed } + log.Warn().Str("value", val).Msg("sqd useDefaultDatasets invalid string, defaulting to true") case int: return val != 0 case int64: return val != 0 case float64: return val != 0 + default: + log.Warn().Str("type", fmt.Sprintf("%T", raw)).Msg("sqd useDefaultDatasets unsupported type, defaulting to true") } return true } + +func sqdParseBoolString(raw string) (bool, bool) { + normalized := strings.ToLower(strings.TrimSpace(raw)) + switch normalized { + case "true", "t", "1", "yes", "y", "on": + return true, true + case "false", "f", "0", "no", "n", "off": + return false, true + default: + return false, false + } +} + +func sqdDatasetFromStringKeyMap(m map[string]interface{}, chainId int64) (string, bool) { + if ds, ok := m[strconv.FormatInt(chainId, 10)].(string); ok && ds != "" { + return ds, true + } + return "", false +} + +func sqdDatasetFromStringKeyMapString(m map[string]string, chainId int64) (string, bool) { + if ds, ok := m[strconv.FormatInt(chainId, 10)]; ok && ds != "" { + return ds, true + } + return "", false +} + +func sqdDatasetFromIntKeyMap(m map[int]interface{}, chainId int64) (string, bool) { + if !sqdChainIdFitsInt(chainId) { + return "", false + } + if ds, ok := m[int(chainId)].(string); ok && ds != "" { + return ds, true + } + return "", false +} + +func sqdDatasetFromIntKeyMapString(m map[int]string, chainId int64) (string, bool) { + if !sqdChainIdFitsInt(chainId) { + return "", false + } + if ds, ok := m[int(chainId)]; ok && ds != "" { + return ds, true + } + return "", false +} + +func sqdDatasetFromInt64KeyMap(m map[int64]interface{}, chainId int64) (string, bool) { + if ds, ok := m[chainId].(string); ok && ds != "" { + return ds, true + } + return "", false +} + +func sqdDatasetFromInt64KeyMapString(m map[int64]string, chainId int64) (string, bool) { + if ds, ok := m[chainId]; ok && ds != "" { + return ds, true + } + return "", false +} + +func sqdDatasetFromFloatKeyMap(m map[float64]interface{}, chainId int64) (string, bool) { + if ds, ok := m[float64(chainId)].(string); ok && ds != "" { + return ds, true + } + return "", false +} + +func sqdDatasetFromFloatKeyMapString(m map[float64]string, chainId int64) (string, bool) { + if ds, ok := m[float64(chainId)]; ok && ds != "" { + return ds, true + } + return "", false +} + +func sqdDatasetFromInterfaceKeyMap(m map[interface{}]interface{}, chainId int64) (string, bool) { + chainIdStr := strconv.FormatInt(chainId, 10) + for key, value := range m { + dataset, ok := value.(string) + if !ok || dataset == "" { + continue + } + if sqdChainIdKeyMatches(key, chainId, chainIdStr) { + return dataset, true + } + } + return "", false +} + +func sqdChainIdKeyMatches(key interface{}, chainId int64, chainIdStr string) bool { + switch k := key.(type) { + case int: + return int64(k) == chainId + case int32: + return int64(k) == chainId + case int64: + return k == chainId + case uint: + return int64(k) == chainId + case uint32: + return int64(k) == chainId + case uint64: + return k == uint64(chainId) + case float64: + return k == float64(chainId) + case string: + return strings.TrimSpace(k) == chainIdStr + default: + return false + } +} + +func sqdChainIdFitsInt(chainId int64) bool { + maxInt := int64(^uint(0) >> 1) + minInt := -maxInt - 1 + return chainId >= minInt && chainId <= maxInt +} diff --git a/thirdparty/sqd_chain_mapping_test.go b/thirdparty/sqd_chain_mapping_test.go index d0cfbe1c6..fd4610db4 100644 --- a/thirdparty/sqd_chain_mapping_test.go +++ b/thirdparty/sqd_chain_mapping_test.go @@ -4,13 +4,8 @@ import ( "testing" "github.com/erpc/erpc/common" - "github.com/erpc/erpc/util" ) -func init() { - util.ConfigureTestLogger() -} - func TestSqdChainToDataset(t *testing.T) { tests := []struct { name string @@ -76,8 +71,11 @@ func TestSqdChainToDataset_UnknownChain(t *testing.T) { func TestSqdDatasetFromSettings_MapInterfaceInterface(t *testing.T) { settings := common.VendorSettings{ "datasetByChainId": map[interface{}]interface{}{ - int64(1): "ethereum-mainnet", - "10": "optimism-mainnet", + int(1): "ethereum-mainnet", + int64(10): "optimism-mainnet", + float64(137): "polygon-mainnet", + "42161": "arbitrum-one", + "not-a-chain": "ignore-me", }, } @@ -96,4 +94,55 @@ func TestSqdDatasetFromSettings_MapInterfaceInterface(t *testing.T) { if dataset != "optimism-mainnet" { t.Errorf("dataset = %q, want optimism-mainnet", dataset) } + + dataset, ok = sqdDatasetFromSettings(settings, 137) + if !ok { + t.Fatalf("expected dataset for chain 137") + } + if dataset != "polygon-mainnet" { + t.Errorf("dataset = %q, want polygon-mainnet", dataset) + } + + dataset, ok = sqdDatasetFromSettings(settings, 42161) + if !ok { + t.Fatalf("expected dataset for chain 42161") + } + if dataset != "arbitrum-one" { + t.Errorf("dataset = %q, want arbitrum-one", dataset) + } +} + +func TestSqdUseDefaultDatasets_Coercion(t *testing.T) { + tests := []struct { + name string + value interface{} + expected bool + }{ + {"bool true", true, true}, + {"bool false", false, false}, + {"string true", "true", true}, + {"string false", "false", false}, + {"string yes", "yes", true}, + {"string no", "no", false}, + {"string on", "on", true}, + {"string off", "off", false}, + {"string one", "1", true}, + {"string zero", "0", false}, + {"int one", 1, true}, + {"int zero", 0, false}, + {"int64 one", int64(1), true}, + {"int64 zero", int64(0), false}, + {"float one", float64(1), true}, + {"float zero", float64(0), false}, + {"unknown string defaults true", "maybe", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := sqdUseDefaultDatasets(common.VendorSettings{"useDefaultDatasets": tt.value}) + if got != tt.expected { + t.Fatalf("expected %v, got %v", tt.expected, got) + } + }) + } } diff --git a/thirdparty/sqd_test.go b/thirdparty/sqd_test.go index 9bf968cfd..e389f7421 100644 --- a/thirdparty/sqd_test.go +++ b/thirdparty/sqd_test.go @@ -104,6 +104,13 @@ func TestSqdVendor_SupportsNetwork(t *testing.T) { settings: nil, expected: false, }, + { + name: "evm network with non-numeric chainId", + networkId: "evm:notanumber", + settings: nil, + expected: false, + wantErr: true, + }, { name: "unsupported chainId with dataset override", networkId: "evm:999999", @@ -204,6 +211,24 @@ func TestSqdVendor_GenerateConfigs(t *testing.T) { assert.Equal(t, "secret", configs[0].JsonRpc.Headers["X-API-Key"]) }) + t.Run("sets wrapper api key custom header", func(t *testing.T) { + upstream := &common.UpstreamConfig{ + Id: "test-sqd", + Type: common.UpstreamTypeEvm, + Endpoint: "https://portal-wrapper.internal/v1/evm/1", + Evm: &common.EvmUpstreamConfig{ChainId: 1}, + } + + configs, err := v.GenerateConfigs(ctx, &logger, upstream, common.VendorSettings{ + "wrapperApiKey": "secret", + "wrapperApiKeyHeader": "X-SQD-Key", + }) + assert.NoError(t, err) + assert.Equal(t, "secret", configs[0].JsonRpc.Headers["X-SQD-Key"]) + _, hasDefault := configs[0].JsonRpc.Headers["X-API-Key"] + assert.False(t, hasDefault) + }) + t.Run("preserves existing wrapper header", func(t *testing.T) { upstream := &common.UpstreamConfig{ Id: "test-sqd", @@ -222,6 +247,32 @@ func TestSqdVendor_GenerateConfigs(t *testing.T) { assert.Equal(t, "existing", configs[0].JsonRpc.Headers["X-API-Key"]) }) + t.Run("applies dataset placeholder to endpoint", func(t *testing.T) { + upstream := &common.UpstreamConfig{ + Id: "test-sqd", + Type: common.UpstreamTypeEvm, + Endpoint: "https://portal.sqd.dev/datasets/{dataset}", + Evm: &common.EvmUpstreamConfig{ChainId: 1}, + } + + configs, err := v.GenerateConfigs(ctx, &logger, upstream, nil) + assert.NoError(t, err) + assert.Equal(t, "https://portal.sqd.dev/datasets/ethereum-mainnet", configs[0].Endpoint) + }) + + t.Run("applies dataset to base endpoint", func(t *testing.T) { + upstream := &common.UpstreamConfig{ + Id: "test-sqd", + Type: common.UpstreamTypeEvm, + Endpoint: "https://portal.sqd.dev/datasets", + Evm: &common.EvmUpstreamConfig{ChainId: 1}, + } + + configs, err := v.GenerateConfigs(ctx, &logger, upstream, nil) + assert.NoError(t, err) + assert.Equal(t, "https://portal.sqd.dev/datasets/ethereum-mainnet", configs[0].Endpoint) + }) + t.Run("preserves user-defined ignoreMethods", func(t *testing.T) { upstream := &common.UpstreamConfig{ Id: "test-sqd", From b6f54921f9b759fd1946556cb86cf27dfc7bfa22 Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 2 Feb 2026 14:17:02 +0100 Subject: [PATCH 04/16] fix: tighten sqd settings parsing --- thirdparty/sqd.go | 35 +++-- thirdparty/sqd_chain_mapping.go | 222 ++++++++++++--------------- thirdparty/sqd_chain_mapping_test.go | 44 ++++-- thirdparty/sqd_test.go | 98 +++++++++++- 4 files changed, 244 insertions(+), 155 deletions(-) diff --git a/thirdparty/sqd.go b/thirdparty/sqd.go index 29f846dcd..19c56bc1d 100644 --- a/thirdparty/sqd.go +++ b/thirdparty/sqd.go @@ -24,7 +24,10 @@ func (v *SqdVendor) Name() string { } func (v *SqdVendor) OwnsUpstream(ups *common.UpstreamConfig) bool { - return ups.VendorName == v.Name() + if ups.VendorName == v.Name() { + return true + } + return strings.Contains(ups.Endpoint, "sqd.dev") } func (v *SqdVendor) SupportsNetwork(ctx context.Context, logger *zerolog.Logger, settings common.VendorSettings, networkId string) (bool, error) { @@ -72,7 +75,15 @@ func (v *SqdVendor) GenerateConfigs(ctx context.Context, logger *zerolog.Logger, if dataset, ok := sqdDatasetForChain(settings, upstream.Evm.ChainId); ok { if endpoint, updated := sqdApplyDatasetToEndpoint(upstream.Endpoint, dataset); updated { upstream.Endpoint = endpoint + } else if logger != nil { + logger.Warn(). + Int64("chainId", upstream.Evm.ChainId). + Str("dataset", dataset). + Str("endpoint", upstream.Endpoint). + Msg("sqd dataset resolved but could not be applied to endpoint format") } + } else if strings.Contains(upstream.Endpoint, "{dataset}") { + return nil, fmt.Errorf("sqd endpoint contains {dataset} placeholder but no dataset found for chainId %d", upstream.Evm.ChainId) } if upstream.IgnoreMethods == nil { @@ -89,18 +100,16 @@ func (v *SqdVendor) GenerateConfigs(ctx context.Context, logger *zerolog.Logger, } } - if settings != nil { - if apiKey, ok := settings["wrapperApiKey"].(string); ok && apiKey != "" { - header := "X-API-Key" - if headerOverride, ok := settings["wrapperApiKeyHeader"].(string); ok && headerOverride != "" { - header = headerOverride - } - if upstream.JsonRpc.Headers == nil { - upstream.JsonRpc.Headers = make(map[string]string) - } - if _, exists := upstream.JsonRpc.Headers[header]; !exists { - upstream.JsonRpc.Headers[header] = apiKey - } + if apiKey, ok := settings["wrapperApiKey"].(string); ok && apiKey != "" { + header := "X-API-Key" + if headerOverride, ok := settings["wrapperApiKeyHeader"].(string); ok && headerOverride != "" { + header = headerOverride + } + if upstream.JsonRpc.Headers == nil { + upstream.JsonRpc.Headers = make(map[string]string) + } + if _, exists := upstream.JsonRpc.Headers[header]; !exists { + upstream.JsonRpc.Headers[header] = apiKey } } diff --git a/thirdparty/sqd_chain_mapping.go b/thirdparty/sqd_chain_mapping.go index ee6b0be16..c55f2777a 100644 --- a/thirdparty/sqd_chain_mapping.go +++ b/thirdparty/sqd_chain_mapping.go @@ -37,8 +37,8 @@ var sqdChainToDataset = map[int64]string{ 11155420: "optimism-sepolia", } -// SqdSupportedChainIds returns a slice of all supported chain IDs. -func SqdSupportedChainIds() []int64 { +// sqdSupportedChainIds returns a slice of all supported chain IDs. +func sqdSupportedChainIds() []int64 { chains := make([]int64, 0, len(sqdChainToDataset)) for chainId := range sqdChainToDataset { chains = append(chains, chainId) @@ -55,49 +55,97 @@ func sqdDatasetFromSettings(settings common.VendorSettings, chainId int64) (stri return dataset, true } - if raw, ok := settings["datasetByChainId"]; ok { - switch m := raw.(type) { - case map[string]string: - if ds, ok := sqdDatasetFromStringKeyMapString(m, chainId); ok { - return ds, true - } - case map[string]interface{}: - if ds, ok := sqdDatasetFromStringKeyMap(m, chainId); ok { - return ds, true - } - case map[int]string: - if ds, ok := sqdDatasetFromIntKeyMapString(m, chainId); ok { - return ds, true - } - case map[int]interface{}: - if ds, ok := sqdDatasetFromIntKeyMap(m, chainId); ok { - return ds, true - } - case map[int64]string: - if ds, ok := sqdDatasetFromInt64KeyMapString(m, chainId); ok { - return ds, true - } - case map[int64]interface{}: - if ds, ok := sqdDatasetFromInt64KeyMap(m, chainId); ok { - return ds, true - } - case map[float64]string: - if ds, ok := sqdDatasetFromFloatKeyMapString(m, chainId); ok { - return ds, true - } - case map[float64]interface{}: - if ds, ok := sqdDatasetFromFloatKeyMap(m, chainId); ok { - return ds, true - } - case map[interface{}]interface{}: - if ds, ok := sqdDatasetFromInterfaceKeyMap(m, chainId); ok { - return ds, true - } - default: - log.Warn().Str("type", fmt.Sprintf("%T", raw)).Msg("sqd datasetByChainId unsupported type") + raw, ok := settings["datasetByChainId"] + if !ok { + return "", false + } + + return sqdDatasetFromMap(raw, chainId) +} + +// sqdDatasetFromMap normalizes any map type from YAML/JSON deserialization +// and looks up the dataset for the given chainId. +func sqdDatasetFromMap(raw interface{}, chainId int64) (string, bool) { + chainIdStr := strconv.FormatInt(chainId, 10) + + switch m := raw.(type) { + case map[string]interface{}: + return sqdExtractStringValue(m, chainIdStr, chainId) + case map[string]string: + if ds, ok := m[chainIdStr]; ok && ds != "" { + return ds, true + } + return "", false + case map[interface{}]interface{}: + return sqdDatasetFromInterfaceMap(m, chainId, chainIdStr) + case map[int]interface{}: + return sqdExtractStringValue(m, int(chainId), chainId) + case map[int]string: + if ds, ok := m[int(chainId)]; ok && ds != "" { + return ds, true + } + return "", false + case map[int64]interface{}: + return sqdExtractStringValue(m, chainId, chainId) + case map[int64]string: + if ds, ok := m[chainId]; ok && ds != "" { + return ds, true + } + return "", false + case map[float64]interface{}: + return sqdExtractStringValue(m, float64(chainId), chainId) + case map[float64]string: + if ds, ok := m[float64(chainId)]; ok && ds != "" { + return ds, true + } + return "", false + default: + log.Warn(). + Int64("chainId", chainId). + Str("type", fmt.Sprintf("%T", raw)). + Msg("sqd datasetByChainId unsupported type") + } + + return "", false +} + +// sqdExtractStringValue looks up a key in a map[K]interface{} and asserts the value is a string. +// Logs a warning if the key exists but the value is not a string. +func sqdExtractStringValue[K comparable](m map[K]interface{}, key K, chainId int64) (string, bool) { + raw, exists := m[key] + if !exists { + return "", false + } + ds, ok := raw.(string) + if !ok || ds == "" { + if !ok { + log.Warn(). + Int64("chainId", chainId). + Str("valueType", fmt.Sprintf("%T", raw)). + Msg("sqd datasetByChainId value is not a string, ignoring") } + return "", false } + return ds, true +} +func sqdDatasetFromInterfaceMap(m map[interface{}]interface{}, chainId int64, chainIdStr string) (string, bool) { + for key, value := range m { + if !sqdChainIdKeyMatches(key, chainId, chainIdStr) { + continue + } + dataset, ok := value.(string) + if !ok || dataset == "" { + if !ok { + log.Warn(). + Int64("chainId", chainId). + Str("valueType", fmt.Sprintf("%T", value)). + Msg("sqd datasetByChainId value is not a string, ignoring") + } + return "", false + } + return dataset, true + } return "", false } @@ -118,7 +166,8 @@ func sqdUseDefaultDatasets(settings common.VendorSettings) bool { if parsed, ok := sqdParseBoolString(val); ok { return parsed } - log.Warn().Str("value", val).Msg("sqd useDefaultDatasets invalid string, defaulting to true") + log.Warn().Str("value", val).Msg("sqd useDefaultDatasets unrecognized string, defaulting to false") + return false case int: return val != 0 case int64: @@ -126,10 +175,9 @@ func sqdUseDefaultDatasets(settings common.VendorSettings) bool { case float64: return val != 0 default: - log.Warn().Str("type", fmt.Sprintf("%T", raw)).Msg("sqd useDefaultDatasets unsupported type, defaulting to true") + log.Warn().Str("type", fmt.Sprintf("%T", raw)).Msg("sqd useDefaultDatasets unsupported type, defaulting to false") + return false } - - return true } func sqdParseBoolString(raw string) (bool, bool) { @@ -144,82 +192,6 @@ func sqdParseBoolString(raw string) (bool, bool) { } } -func sqdDatasetFromStringKeyMap(m map[string]interface{}, chainId int64) (string, bool) { - if ds, ok := m[strconv.FormatInt(chainId, 10)].(string); ok && ds != "" { - return ds, true - } - return "", false -} - -func sqdDatasetFromStringKeyMapString(m map[string]string, chainId int64) (string, bool) { - if ds, ok := m[strconv.FormatInt(chainId, 10)]; ok && ds != "" { - return ds, true - } - return "", false -} - -func sqdDatasetFromIntKeyMap(m map[int]interface{}, chainId int64) (string, bool) { - if !sqdChainIdFitsInt(chainId) { - return "", false - } - if ds, ok := m[int(chainId)].(string); ok && ds != "" { - return ds, true - } - return "", false -} - -func sqdDatasetFromIntKeyMapString(m map[int]string, chainId int64) (string, bool) { - if !sqdChainIdFitsInt(chainId) { - return "", false - } - if ds, ok := m[int(chainId)]; ok && ds != "" { - return ds, true - } - return "", false -} - -func sqdDatasetFromInt64KeyMap(m map[int64]interface{}, chainId int64) (string, bool) { - if ds, ok := m[chainId].(string); ok && ds != "" { - return ds, true - } - return "", false -} - -func sqdDatasetFromInt64KeyMapString(m map[int64]string, chainId int64) (string, bool) { - if ds, ok := m[chainId]; ok && ds != "" { - return ds, true - } - return "", false -} - -func sqdDatasetFromFloatKeyMap(m map[float64]interface{}, chainId int64) (string, bool) { - if ds, ok := m[float64(chainId)].(string); ok && ds != "" { - return ds, true - } - return "", false -} - -func sqdDatasetFromFloatKeyMapString(m map[float64]string, chainId int64) (string, bool) { - if ds, ok := m[float64(chainId)]; ok && ds != "" { - return ds, true - } - return "", false -} - -func sqdDatasetFromInterfaceKeyMap(m map[interface{}]interface{}, chainId int64) (string, bool) { - chainIdStr := strconv.FormatInt(chainId, 10) - for key, value := range m { - dataset, ok := value.(string) - if !ok || dataset == "" { - continue - } - if sqdChainIdKeyMatches(key, chainId, chainIdStr) { - return dataset, true - } - } - return "", false -} - func sqdChainIdKeyMatches(key interface{}, chainId int64, chainIdStr string) bool { switch k := key.(type) { case int: @@ -242,9 +214,3 @@ func sqdChainIdKeyMatches(key interface{}, chainId int64, chainIdStr string) boo return false } } - -func sqdChainIdFitsInt(chainId int64) bool { - maxInt := int64(^uint(0) >> 1) - minInt := -maxInt - 1 - return chainId >= minInt && chainId <= maxInt -} diff --git a/thirdparty/sqd_chain_mapping_test.go b/thirdparty/sqd_chain_mapping_test.go index fd4610db4..fa4faa2ce 100644 --- a/thirdparty/sqd_chain_mapping_test.go +++ b/thirdparty/sqd_chain_mapping_test.go @@ -33,27 +33,19 @@ func TestSqdChainToDataset(t *testing.T) { } func TestSqdSupportedChainIds(t *testing.T) { - chainIds := SqdSupportedChainIds() + chainIds := sqdSupportedChainIds() if len(chainIds) != len(sqdChainToDataset) { - t.Errorf("SqdSupportedChainIds() returned %d chains, want %d", len(chainIds), len(sqdChainToDataset)) + t.Errorf("sqdSupportedChainIds() returned %d chains, want %d", len(chainIds), len(sqdChainToDataset)) } - // Verify all returned chain IDs exist in the map - for _, chainId := range chainIds { - if _, ok := sqdChainToDataset[chainId]; !ok { - t.Errorf("SqdSupportedChainIds() returned chainId %d which is not in sqdChainToDataset", chainId) - } - } - - // Verify all map keys are in the returned slice chainIdSet := make(map[int64]bool) for _, id := range chainIds { chainIdSet[id] = true } for chainId := range sqdChainToDataset { if !chainIdSet[chainId] { - t.Errorf("chainId %d from sqdChainToDataset is missing from SqdSupportedChainIds()", chainId) + t.Errorf("chainId %d from sqdChainToDataset is missing from sqdSupportedChainIds()", chainId) } } } @@ -112,6 +104,19 @@ func TestSqdDatasetFromSettings_MapInterfaceInterface(t *testing.T) { } } +func TestSqdDatasetFromSettings_NonStringValue(t *testing.T) { + settings := common.VendorSettings{ + "datasetByChainId": map[string]interface{}{ + "1": 42, // non-string value + }, + } + + _, ok := sqdDatasetFromSettings(settings, 1) + if ok { + t.Error("expected false for non-string dataset value") + } +} + func TestSqdUseDefaultDatasets_Coercion(t *testing.T) { tests := []struct { name string @@ -134,7 +139,8 @@ func TestSqdUseDefaultDatasets_Coercion(t *testing.T) { {"int64 zero", int64(0), false}, {"float one", float64(1), true}, {"float zero", float64(0), false}, - {"unknown string defaults true", "maybe", true}, + {"unknown string defaults false", "maybe", false}, + {"unknown type defaults false", []string{"bad"}, false}, } for _, tt := range tests { @@ -146,3 +152,17 @@ func TestSqdUseDefaultDatasets_Coercion(t *testing.T) { }) } } + +func TestSqdUseDefaultDatasets_NilSettings(t *testing.T) { + got := sqdUseDefaultDatasets(nil) + if !got { + t.Fatal("expected true for nil settings") + } +} + +func TestSqdUseDefaultDatasets_NotSet(t *testing.T) { + got := sqdUseDefaultDatasets(common.VendorSettings{}) + if !got { + t.Fatal("expected true when useDefaultDatasets not set") + } +} diff --git a/thirdparty/sqd_test.go b/thirdparty/sqd_test.go index e389f7421..752db9d80 100644 --- a/thirdparty/sqd_test.go +++ b/thirdparty/sqd_test.go @@ -2,6 +2,7 @@ package thirdparty import ( "context" + "net/http" "testing" "github.com/erpc/erpc/common" @@ -33,9 +34,9 @@ func TestSqdVendor_OwnsUpstream(t *testing.T) { expected: true, }, { - name: "endpoint with portal.sqd.dev but no vendorName", + name: "endpoint with portal.sqd.dev detected", upstream: &common.UpstreamConfig{Endpoint: "https://portal.sqd.dev/datasets/ethereum-mainnet"}, - expected: false, + expected: true, }, { name: "other vendor", @@ -273,6 +274,33 @@ func TestSqdVendor_GenerateConfigs(t *testing.T) { assert.Equal(t, "https://portal.sqd.dev/datasets/ethereum-mainnet", configs[0].Endpoint) }) + t.Run("applies dataset to base endpoint with trailing slash", func(t *testing.T) { + upstream := &common.UpstreamConfig{ + Id: "test-sqd", + Type: common.UpstreamTypeEvm, + Endpoint: "https://portal.sqd.dev/datasets/", + Evm: &common.EvmUpstreamConfig{ChainId: 1}, + } + + configs, err := v.GenerateConfigs(ctx, &logger, upstream, nil) + assert.NoError(t, err) + assert.Equal(t, "https://portal.sqd.dev/datasets/ethereum-mainnet", configs[0].Endpoint) + }) + + t.Run("fails when dataset placeholder unresolved", func(t *testing.T) { + upstream := &common.UpstreamConfig{ + Id: "test-sqd", + Type: common.UpstreamTypeEvm, + Endpoint: "https://portal.sqd.dev/datasets/{dataset}", + Evm: &common.EvmUpstreamConfig{ChainId: 999999}, + } + + _, err := v.GenerateConfigs(ctx, &logger, upstream, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "{dataset}") + assert.Contains(t, err.Error(), "999999") + }) + t.Run("preserves user-defined ignoreMethods", func(t *testing.T) { upstream := &common.UpstreamConfig{ Id: "test-sqd", @@ -324,3 +352,69 @@ func TestSqdVendor_GenerateConfigs(t *testing.T) { assert.Contains(t, err.Error(), "chainId") }) } + +func TestSqdVendor_GetVendorSpecificErrorIfAny(t *testing.T) { + v := CreateSqdVendor().(*SqdVendor) + + tests := []struct { + name string + resp *http.Response + expectNil bool + expectType string + }{ + { + name: "nil response returns nil", + resp: nil, + expectNil: true, + }, + { + name: "200 returns nil", + resp: &http.Response{StatusCode: http.StatusOK}, + expectNil: true, + }, + { + name: "401 returns unauthorized", + resp: &http.Response{StatusCode: http.StatusUnauthorized}, + expectType: "ErrEndpointUnauthorized", + }, + { + name: "403 returns unauthorized", + resp: &http.Response{StatusCode: http.StatusForbidden}, + expectType: "ErrEndpointUnauthorized", + }, + { + name: "429 returns capacity exceeded", + resp: &http.Response{StatusCode: http.StatusTooManyRequests}, + expectType: "ErrEndpointCapacityExceeded", + }, + { + name: "402 returns billing issue", + resp: &http.Response{StatusCode: http.StatusPaymentRequired}, + expectType: "ErrEndpointBillingIssue", + }, + { + name: "500 returns nil (handled by generic normalization)", + resp: &http.Response{StatusCode: http.StatusInternalServerError}, + expectNil: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := v.GetVendorSpecificErrorIfAny(nil, tt.resp, nil, nil) + if tt.expectNil { + assert.NoError(t, err) + return + } + assert.Error(t, err) + switch tt.expectType { + case "ErrEndpointUnauthorized": + assert.True(t, common.HasErrorCode(err, common.ErrCodeEndpointUnauthorized), "expected ErrEndpointUnauthorized, got %v", err) + case "ErrEndpointCapacityExceeded": + assert.True(t, common.HasErrorCode(err, common.ErrCodeEndpointCapacityExceeded), "expected ErrEndpointCapacityExceeded, got %v", err) + case "ErrEndpointBillingIssue": + assert.True(t, common.HasErrorCode(err, common.ErrCodeEndpointBillingIssue), "expected ErrEndpointBillingIssue, got %v", err) + } + }) + } +} From 74205d2e57a3fbb084fc470d31115e90d3ae9052 Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 2 Feb 2026 14:48:01 +0100 Subject: [PATCH 05/16] test: fix rate limiter registry context --- erpc/networks_failsafe_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpc/networks_failsafe_test.go b/erpc/networks_failsafe_test.go index 33ecff2d6..9964623ff 100644 --- a/erpc/networks_failsafe_test.go +++ b/erpc/networks_failsafe_test.go @@ -1398,7 +1398,7 @@ func TestNetworkFailsafe_UpstreamGroupFilter(t *testing.T) { }, } - rateLimitersRegistry, err := upstream.NewRateLimitersRegistry(&common.RateLimiterConfig{}, &log.Logger) + rateLimitersRegistry, err := upstream.NewRateLimitersRegistry(context.Background(), &common.RateLimiterConfig{}, &log.Logger) require.NoError(t, err) metricsTracker := health.NewTracker(&log.Logger, "test", time.Minute) @@ -1509,7 +1509,7 @@ func TestNetworkFailsafe_UpstreamGroupFilter(t *testing.T) { }, } - rateLimitersRegistry, err := upstream.NewRateLimitersRegistry(&common.RateLimiterConfig{}, &log.Logger) + rateLimitersRegistry, err := upstream.NewRateLimitersRegistry(context.Background(), &common.RateLimiterConfig{}, &log.Logger) require.NoError(t, err) metricsTracker := health.NewTracker(&log.Logger, "test", time.Minute) From 7cf147de90ab7af1f939da32a3dff2bfb61d99c8 Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 2 Feb 2026 15:30:00 +0100 Subject: [PATCH 06/16] chore: trigger ci From 1a8ce8b9e122a2f7581fc08d219575b8813622ab Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 2 Feb 2026 15:33:17 +0100 Subject: [PATCH 07/16] chore: retrigger tests From def9a4af55c1dcfb16dfddbaf741b9ef6d39d693 Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 2 Feb 2026 16:10:27 +0100 Subject: [PATCH 08/16] fix: avoid overflow in sqd chain id matching --- thirdparty/sqd_chain_mapping.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/thirdparty/sqd_chain_mapping.go b/thirdparty/sqd_chain_mapping.go index c55f2777a..5475f4d28 100644 --- a/thirdparty/sqd_chain_mapping.go +++ b/thirdparty/sqd_chain_mapping.go @@ -2,6 +2,7 @@ package thirdparty import ( "fmt" + "math" "strconv" "strings" @@ -201,11 +202,17 @@ func sqdChainIdKeyMatches(key interface{}, chainId int64, chainIdStr string) boo case int64: return k == chainId case uint: + if uint64(k) > math.MaxInt64 { + return false + } return int64(k) == chainId case uint32: return int64(k) == chainId case uint64: - return k == uint64(chainId) + if k > math.MaxInt64 { + return false + } + return int64(k) == chainId case float64: return k == float64(chainId) case string: From 9e9329063215de6b474b25c3eca7aa22f6208742 Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 2 Feb 2026 16:34:28 +0100 Subject: [PATCH 09/16] test: add sqd wrapper e2e integration --- .../sqd_portal_wrapper_integration_test.go | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 test/integration/sqd_portal_wrapper_integration_test.go diff --git a/test/integration/sqd_portal_wrapper_integration_test.go b/test/integration/sqd_portal_wrapper_integration_test.go new file mode 100644 index 000000000..19c457ab0 --- /dev/null +++ b/test/integration/sqd_portal_wrapper_integration_test.go @@ -0,0 +1,132 @@ +// Package integration contains integration tests that run against live services. +// +// SQD wrapper tests are skipped unless SQD_WRAPPER_E2E_ENDPOINT is set. +// +// Environment variables: +// - SQD_WRAPPER_E2E_ENDPOINT: wrapper JSON-RPC endpoint (required) +// - SQD_WRAPPER_E2E_CHAIN_ID: chain id for X-Chain-Id header (optional, default 1) +// - SQD_WRAPPER_E2E_AUTH: auth headers "Header: value" format, ";" separated (optional) +// +// Usage: +// +// SQD_WRAPPER_E2E_ENDPOINT=http://localhost:8080 \ +// SQD_WRAPPER_E2E_CHAIN_ID=1 \ +// go test -v -run TestSqdPortalWrapper_EthChainId ./test/integration/... +package integration + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strconv" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +type sqdJsonRPCRequest struct { + JSONRPC string `json:"jsonrpc"` + ID interface{} `json:"id"` + Method string `json:"method"` + Params []interface{} `json:"params"` +} + +type sqdJsonRPCResponse struct { + JSONRPC string `json:"jsonrpc"` + ID interface{} `json:"id"` + Result json.RawMessage `json:"result,omitempty"` + Error *json.RawMessage `json:"error,omitempty"` +} + +func getWrapperEndpoint(t *testing.T) string { + endpoint := strings.TrimSpace(os.Getenv("SQD_WRAPPER_E2E_ENDPOINT")) + if endpoint == "" { + t.Skip("SQD_WRAPPER_E2E_ENDPOINT not set, skipping SQD wrapper integration test") + } + return endpoint +} + +func getWrapperChainID() string { + chainID := strings.TrimSpace(os.Getenv("SQD_WRAPPER_E2E_CHAIN_ID")) + if chainID == "" { + return "1" + } + return chainID +} + +func getWrapperAuthHeaders() map[string]string { + headers := make(map[string]string) + raw := os.Getenv("SQD_WRAPPER_E2E_AUTH") + if raw == "" { + return headers + } + parts := strings.Split(raw, ";") + for _, part := range parts { + part = strings.TrimSpace(part) + if idx := strings.Index(part, ":"); idx > 0 { + key := strings.TrimSpace(part[:idx]) + value := strings.TrimSpace(part[idx+1:]) + headers[key] = value + } + } + return headers +} + +func makeWrapperRequest(t *testing.T, endpoint string, payload interface{}) sqdJsonRPCResponse { + t.Helper() + + body, err := json.Marshal(payload) + require.NoError(t, err) + + req, err := http.NewRequest("POST", endpoint, bytes.NewReader(body)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Chain-Id", getWrapperChainID()) + + for key, value := range getWrapperAuthHeaders() { + req.Header.Set(key, value) + } + + client := &http.Client{Timeout: 15 * time.Second} + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + if resp.StatusCode != http.StatusOK { + t.Fatalf("unexpected status %d: %s", resp.StatusCode, string(data)) + } + + var parsed sqdJsonRPCResponse + require.NoError(t, json.Unmarshal(data, &parsed)) + return parsed +} + +func TestSqdPortalWrapper_EthChainId(t *testing.T) { + endpoint := getWrapperEndpoint(t) + + resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ + JSONRPC: "2.0", + ID: 1, + Method: "eth_chainId", + Params: []interface{}{}, + }) + + if resp.Error != nil { + t.Fatalf("rpc error: %s", string(*resp.Error)) + } + + var chainIDHex string + require.NoError(t, json.Unmarshal(resp.Result, &chainIDHex)) + require.True(t, strings.HasPrefix(chainIDHex, "0x"), "expected hex chainId, got %q", chainIDHex) + + _, err := strconv.ParseInt(strings.TrimPrefix(chainIDHex, "0x"), 16, 64) + require.NoError(t, err, fmt.Sprintf("invalid chainId hex: %q", chainIDHex)) +} From 5062ac0d00061435eeba0c5ec2ac4cdfb8133140 Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 2 Feb 2026 16:42:24 +0100 Subject: [PATCH 10/16] test: cover sqd wrapper supported methods --- .../sqd_portal_wrapper_integration_test.go | 358 +++++++++++++++++- 1 file changed, 339 insertions(+), 19 deletions(-) diff --git a/test/integration/sqd_portal_wrapper_integration_test.go b/test/integration/sqd_portal_wrapper_integration_test.go index 19c457ab0..5761b8ed8 100644 --- a/test/integration/sqd_portal_wrapper_integration_test.go +++ b/test/integration/sqd_portal_wrapper_integration_test.go @@ -11,7 +11,7 @@ // // SQD_WRAPPER_E2E_ENDPOINT=http://localhost:8080 \ // SQD_WRAPPER_E2E_CHAIN_ID=1 \ -// go test -v -run TestSqdPortalWrapper_EthChainId ./test/integration/... +// go test -v -run TestSqdPortalWrapper_Methods ./test/integration/... package integration import ( @@ -20,6 +20,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "strconv" "strings" @@ -43,6 +44,22 @@ type sqdJsonRPCResponse struct { Error *json.RawMessage `json:"error,omitempty"` } +type sqdRpcResponse struct { + Status int + Body []byte + Parsed sqdJsonRPCResponse +} + +type sqdRpcError struct { + Code int `json:"code"` + Message string `json:"message"` + Data map[string]interface{} `json:"data"` +} + +type sqdCapabilitiesResponse struct { + Methods []string `json:"methods"` +} + func getWrapperEndpoint(t *testing.T) string { endpoint := strings.TrimSpace(os.Getenv("SQD_WRAPPER_E2E_ENDPOINT")) if endpoint == "" { @@ -77,7 +94,44 @@ func getWrapperAuthHeaders() map[string]string { return headers } -func makeWrapperRequest(t *testing.T, endpoint string, payload interface{}) sqdJsonRPCResponse { +func wrapperBaseURL(endpoint string) string { + parsed, err := url.Parse(endpoint) + if err != nil || parsed.Scheme == "" { + return "http://" + strings.TrimRight(endpoint, "/") + } + if parsed.Host == "" { + return strings.TrimRight(endpoint, "/") + } + return fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host) +} + +func fetchWrapperCapabilities(t *testing.T, endpoint string) map[string]bool { + t.Helper() + + base := wrapperBaseURL(endpoint) + req, err := http.NewRequest("GET", base+"/capabilities", nil) + require.NoError(t, err) + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode, "capabilities status %d: %s", resp.StatusCode, string(data)) + + var parsed sqdCapabilitiesResponse + require.NoError(t, json.Unmarshal(data, &parsed)) + + methods := make(map[string]bool, len(parsed.Methods)) + for _, method := range parsed.Methods { + methods[method] = true + } + return methods +} + +func makeWrapperRequest(t *testing.T, endpoint string, payload interface{}) sqdRpcResponse { t.Helper() body, err := json.Marshal(payload) @@ -100,33 +154,299 @@ func makeWrapperRequest(t *testing.T, endpoint string, payload interface{}) sqdJ data, err := io.ReadAll(resp.Body) require.NoError(t, err) - if resp.StatusCode != http.StatusOK { - t.Fatalf("unexpected status %d: %s", resp.StatusCode, string(data)) - } - var parsed sqdJsonRPCResponse require.NoError(t, json.Unmarshal(data, &parsed)) + return sqdRpcResponse{ + Status: resp.StatusCode, + Body: data, + Parsed: parsed, + } +} + +func parseHexInt64(t *testing.T, value string) int64 { + t.Helper() + require.True(t, strings.HasPrefix(value, "0x"), "expected hex value, got %q", value) + parsed, err := strconv.ParseInt(strings.TrimPrefix(value, "0x"), 16, 64) + require.NoError(t, err, fmt.Sprintf("invalid hex value: %q", value)) return parsed } -func TestSqdPortalWrapper_EthChainId(t *testing.T) { +func skipIfPortalMissingField(t *testing.T, method string, resp sqdRpcResponse) bool { + t.Helper() + + if resp.Status == http.StatusOK { + return false + } + if resp.Parsed.Error == nil { + return false + } + var rpcErr sqdRpcError + if err := json.Unmarshal(*resp.Parsed.Error, &rpcErr); err != nil { + return false + } + if strings.Contains(rpcErr.Message, "portal does not support required field") { + t.Skipf("%s skipped: %s (configure upstream or update portal)", method, rpcErr.Message) + return true + } + return false +} + +func TestSqdPortalWrapper_Methods(t *testing.T) { endpoint := getWrapperEndpoint(t) + capabilities := fetchWrapperCapabilities(t, endpoint) + + baseMethods := []string{ + "eth_chainId", + "eth_blockNumber", + "eth_getBlockByNumber", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getLogs", + "trace_block", + } + upstreamMethods := []string{ + "eth_getBlockByHash", + "eth_getTransactionByHash", + "eth_getTransactionReceipt", + "trace_transaction", + } + + for _, method := range baseMethods { + if !capabilities[method] { + t.Fatalf("capabilities missing base method %s", method) + } + } + + t.Run("eth_chainId", func(t *testing.T) { + resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ + JSONRPC: "2.0", + ID: 1, + Method: "eth_chainId", + Params: []interface{}{}, + }) + if resp.Status != http.StatusOK { + t.Fatalf("unexpected status %d: %s", resp.Status, string(resp.Body)) + } + if resp.Parsed.Error != nil { + t.Fatalf("rpc error: %s", string(*resp.Parsed.Error)) + } + var chainIDHex string + require.NoError(t, json.Unmarshal(resp.Parsed.Result, &chainIDHex)) + parseHexInt64(t, chainIDHex) + }) - resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ - JSONRPC: "2.0", - ID: 1, - Method: "eth_chainId", - Params: []interface{}{}, + t.Run("eth_blockNumber", func(t *testing.T) { + resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ + JSONRPC: "2.0", + ID: 2, + Method: "eth_blockNumber", + Params: []interface{}{}, + }) + if resp.Status != http.StatusOK { + t.Fatalf("unexpected status %d: %s", resp.Status, string(resp.Body)) + } + if resp.Parsed.Error != nil { + t.Fatalf("rpc error: %s", string(*resp.Parsed.Error)) + } + var blockNumberHex string + require.NoError(t, json.Unmarshal(resp.Parsed.Result, &blockNumberHex)) + parseHexInt64(t, blockNumberHex) + }) + + var blockHash string + var txHash string + t.Run("eth_getBlockByNumber", func(t *testing.T) { + resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ + JSONRPC: "2.0", + ID: 3, + Method: "eth_getBlockByNumber", + Params: []interface{}{"latest", false}, + }) + if skipIfPortalMissingField(t, "eth_getBlockByNumber", resp) { + return + } + if resp.Status != http.StatusOK { + t.Fatalf("unexpected status %d: %s", resp.Status, string(resp.Body)) + } + if resp.Parsed.Error != nil { + t.Fatalf("rpc error: %s", string(*resp.Parsed.Error)) + } + if string(resp.Parsed.Result) == "null" { + t.Skip("no block returned for latest") + } + var block map[string]interface{} + require.NoError(t, json.Unmarshal(resp.Parsed.Result, &block)) + if hash, ok := block["hash"].(string); ok { + blockHash = hash + } + if txs, ok := block["transactions"].([]interface{}); ok && len(txs) > 0 { + if first, ok := txs[0].(string); ok { + txHash = first + } + } + }) + + t.Run("eth_getTransactionByBlockNumberAndIndex", func(t *testing.T) { + resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ + JSONRPC: "2.0", + ID: 4, + Method: "eth_getTransactionByBlockNumberAndIndex", + Params: []interface{}{"latest", "0x0"}, + }) + if skipIfPortalMissingField(t, "eth_getTransactionByBlockNumberAndIndex", resp) { + return + } + if resp.Status != http.StatusOK { + t.Fatalf("unexpected status %d: %s", resp.Status, string(resp.Body)) + } + if resp.Parsed.Error != nil { + t.Fatalf("rpc error: %s", string(*resp.Parsed.Error)) + } + if string(resp.Parsed.Result) != "null" { + var tx map[string]interface{} + require.NoError(t, json.Unmarshal(resp.Parsed.Result, &tx)) + } + }) + + t.Run("eth_getLogs", func(t *testing.T) { + resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ + JSONRPC: "2.0", + ID: 5, + Method: "eth_getLogs", + Params: []interface{}{map[string]interface{}{}}, + }) + if resp.Status != http.StatusOK { + t.Fatalf("unexpected status %d: %s", resp.Status, string(resp.Body)) + } + if resp.Parsed.Error != nil { + t.Fatalf("rpc error: %s", string(*resp.Parsed.Error)) + } + var logs []interface{} + require.NoError(t, json.Unmarshal(resp.Parsed.Result, &logs)) + }) + + t.Run("trace_block", func(t *testing.T) { + resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ + JSONRPC: "2.0", + ID: 6, + Method: "trace_block", + Params: []interface{}{"latest"}, + }) + if skipIfPortalMissingField(t, "trace_block", resp) { + return + } + if resp.Status != http.StatusOK { + t.Fatalf("unexpected status %d: %s", resp.Status, string(resp.Body)) + } + if resp.Parsed.Error != nil { + t.Fatalf("rpc error: %s", string(*resp.Parsed.Error)) + } + var traces []interface{} + require.NoError(t, json.Unmarshal(resp.Parsed.Result, &traces)) }) - if resp.Error != nil { - t.Fatalf("rpc error: %s", string(*resp.Error)) + upstreamEnabled := false + for _, method := range upstreamMethods { + if capabilities[method] { + upstreamEnabled = true + break + } + } + if !upstreamEnabled { + return + } + + if blockHash == "" || txHash == "" { + t.Skip("upstream methods enabled, but missing block/tx hash from latest block") } - var chainIDHex string - require.NoError(t, json.Unmarshal(resp.Result, &chainIDHex)) - require.True(t, strings.HasPrefix(chainIDHex, "0x"), "expected hex chainId, got %q", chainIDHex) + t.Run("eth_getBlockByHash", func(t *testing.T) { + resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ + JSONRPC: "2.0", + ID: 7, + Method: "eth_getBlockByHash", + Params: []interface{}{blockHash, false}, + }) + if resp.Status != http.StatusOK { + t.Fatalf("unexpected status %d: %s", resp.Status, string(resp.Body)) + } + if resp.Parsed.Error != nil { + t.Fatalf("rpc error: %s", string(*resp.Parsed.Error)) + } + if string(resp.Parsed.Result) != "null" { + var block map[string]interface{} + require.NoError(t, json.Unmarshal(resp.Parsed.Result, &block)) + } + }) + + t.Run("eth_getTransactionByHash", func(t *testing.T) { + resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ + JSONRPC: "2.0", + ID: 8, + Method: "eth_getTransactionByHash", + Params: []interface{}{txHash}, + }) + if resp.Status != http.StatusOK { + t.Fatalf("unexpected status %d: %s", resp.Status, string(resp.Body)) + } + if resp.Parsed.Error != nil { + t.Fatalf("rpc error: %s", string(*resp.Parsed.Error)) + } + if string(resp.Parsed.Result) != "null" { + var tx map[string]interface{} + require.NoError(t, json.Unmarshal(resp.Parsed.Result, &tx)) + } + }) - _, err := strconv.ParseInt(strings.TrimPrefix(chainIDHex, "0x"), 16, 64) - require.NoError(t, err, fmt.Sprintf("invalid chainId hex: %q", chainIDHex)) + t.Run("eth_getTransactionReceipt", func(t *testing.T) { + resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ + JSONRPC: "2.0", + ID: 9, + Method: "eth_getTransactionReceipt", + Params: []interface{}{txHash}, + }) + if resp.Status != http.StatusOK { + t.Fatalf("unexpected status %d: %s", resp.Status, string(resp.Body)) + } + if resp.Parsed.Error != nil { + t.Fatalf("rpc error: %s", string(*resp.Parsed.Error)) + } + if string(resp.Parsed.Result) != "null" { + var receipt map[string]interface{} + require.NoError(t, json.Unmarshal(resp.Parsed.Result, &receipt)) + } + }) + + t.Run("trace_transaction", func(t *testing.T) { + resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ + JSONRPC: "2.0", + ID: 10, + Method: "trace_transaction", + Params: []interface{}{txHash}, + }) + if resp.Status != http.StatusOK { + t.Fatalf("unexpected status %d: %s", resp.Status, string(resp.Body)) + } + if resp.Parsed.Error != nil { + t.Fatalf("rpc error: %s", string(*resp.Parsed.Error)) + } + var traces []interface{} + require.NoError(t, json.Unmarshal(resp.Parsed.Result, &traces)) + }) + + t.Run("eth_getLogs blockHash", func(t *testing.T) { + resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ + JSONRPC: "2.0", + ID: 11, + Method: "eth_getLogs", + Params: []interface{}{map[string]interface{}{"blockHash": blockHash}}, + }) + if resp.Status != http.StatusOK { + t.Fatalf("unexpected status %d: %s", resp.Status, string(resp.Body)) + } + if resp.Parsed.Error != nil { + t.Fatalf("rpc error: %s", string(*resp.Parsed.Error)) + } + var logs []interface{} + require.NoError(t, json.Unmarshal(resp.Parsed.Result, &logs)) + }) } From b47cc2b4844ec47f23f8c9d22c052ccd2f99283b Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 2 Feb 2026 16:47:51 +0100 Subject: [PATCH 11/16] test: avoid upstream methods in sqd e2e --- .../sqd_portal_wrapper_integration_test.go | 122 ------------------ 1 file changed, 122 deletions(-) diff --git a/test/integration/sqd_portal_wrapper_integration_test.go b/test/integration/sqd_portal_wrapper_integration_test.go index 5761b8ed8..a33efc422 100644 --- a/test/integration/sqd_portal_wrapper_integration_test.go +++ b/test/integration/sqd_portal_wrapper_integration_test.go @@ -203,12 +203,6 @@ func TestSqdPortalWrapper_Methods(t *testing.T) { "eth_getLogs", "trace_block", } - upstreamMethods := []string{ - "eth_getBlockByHash", - "eth_getTransactionByHash", - "eth_getTransactionReceipt", - "trace_transaction", - } for _, method := range baseMethods { if !capabilities[method] { @@ -252,8 +246,6 @@ func TestSqdPortalWrapper_Methods(t *testing.T) { parseHexInt64(t, blockNumberHex) }) - var blockHash string - var txHash string t.Run("eth_getBlockByNumber", func(t *testing.T) { resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ JSONRPC: "2.0", @@ -275,14 +267,6 @@ func TestSqdPortalWrapper_Methods(t *testing.T) { } var block map[string]interface{} require.NoError(t, json.Unmarshal(resp.Parsed.Result, &block)) - if hash, ok := block["hash"].(string); ok { - blockHash = hash - } - if txs, ok := block["transactions"].([]interface{}); ok && len(txs) > 0 { - if first, ok := txs[0].(string); ok { - txHash = first - } - } }) t.Run("eth_getTransactionByBlockNumberAndIndex", func(t *testing.T) { @@ -343,110 +327,4 @@ func TestSqdPortalWrapper_Methods(t *testing.T) { var traces []interface{} require.NoError(t, json.Unmarshal(resp.Parsed.Result, &traces)) }) - - upstreamEnabled := false - for _, method := range upstreamMethods { - if capabilities[method] { - upstreamEnabled = true - break - } - } - if !upstreamEnabled { - return - } - - if blockHash == "" || txHash == "" { - t.Skip("upstream methods enabled, but missing block/tx hash from latest block") - } - - t.Run("eth_getBlockByHash", func(t *testing.T) { - resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ - JSONRPC: "2.0", - ID: 7, - Method: "eth_getBlockByHash", - Params: []interface{}{blockHash, false}, - }) - if resp.Status != http.StatusOK { - t.Fatalf("unexpected status %d: %s", resp.Status, string(resp.Body)) - } - if resp.Parsed.Error != nil { - t.Fatalf("rpc error: %s", string(*resp.Parsed.Error)) - } - if string(resp.Parsed.Result) != "null" { - var block map[string]interface{} - require.NoError(t, json.Unmarshal(resp.Parsed.Result, &block)) - } - }) - - t.Run("eth_getTransactionByHash", func(t *testing.T) { - resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ - JSONRPC: "2.0", - ID: 8, - Method: "eth_getTransactionByHash", - Params: []interface{}{txHash}, - }) - if resp.Status != http.StatusOK { - t.Fatalf("unexpected status %d: %s", resp.Status, string(resp.Body)) - } - if resp.Parsed.Error != nil { - t.Fatalf("rpc error: %s", string(*resp.Parsed.Error)) - } - if string(resp.Parsed.Result) != "null" { - var tx map[string]interface{} - require.NoError(t, json.Unmarshal(resp.Parsed.Result, &tx)) - } - }) - - t.Run("eth_getTransactionReceipt", func(t *testing.T) { - resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ - JSONRPC: "2.0", - ID: 9, - Method: "eth_getTransactionReceipt", - Params: []interface{}{txHash}, - }) - if resp.Status != http.StatusOK { - t.Fatalf("unexpected status %d: %s", resp.Status, string(resp.Body)) - } - if resp.Parsed.Error != nil { - t.Fatalf("rpc error: %s", string(*resp.Parsed.Error)) - } - if string(resp.Parsed.Result) != "null" { - var receipt map[string]interface{} - require.NoError(t, json.Unmarshal(resp.Parsed.Result, &receipt)) - } - }) - - t.Run("trace_transaction", func(t *testing.T) { - resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ - JSONRPC: "2.0", - ID: 10, - Method: "trace_transaction", - Params: []interface{}{txHash}, - }) - if resp.Status != http.StatusOK { - t.Fatalf("unexpected status %d: %s", resp.Status, string(resp.Body)) - } - if resp.Parsed.Error != nil { - t.Fatalf("rpc error: %s", string(*resp.Parsed.Error)) - } - var traces []interface{} - require.NoError(t, json.Unmarshal(resp.Parsed.Result, &traces)) - }) - - t.Run("eth_getLogs blockHash", func(t *testing.T) { - resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ - JSONRPC: "2.0", - ID: 11, - Method: "eth_getLogs", - Params: []interface{}{map[string]interface{}{"blockHash": blockHash}}, - }) - if resp.Status != http.StatusOK { - t.Fatalf("unexpected status %d: %s", resp.Status, string(resp.Body)) - } - if resp.Parsed.Error != nil { - t.Fatalf("rpc error: %s", string(*resp.Parsed.Error)) - } - var logs []interface{} - require.NoError(t, json.Unmarshal(resp.Parsed.Result, &logs)) - }) } From 7c1ca0afb63438f20450ca229bd0e1a6ea02c5c1 Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 2 Feb 2026 16:55:21 +0100 Subject: [PATCH 12/16] test: skip upstream-only sqd methods --- .../sqd_portal_wrapper_integration_test.go | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/test/integration/sqd_portal_wrapper_integration_test.go b/test/integration/sqd_portal_wrapper_integration_test.go index a33efc422..5cbc1a37e 100644 --- a/test/integration/sqd_portal_wrapper_integration_test.go +++ b/test/integration/sqd_portal_wrapper_integration_test.go @@ -185,7 +185,7 @@ func skipIfPortalMissingField(t *testing.T, method string, resp sqdRpcResponse) return false } if strings.Contains(rpcErr.Message, "portal does not support required field") { - t.Skipf("%s skipped: %s (configure upstream or update portal)", method, rpcErr.Message) + t.Skipf("%s skipped: %s (requires upstream for this dataset)", method, rpcErr.Message) return true } return false @@ -203,12 +203,23 @@ func TestSqdPortalWrapper_Methods(t *testing.T) { "eth_getLogs", "trace_block", } + upstreamOnlyMethods := []string{ + "eth_getBlockByHash", + "eth_getTransactionByHash", + "eth_getTransactionReceipt", + "trace_transaction", + } for _, method := range baseMethods { if !capabilities[method] { t.Fatalf("capabilities missing base method %s", method) } } + for _, method := range upstreamOnlyMethods { + if capabilities[method] { + t.Logf("capabilities advertises upstream-only method %s; skipping per test config", method) + } + } t.Run("eth_chainId", func(t *testing.T) { resp := makeWrapperRequest(t, endpoint, sqdJsonRPCRequest{ @@ -327,4 +338,15 @@ func TestSqdPortalWrapper_Methods(t *testing.T) { var traces []interface{} require.NoError(t, json.Unmarshal(resp.Parsed.Result, &traces)) }) + + t.Run("eth_getLogs blockHash (requires upstream)", func(t *testing.T) { + t.Skip("requires upstream for blockHash filter") + }) + + for _, method := range upstreamOnlyMethods { + method := method + t.Run(method+" (requires upstream)", func(t *testing.T) { + t.Skip("requires upstream") + }) + } } From 0f94a895e1ec23cb5a1c5a24b4dd4bfa840560a4 Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 2 Feb 2026 17:02:17 +0100 Subject: [PATCH 13/16] test: keep portal missing-field errors strict --- .../sqd_portal_wrapper_integration_test.go | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/test/integration/sqd_portal_wrapper_integration_test.go b/test/integration/sqd_portal_wrapper_integration_test.go index 5cbc1a37e..11cce19a0 100644 --- a/test/integration/sqd_portal_wrapper_integration_test.go +++ b/test/integration/sqd_portal_wrapper_integration_test.go @@ -50,12 +50,6 @@ type sqdRpcResponse struct { Parsed sqdJsonRPCResponse } -type sqdRpcError struct { - Code int `json:"code"` - Message string `json:"message"` - Data map[string]interface{} `json:"data"` -} - type sqdCapabilitiesResponse struct { Methods []string `json:"methods"` } @@ -171,26 +165,6 @@ func parseHexInt64(t *testing.T, value string) int64 { return parsed } -func skipIfPortalMissingField(t *testing.T, method string, resp sqdRpcResponse) bool { - t.Helper() - - if resp.Status == http.StatusOK { - return false - } - if resp.Parsed.Error == nil { - return false - } - var rpcErr sqdRpcError - if err := json.Unmarshal(*resp.Parsed.Error, &rpcErr); err != nil { - return false - } - if strings.Contains(rpcErr.Message, "portal does not support required field") { - t.Skipf("%s skipped: %s (requires upstream for this dataset)", method, rpcErr.Message) - return true - } - return false -} - func TestSqdPortalWrapper_Methods(t *testing.T) { endpoint := getWrapperEndpoint(t) capabilities := fetchWrapperCapabilities(t, endpoint) @@ -264,9 +238,6 @@ func TestSqdPortalWrapper_Methods(t *testing.T) { Method: "eth_getBlockByNumber", Params: []interface{}{"latest", false}, }) - if skipIfPortalMissingField(t, "eth_getBlockByNumber", resp) { - return - } if resp.Status != http.StatusOK { t.Fatalf("unexpected status %d: %s", resp.Status, string(resp.Body)) } @@ -287,9 +258,6 @@ func TestSqdPortalWrapper_Methods(t *testing.T) { Method: "eth_getTransactionByBlockNumberAndIndex", Params: []interface{}{"latest", "0x0"}, }) - if skipIfPortalMissingField(t, "eth_getTransactionByBlockNumberAndIndex", resp) { - return - } if resp.Status != http.StatusOK { t.Fatalf("unexpected status %d: %s", resp.Status, string(resp.Body)) } @@ -326,9 +294,6 @@ func TestSqdPortalWrapper_Methods(t *testing.T) { Method: "trace_block", Params: []interface{}{"latest"}, }) - if skipIfPortalMissingField(t, "trace_block", resp) { - return - } if resp.Status != http.StatusOK { t.Fatalf("unexpected status %d: %s", resp.Status, string(resp.Body)) } From 46ce95b83dd23303a62685bda9abc6c10e3172bb Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 2 Feb 2026 17:11:13 +0100 Subject: [PATCH 14/16] ci: run release workflow on morpho-main --- .github/workflows/release.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 33144b990..9e77fd0a2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,7 @@ on: push: branches: - main + - morpho-main permissions: attestations: write @@ -121,14 +122,14 @@ jobs: - Generated release files - Generated checksums branch: "release/${{ github.event.inputs.version_tag }}" - base: main + base: morpho-main labels: release # Run on release PR merge release: if: | github.event_name == 'push' && - github.ref == 'refs/heads/main' && + (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/morpho-main') && contains(github.event.head_commit.message, 'chore: release') runs-on: "${{ github.repository_owner == 'erpc' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }}" steps: @@ -250,7 +251,7 @@ jobs: docker-build-amd64: runs-on: "${{ github.repository_owner == 'erpc' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }}" needs: [prepare-release, release] - if: always() && github.ref == 'refs/heads/main' + if: always() && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/morpho-main') timeout-minutes: 35 outputs: digest_main: ${{ steps.build_main.outputs.digest }} @@ -331,7 +332,7 @@ jobs: docker-build-arm64: runs-on: "${{ github.repository_owner == 'erpc' && 'blacksmith-8vcpu-ubuntu-2404-arm' || 'ubuntu-24.04-arm' }}" needs: [prepare-release, release] - if: always() && github.ref == 'refs/heads/main' + if: always() && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/morpho-main') timeout-minutes: 35 outputs: digest_main: ${{ steps.build_main.outputs.digest }} @@ -412,7 +413,7 @@ jobs: docker-manifest: runs-on: "${{ github.repository_owner == 'erpc' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}" needs: [docker-build-amd64, docker-build-arm64] - if: always() && github.ref == 'refs/heads/main' + if: always() && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/morpho-main') steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 @@ -502,7 +503,7 @@ jobs: docker-cleanup-sha256-tags: runs-on: "${{ github.repository_owner == 'erpc' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}" needs: [docker-manifest] - if: always() && github.ref == 'refs/heads/main' + if: always() && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/morpho-main') steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 From a7c1668b63e42a556dbfb0bf7d70ee05134859c1 Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 2 Feb 2026 17:13:28 +0100 Subject: [PATCH 15/16] ci: tag docker images with branch name --- .github/workflows/release.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e77fd0a2..edddcb9b1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -295,7 +295,7 @@ jobs: VERSION=$(echo "${{ github.event.head_commit.message }}" | grep -oP 'release \K([0-9]+\.[0-9]+\.[0-9]+)') echo "VERSION=$VERSION" >> $GITHUB_OUTPUT - - name: Build+push by digest (main) + - name: Build+push by digest (branch) id: build_main if: github.event.inputs.version_tag == '' uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 @@ -303,7 +303,7 @@ jobs: context: . platforms: linux/amd64 build-args: | - VERSION=main + VERSION=${{ github.ref_name }} COMMIT_SHA=${{ steps.meta.outputs.short_sha }} sbom: false # not supported on docker driver, maybe works with blacksmith? outputs: type=registry,name=ghcr.io/${{ steps.meta.outputs.repo }},push-by-digest=true @@ -376,7 +376,7 @@ jobs: VERSION=$(echo "${{ github.event.head_commit.message }}" | grep -oP 'release \K([0-9]+\.[0-9]+\.[0-9]+)') echo "VERSION=$VERSION" >> $GITHUB_OUTPUT - - name: Build+push by digest (main) + - name: Build+push by digest (branch) id: build_main if: github.event.inputs.version_tag == '' uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 @@ -384,7 +384,7 @@ jobs: context: . platforms: linux/arm64 build-args: | - VERSION=main + VERSION=${{ github.ref_name }} COMMIT_SHA=${{ steps.meta.outputs.short_sha }} sbom: false # not supported on docker driver, maybe works with blacksmith? outputs: type=registry,name=ghcr.io/${{ steps.meta.outputs.repo }},push-by-digest=true @@ -440,17 +440,17 @@ jobs: VERSION=$(echo "${{ github.event.head_commit.message }}" | grep -oP 'release \K([0-9]+\.[0-9]+\.[0-9]+)') echo "VERSION=$VERSION" >> $GITHUB_OUTPUT - - name: Create multi-arch manifest for main + - name: Create multi-arch manifest for branch id: main if: github.event.inputs.version_tag == '' run: | docker buildx imagetools create \ - -t ghcr.io/${{ steps.meta.outputs.repo }}:main \ + -t ghcr.io/${{ steps.meta.outputs.repo }}:${{ github.ref_name }} \ ghcr.io/${{ steps.meta.outputs.repo }}@${{ needs.docker-build-amd64.outputs.digest_main }} \ ghcr.io/${{ steps.meta.outputs.repo }}@${{ needs.docker-build-arm64.outputs.digest_main }} - docker pull ghcr.io/${{ steps.meta.outputs.repo }}:main - DIGEST_MAIN=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/${{ steps.meta.outputs.repo }}:main) + docker pull ghcr.io/${{ steps.meta.outputs.repo }}:${{ github.ref_name }} + DIGEST_MAIN=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/${{ steps.meta.outputs.repo }}:${{ github.ref_name }}) echo "digest_main=${DIGEST_MAIN#*@}" >> "$GITHUB_OUTPUT" - name: Create multi-arch manifests for release From 9cb215982631acc281c08a9ff41d31dc3e336637 Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 2 Feb 2026 22:20:08 +0100 Subject: [PATCH 16/16] fix: avoid uint chainId conversions --- thirdparty/sqd_chain_mapping.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/thirdparty/sqd_chain_mapping.go b/thirdparty/sqd_chain_mapping.go index 5475f4d28..a9b9ab256 100644 --- a/thirdparty/sqd_chain_mapping.go +++ b/thirdparty/sqd_chain_mapping.go @@ -2,7 +2,6 @@ package thirdparty import ( "fmt" - "math" "strconv" "strings" @@ -202,17 +201,11 @@ func sqdChainIdKeyMatches(key interface{}, chainId int64, chainIdStr string) boo case int64: return k == chainId case uint: - if uint64(k) > math.MaxInt64 { - return false - } - return int64(k) == chainId + return strconv.FormatUint(uint64(k), 10) == chainIdStr case uint32: - return int64(k) == chainId + return strconv.FormatUint(uint64(k), 10) == chainIdStr case uint64: - if k > math.MaxInt64 { - return false - } - return int64(k) == chainId + return strconv.FormatUint(k, 10) == chainIdStr case float64: return k == float64(chainId) case string: