From 0e609fdf249941d7e286d5c6c12026a7eae799f9 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 24 Feb 2026 16:59:44 -0800 Subject: [PATCH] Fix dependency manager handling of block heights outside of query range --- go.mod | 12 +- go.sum | 50 ++--- .../dependencymanager/dependencyinstaller.go | 137 ++++++++----- .../dependencyinstaller_test.go | 189 +++++++++++------- 4 files changed, 231 insertions(+), 157 deletions(-) diff --git a/go.mod b/go.mod index 9486b11ee..8fbd443c5 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/onflow/flow-go v0.45.0-internal-rc.3.0.20260129222115-cc0505f2afd5 github.com/onflow/flow-go-sdk v1.9.13 github.com/onflow/flow/protobuf/go/flow v0.4.19 - github.com/onflow/flowkit/v2 v2.10.2 + github.com/onflow/flowkit/v2 v2.11.0 github.com/onflowser/flowser/v3 v3.2.1-0.20240131200229-7d4d22715f48 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/errors v0.9.1 @@ -39,7 +39,7 @@ require ( github.com/stretchr/testify v1.11.1 golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 golang.org/x/term v0.40.0 - google.golang.org/grpc v1.78.0 + google.golang.org/grpc v1.79.1 ) require ( @@ -270,12 +270,12 @@ require ( go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/sdk v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.7.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/go.sum b/go.sum index 53b63b962..4058a523f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= @@ -159,8 +159,8 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e h1:0XBUw73chJ1VYSsfvcPvVT7auykAJce9FpRr10L6Qhw= github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:P13beTBKr5Q18lJe1rIoLUqjM+CB1zYrRg44ZqGuQSA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= -github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/cockroachdb/crlib v0.0.0-20241015224233-894974b3ad94 h1:bvJv505UUfjzbaIPdNS4AEkHreDqQk6yuNpsdRHpwFA= github.com/cockroachdb/crlib v0.0.0-20241015224233-894974b3ad94/go.mod h1:Gq51ZeKaFCXk6QwuGM0w1dnaOqc/F5zKT2zA9D6Xeac= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= @@ -266,12 +266,12 @@ github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4s github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= -github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= -github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= +github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= -github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= @@ -820,8 +820,8 @@ github.com/onflow/flow-nft/lib/go/templates v1.3.0 h1:uGIBy4GEY6Z9hKP7sm5nA5kwvb github.com/onflow/flow-nft/lib/go/templates v1.3.0/go.mod h1:gVbb5fElaOwKhV5UEUjM+JQTjlsguHg2jwRupfM/nng= github.com/onflow/flow/protobuf/go/flow v0.4.19 h1:oYQoHWT/Iu441tX908qhCy7pCWAtwDspVrWbFGoTH1o= github.com/onflow/flow/protobuf/go/flow v0.4.19/go.mod h1:NA2pX2nw8zuaxfKphhKsk00kWLwfd+tv8mS23YXO4Sk= -github.com/onflow/flowkit/v2 v2.10.2 h1:Rd4CtA5osdiYjvRdIzpflDnpST17QbkVANkQFjHDeMA= -github.com/onflow/flowkit/v2 v2.10.2/go.mod h1:TsJ6WMt3gns6mrM4q16X/z/+VBSjHNNJJwOQmWMe+eM= +github.com/onflow/flowkit/v2 v2.11.0 h1:/PilCUiaBPYv5jWmJ2ij2H5PtAehEke+r5e8mbvBpUw= +github.com/onflow/flowkit/v2 v2.11.0/go.mod h1:lN8K+mJ1as/5CusbVgnIXXSocL9QqP0UQkNLVap3KR4= github.com/onflow/go-ethereum v1.15.10 h1:blZBeOLJDOVWqKuhkkMh6S2PKQAJvdgbvOL9ZNggFcU= github.com/onflow/go-ethereum v1.15.10/go.mod h1:t2nZJtwruVjA5u5yEK8InFzjImFLHrF7ak2bw3E4LDM= github.com/onflow/nft-storefront/lib/go/contracts v1.0.0 h1:sxyWLqGm/p4EKT6DUlQESDG1ZNMN9GjPCm1gTq7NGfc= @@ -1154,26 +1154,26 @@ go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= -go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy03iVTXP6ffeN2iXrxfGsZGCjVx0/4KlizjyBwU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0/go.mod h1:TMu73/k1CP8nBUpDLc71Wj/Kf7ZS9FK5b53VapRsP9o= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -1458,8 +1458,8 @@ google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/internal/dependencymanager/dependencyinstaller.go b/internal/dependencymanager/dependencyinstaller.go index ac560a5c6..c1e59d889 100644 --- a/internal/dependencymanager/dependencyinstaller.go +++ b/internal/dependencymanager/dependencyinstaller.go @@ -24,7 +24,6 @@ import ( "encoding/hex" "fmt" "path/filepath" - "strings" "github.com/psiemens/sconfig" @@ -130,24 +129,25 @@ func (f *DependencyFlags) AddToCommand(cmd *cobra.Command) { } type DependencyInstaller struct { - Gateways map[string]gateway.Gateway - Logger output.Logger - State *flowkit.State - SaveState bool - TargetDir string - SkipDeployments bool - SkipAlias bool - SkipUpdatePrompts bool - Update bool - DeploymentAccount string - Name string - logs categorizedLogs - dependencies map[string]config.Dependency - accountAliases map[string]map[string]flowsdk.Address // network -> account -> alias - installCount int // Track number of dependencies installed - pendingPrompts []pendingPrompt // Dependencies that need prompts after tree display - prompter Prompter // Optional: for testing. If nil, uses real prompts - blockHeightCache map[string]uint64 // Cache of latest block heights per network for consistent pinning + Gateways map[string]gateway.Gateway + Logger output.Logger + State *flowkit.State + SaveState bool + TargetDir string + SkipDeployments bool + SkipAlias bool + SkipUpdatePrompts bool + Update bool + DeploymentAccount string + Name string + logs categorizedLogs + dependencies map[string]config.Dependency + accountAliases map[string]map[string]flowsdk.Address // network -> account -> alias + installCount int // Track number of dependencies installed + pendingPrompts []pendingPrompt // Dependencies that need prompts after tree display + prompter Prompter // Optional: for testing. If nil, uses real prompts + blockHeightCache map[string]uint64 // Cache of latest block heights per network for consistent pinning + minQueryableHeightCache map[string]uint64 // Cache of minimum queryable block heights per network (from CompatibleRange) } type Prompter interface { @@ -189,23 +189,24 @@ func NewDependencyInstaller(logger output.Logger, state *flowkit.State, saveStat } return &DependencyInstaller{ - Gateways: gateways, - Logger: logger, - State: state, - SaveState: saveState, - TargetDir: targetDir, - SkipDeployments: flags.skipDeployments, - SkipAlias: flags.skipAlias, - SkipUpdatePrompts: flags.skipUpdatePrompts, - Update: flags.update, - DeploymentAccount: flags.deploymentAccount, - Name: flags.name, - dependencies: make(map[string]config.Dependency), - logs: categorizedLogs{}, - accountAliases: make(map[string]map[string]flowsdk.Address), - pendingPrompts: make([]pendingPrompt, 0), - prompter: prompter{}, - blockHeightCache: make(map[string]uint64), + Gateways: gateways, + Logger: logger, + State: state, + SaveState: saveState, + TargetDir: targetDir, + SkipDeployments: flags.skipDeployments, + SkipAlias: flags.skipAlias, + SkipUpdatePrompts: flags.skipUpdatePrompts, + Update: flags.update, + DeploymentAccount: flags.deploymentAccount, + Name: flags.name, + dependencies: make(map[string]config.Dependency), + logs: categorizedLogs{}, + accountAliases: make(map[string]map[string]flowsdk.Address), + pendingPrompts: make([]pendingPrompt, 0), + prompter: prompter{}, + blockHeightCache: make(map[string]uint64), + minQueryableHeightCache: make(map[string]uint64), }, nil } @@ -461,6 +462,40 @@ func (di *DependencyInstaller) processDependency(dependency config.Dependency) e return di.processDependencies(dependency) } +func (di *DependencyInstaller) getMinQueryableBlockHeight(network string) (uint64, error) { + // Check cache first + if height, ok := di.minQueryableHeightCache[network]; ok { + return height, nil + } + + gw, ok := di.Gateways[network] + if !ok { + return 0, fmt.Errorf("gateway for network %s not found", network) + } + + ctx := context.Background() + nodeVersionInfo, err := gw.GetNodeVersionInfo(ctx) + if err != nil { + return 0, fmt.Errorf("failed to get node version info for %s: %w", network, err) + } + + if nodeVersionInfo == nil { + return 0, fmt.Errorf("node version info is nil for %s", network) + } + + // Get the minimum queryable block height from the compatible range + var minHeight uint64 + if nodeVersionInfo.CompatibleRange != nil { + minHeight = nodeVersionInfo.CompatibleRange.StartHeight + } + + // Cache the result (only if cache map is initialized) + if di.minQueryableHeightCache != nil { + di.minQueryableHeightCache[network] = minHeight + } + return minHeight, nil +} + // getLatestBlockHeight returns the current block height for a given network. // Results are cached per network to ensure all dependencies in a single install // operation get pinned to the same block height for consistency. @@ -609,33 +644,29 @@ func (di *DependencyInstaller) fetchDependenciesWithDepth(dependency config.Depe } else { // Use pinned block height for frozen dependencies blockHeight = existingDependency.BlockHeight - } - accountContracts, err := di.getContractsAtBlockHeight(networkName, address, blockHeight) - if err != nil { - // If we get a spork-related error (block height too old), fall back to latest - // This happens when flow.json has old block heights from before the current spork - // We'll check the hash later - if it matches, we just update metadata; if not, normal update flow applies - if strings.Contains(err.Error(), "spork root block height") || strings.Contains(err.Error(), "key not found") { - di.Logger.Info(fmt.Sprintf(" %s Block height %d is from before current spork, fetching latest version", util.PrintEmoji("⚠️"), blockHeight)) + // Proactively check if this block height is within the node's compatible range + minQueryableHeight, err := di.getMinQueryableBlockHeight(networkName) + if err != nil { + return fmt.Errorf("failed to check compatible block height range: %w", err) + } + + if blockHeight < minQueryableHeight { + // Block height is before the minimum queryable height, need to use latest hadSporkRecovery = true - // Get the current block height (will be cached from above for new deps) latestHeight, err := di.getLatestBlockHeight(networkName) if err != nil { return fmt.Errorf("failed to get latest block height: %w", err) } - // Fetch at that specific block height - accountContracts, err = di.getContractsAtBlockHeight(networkName, address, latestHeight) - if err != nil { - return fmt.Errorf("error fetching contracts: %w", err) - } - // Update blockHeight so it's used consistently for this dependency blockHeight = latestHeight - } else { - return fmt.Errorf("error fetching contracts: %w", err) } } + accountContracts, err := di.getContractsAtBlockHeight(networkName, address, blockHeight) + if err != nil { + return fmt.Errorf("error fetching contracts: %w", err) + } + contract, ok := accountContracts[contractName] if !ok { return fmt.Errorf("contract %s not found at address %s", contractName, address.String()) diff --git a/internal/dependencymanager/dependencyinstaller_test.go b/internal/dependencymanager/dependencyinstaller_test.go index f849887b0..8aa9d516e 100644 --- a/internal/dependencymanager/dependencyinstaller_test.go +++ b/internal/dependencymanager/dependencyinstaller_test.go @@ -76,6 +76,7 @@ func TestDependencyInstallerInstall(t *testing.T) { t.Run("Success", func(t *testing.T) { gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -168,6 +169,7 @@ func TestDependencyInstallerInstallFromFreshClone(t *testing.T) { state.Dependencies().AddOrUpdate(dep) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -242,6 +244,7 @@ func TestDependencyInstallerInstallFromFreshClone(t *testing.T) { state.Dependencies().AddOrUpdate(dep) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -319,6 +322,7 @@ func TestDependencyInstallerInstallFromFreshClone(t *testing.T) { state.Dependencies().AddOrUpdate(dep) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -390,6 +394,7 @@ func TestDependencyInstallerInstallFromFreshClone(t *testing.T) { state.Dependencies().AddOrUpdate(dep) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -472,6 +477,7 @@ func TestDependencyInstallerInstallFromFreshClone(t *testing.T) { assert.NoError(t, err) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -555,6 +561,7 @@ func TestDependencyInstallerInstallFromFreshClone(t *testing.T) { assert.NoError(t, err) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -622,6 +629,7 @@ func TestDependencyInstallerInstallFromFreshClone(t *testing.T) { state.Dependencies().AddOrUpdate(dep) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -710,6 +718,7 @@ func TestDependencyInstallerInstallFromFreshClone(t *testing.T) { assert.NoError(t, err) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -802,6 +811,7 @@ func TestDependencyInstallerInstallFromFreshClone(t *testing.T) { assert.NoError(t, err) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -886,6 +896,7 @@ func TestDependencyInstallerInstallFromFreshClone(t *testing.T) { assert.NoError(t, err) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -974,6 +985,7 @@ func TestDependencyInstallerInstallFromFreshClone(t *testing.T) { assert.NoError(t, err) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -1062,6 +1074,7 @@ func TestDependencyInstallerInstallFromFreshClone(t *testing.T) { assert.NoError(t, err) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -1151,6 +1164,7 @@ func TestDependencyInstallerInstallFromFreshClone(t *testing.T) { assert.NoError(t, err) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -1231,6 +1245,7 @@ func TestDependencyInstallerInstallFromFreshClone(t *testing.T) { assert.NoError(t, err) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -1297,6 +1312,7 @@ func TestDependencyInstallerAdd(t *testing.T) { t.Run("Success", func(t *testing.T) { gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -1338,6 +1354,7 @@ func TestDependencyInstallerAdd(t *testing.T) { t.Run("Success", func(t *testing.T) { gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) setupAccountMocks := func(args mock.Arguments) { addr := args.Get(1).(flow.Address) @@ -1387,6 +1404,7 @@ func TestDependencyInstallerAdd(t *testing.T) { t.Run("Add by core contract name", func(t *testing.T) { gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -1455,6 +1473,7 @@ func TestDependencyInstallerAddMany(t *testing.T) { t.Run("AddMultipleDependencies", func(t *testing.T) { gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { addr := args.Get(1).(flow.Address) assert.Equal(t, addr.String(), serviceAddress) @@ -1520,10 +1539,13 @@ func TestTransitiveConflictAllowedWithMatchingAlias(t *testing.T) { // Gateways per network gwTestnet := mocks.DefaultMockGateway() + gwTestnet.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gwTestnet.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gwMainnet := mocks.DefaultMockGateway() + gwMainnet.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gwMainnet.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gwEmulator := mocks.DefaultMockGateway() + gwEmulator.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gwEmulator.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) // Addresses @@ -1607,6 +1629,7 @@ func TestTransitiveConflictErrorsWithoutAlias(t *testing.T) { // Gateways gwTestnet := mocks.DefaultMockGateway() + gwTestnet.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gwTestnet.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) // Addresses @@ -1663,6 +1686,7 @@ func TestDependencyInstallerAliasTracking(t *testing.T) { t.Run("AutoApplyAliasForSameAccount", func(t *testing.T) { gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) // Mock the same account for both contracts gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -1738,6 +1762,7 @@ func TestDependencyFlagsDeploymentAccount(t *testing.T) { t.Run("Valid deployment account - skips prompt", func(t *testing.T) { gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -1786,6 +1811,7 @@ func TestDependencyFlagsDeploymentAccount(t *testing.T) { t.Run("Valid deployment account with forced network", func(t *testing.T) { gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) di := &DependencyInstaller{ Gateways: map[string]gateway.Gateway{ @@ -1824,6 +1850,7 @@ func TestDependencyFlagsDeploymentAccount(t *testing.T) { t.Run("Invalid deployment account - returns error", func(t *testing.T) { gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) di := &DependencyInstaller{ Gateways: map[string]gateway.Gateway{ @@ -1849,6 +1876,7 @@ func TestDependencyFlagsDeploymentAccount(t *testing.T) { t.Run("Empty deployment account - uses prompt behavior", func(t *testing.T) { gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) di := &DependencyInstaller{ Gateways: map[string]gateway.Gateway{ @@ -1912,6 +1940,7 @@ func TestDependencyFlagsIntegration(t *testing.T) { t.Run("DeFi Actions contracts deploy only on emulator", func(t *testing.T) { gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) di := &DependencyInstaller{ Gateways: map[string]gateway.Gateway{ @@ -1960,6 +1989,7 @@ func TestAliasedImportHandling(t *testing.T) { _, state, _ := util.TestMocks(t) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) barAddr := flow.HexToAddress("0x0c") // testnet address hosting Bar fooTestAddr := flow.HexToAddress("0x0b") // testnet Foo address @@ -2033,6 +2063,7 @@ func TestDependencyInstallerWithAlias(t *testing.T) { t.Run("AddBySourceStringWithName", func(t *testing.T) { gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 100}}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -2084,6 +2115,7 @@ func TestDependencyInstallerWithAlias(t *testing.T) { t.Run("AddByCoreContractNameWithName", func(t *testing.T) { // Mock the gateway to return FlowToken contract gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { addr := args.Get(1).(flow.Address) acc := tests.NewAccountWithAddress(addr.String()) @@ -2142,6 +2174,7 @@ func TestBlockHeightPinning(t *testing.T) { _, state, _ := util.TestMocks(t) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{ BlockHeader: flow.BlockHeader{Height: 12345}, }, nil) @@ -2199,6 +2232,7 @@ func TestBlockHeightPinning(t *testing.T) { state.Dependencies().AddOrUpdate(oldDep) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{ BlockHeader: flow.BlockHeader{Height: 55555}, }, nil) @@ -2248,6 +2282,7 @@ func TestBlockHeightPinning(t *testing.T) { state.Dependencies().AddOrUpdate(frozenDep) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{ BlockHeader: flow.BlockHeader{Height: 99999}, }, nil) @@ -2296,6 +2331,7 @@ func TestBlockHeightPinning(t *testing.T) { state.Dependencies().AddOrUpdate(existingDep) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{ BlockHeader: flow.BlockHeader{Height: 50000}, }, nil) @@ -2349,6 +2385,7 @@ func TestBlockHeightPinning(t *testing.T) { state.Dependencies().AddOrUpdate(pinnedDep) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{ BlockHeader: flow.BlockHeader{Height: 99999}, }, nil) @@ -2400,6 +2437,7 @@ func TestBlockHeightPinning(t *testing.T) { state.Dependencies().AddOrUpdate(outdatedDep) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{ BlockHeader: flow.BlockHeader{Height: 99999}, }, nil) @@ -2457,6 +2495,7 @@ func TestBlockHeightPinning(t *testing.T) { state.Dependencies().AddOrUpdate(pinnedDep) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{ BlockHeader: flow.BlockHeader{Height: 50000}, }, nil) @@ -2491,6 +2530,7 @@ func TestBlockHeightPinning(t *testing.T) { _, state, _ := util.TestMocks(t) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(nil, fmt.Errorf("network error")) gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { @@ -2532,6 +2572,7 @@ func TestBlockHeightPinning(t *testing.T) { callCount := 0 gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Run(func(args mock.Arguments) { callCount++ // Simulate blockchain progressing: each call returns a higher block @@ -2600,12 +2641,12 @@ func TestBlockHeightPinning(t *testing.T) { assert.Equal(t, savedDepA.BlockHeight, savedDepB.BlockHeight, "All deps in same install should have same block height") }) - t.Run("PreSporkBlockHeightWithMatchingHashUpdatesMetadata", func(t *testing.T) { + t.Run("BlockHeightBeforeCompatibleRangeWithMatchingHashUpdatesMetadata", func(t *testing.T) { _, state, _ := util.TestMocks(t) contractCode := []byte("access(all) contract TestContract { access(all) let name: String; init() { self.name = \"Test\" } }") - // Add an existing dependency with a pre-spork block height but correct hash + // Add an existing dependency with a block height before the compatible range but correct hash existingDep := config.Dependency{ Name: "TestContract", Source: config.Source{ @@ -2613,41 +2654,44 @@ func TestBlockHeightPinning(t *testing.T) { Address: serviceAddress, ContractName: "TestContract", }, - BlockHeight: 138158854, // Pre-spork block height + BlockHeight: 138158854, // Block height before compatible range Hash: computeHash(contractCode), // Hash matches current on-chain code } state.Dependencies().AddOrUpdate(existingDep) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 280224020}}, nil) - // Simulate spork error for old block height, success for current block height + // Mock GetNodeVersionInfo to return compatible range + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{ + CompatibleRange: &flow.CompatibleRange{ + StartHeight: 280224020, // Min queryable height + EndHeight: 300000000, + }, + }, nil) + + // With proactive checking, we'll only fetch at the current block height gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { - requestedHeight := args.Get(2).(uint64) // arg 0 = ctx, arg 1 = address, arg 2 = blockHeight - if requestedHeight == 138158854 { - // Old pre-spork block → error - gw.GetAccountAtBlockHeight.Return(nil, fmt.Errorf("not found: block height 138158854 is less than the spork root block height 280224020")) - } else if requestedHeight == 280224020 { - // Current block → success - acc := tests.NewAccountWithAddress(serviceAddress.String()) - acc.Contracts = map[string][]byte{ - "TestContract": contractCode, - } - gw.GetAccountAtBlockHeight.Return(acc, nil) + acc := tests.NewAccountWithAddress(serviceAddress.String()) + acc.Contracts = map[string][]byte{ + "TestContract": contractCode, } + gw.GetAccountAtBlockHeight.Return(acc, nil) }) di := &DependencyInstaller{ - Gateways: map[string]gateway.Gateway{config.EmulatorNetwork.Name: gw.Mock}, - Logger: logger, - State: state, - SkipDeployments: true, - SkipAlias: true, - Update: false, // NO update flag - but should succeed because hash matches - dependencies: make(map[string]config.Dependency), - logs: categorizedLogs{}, - prompter: &mockPrompter{responses: []bool{}}, - blockHeightCache: make(map[string]uint64), + Gateways: map[string]gateway.Gateway{config.EmulatorNetwork.Name: gw.Mock}, + Logger: logger, + State: state, + SkipDeployments: true, + SkipAlias: true, + Update: false, // NO update flag - but should succeed because hash matches + dependencies: make(map[string]config.Dependency), + logs: categorizedLogs{}, + prompter: &mockPrompter{responses: []bool{}}, + blockHeightCache: make(map[string]uint64), + minQueryableHeightCache: make(map[string]uint64), } dep := config.Dependency{ @@ -2669,13 +2713,13 @@ func TestBlockHeightPinning(t *testing.T) { assert.Equal(t, computeHash(contractCode), savedDep.Hash, "Hash should remain the same") }) - t.Run("PreSporkBlockHeightWithMismatchedHashAndSkipUpdatePromptsErrors", func(t *testing.T) { + t.Run("BlockHeightBeforeCompatibleRangeWithMismatchedHashAndSkipUpdatePromptsErrors", func(t *testing.T) { _, state, _ := util.TestMocks(t) oldCode := []byte("access(all) contract TestContract { access(all) let name: String; init() { self.name = \"OldVersion\" } }") newCode := []byte("access(all) contract TestContract { access(all) let name: String; init() { self.name = \"NewVersion\" } }") - // Add an existing dependency with a pre-spork block height and old hash + // Add an existing dependency with a block height before compatible range and old hash existingDep := config.Dependency{ Name: "TestContract", Source: config.Source{ @@ -2683,7 +2727,7 @@ func TestBlockHeightPinning(t *testing.T) { Address: serviceAddress, ContractName: "TestContract", }, - BlockHeight: 138158854, // Pre-spork block height + BlockHeight: 138158854, // Block height before compatible range Hash: computeHash(oldCode), } state.Dependencies().AddOrUpdate(existingDep) @@ -2696,35 +2740,38 @@ func TestBlockHeightPinning(t *testing.T) { assert.NoError(t, err) gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 280224020}}, nil) - // Simulate pre-spork error then success at current block with NEW hash + // Mock GetNodeVersionInfo to return compatible range + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{ + CompatibleRange: &flow.CompatibleRange{ + StartHeight: 280224020, + EndHeight: 300000000, + }, + }, nil) + + // With proactive checking, we'll only fetch at the current block height with NEW code gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { - requestedHeight := args.Get(2).(uint64) - if requestedHeight == 138158854 { - // Old pre-spork block → error - gw.GetAccountAtBlockHeight.Return(nil, fmt.Errorf("not found: block height 138158854 is less than the spork root block height 280224020")) - } else if requestedHeight == 280224020 { - // Current block → success with NEW code - acc := tests.NewAccountWithAddress(serviceAddress.String()) - acc.Contracts = map[string][]byte{ - "TestContract": newCode, - } - gw.GetAccountAtBlockHeight.Return(acc, nil) + acc := tests.NewAccountWithAddress(serviceAddress.String()) + acc.Contracts = map[string][]byte{ + "TestContract": newCode, } + gw.GetAccountAtBlockHeight.Return(acc, nil) }) di := &DependencyInstaller{ - Gateways: map[string]gateway.Gateway{config.EmulatorNetwork.Name: gw.Mock}, - Logger: logger, - State: state, - SkipDeployments: true, - SkipAlias: true, - SkipUpdatePrompts: true, // Want to keep frozen, but can't! - dependencies: make(map[string]config.Dependency), - logs: categorizedLogs{}, - prompter: &mockPrompter{responses: []bool{}}, - blockHeightCache: make(map[string]uint64), + Gateways: map[string]gateway.Gateway{config.EmulatorNetwork.Name: gw.Mock}, + Logger: logger, + State: state, + SkipDeployments: true, + SkipAlias: true, + SkipUpdatePrompts: true, // Want to keep frozen, but can't! + dependencies: make(map[string]config.Dependency), + logs: categorizedLogs{}, + prompter: &mockPrompter{responses: []bool{}}, + blockHeightCache: make(map[string]uint64), + minQueryableHeightCache: make(map[string]uint64), } dep := config.Dependency{ @@ -2737,18 +2784,18 @@ func TestBlockHeightPinning(t *testing.T) { } err = di.Add(dep) - // Should ERROR: pre-spork block not accessible, network has different hash, can't keep frozen - assert.Error(t, err, "Should error when trying to keep frozen with pre-spork block and hash mismatch") + // Should ERROR: block not accessible (outside compatible range), network has different hash, can't keep frozen + assert.Error(t, err, "Should error when trying to keep frozen with inaccessible block and hash mismatch") assert.Contains(t, err.Error(), "cannot keep frozen", "Error should mention inability to freeze") assert.Contains(t, err.Error(), "138158854", "Error should mention the old block height") assert.Contains(t, err.Error(), "280224020", "Error should mention the new block height") assert.Contains(t, err.Error(), "no longer accessible", "Error should explain block is not accessible") }) - t.Run("PreSporkBlockHeightWithMismatchedHashRequiresUpdateFlag", func(t *testing.T) { + t.Run("BlockHeightBeforeCompatibleRangeWithMismatchedHashRequiresUpdateFlag", func(t *testing.T) { _, state, _ := util.TestMocks(t) - // Add an existing dependency with a pre-spork block height + // Add an existing dependency with a block height before compatible range existingDep := config.Dependency{ Name: "TestContract", Source: config.Source{ @@ -2756,7 +2803,7 @@ func TestBlockHeightPinning(t *testing.T) { Address: serviceAddress, ContractName: "TestContract", }, - BlockHeight: 138158854, // Pre-spork block height + BlockHeight: 138158854, // Block height before compatible range Hash: "oldhash", } state.Dependencies().AddOrUpdate(existingDep) @@ -2764,24 +2811,24 @@ func TestBlockHeightPinning(t *testing.T) { contractCode := []byte("access(all) contract TestContract { access(all) let name: String; init() { self.name = \"Test\" } }") gw := mocks.DefaultMockGateway() + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{CompatibleRange: nil}, nil) gw.GetLatestBlock.Return(&flow.Block{BlockHeader: flow.BlockHeader{Height: 280224020}}, nil) - // Track calls to GetAccountAtBlockHeight - callCount := 0 + // Mock GetNodeVersionInfo to return compatible range + gw.GetNodeVersionInfo.Return(&flow.NodeVersionInfo{ + CompatibleRange: &flow.CompatibleRange{ + StartHeight: 280224020, + EndHeight: 300000000, + }, + }, nil) + + // With --update flag, we go straight to latest block height gw.GetAccountAtBlockHeight.Run(func(args mock.Arguments) { - callCount++ - requestedHeight := args.Get(2).(uint64) // arg 0 = ctx, arg 1 = address, arg 2 = blockHeight - if requestedHeight == 138158854 { - // Old pre-spork block → error - gw.GetAccountAtBlockHeight.Return(nil, fmt.Errorf("not found: block height 138158854 is less than the spork root block height 280224020")) - } else if requestedHeight == 280224020 { - // Current block → success - acc := tests.NewAccountWithAddress(serviceAddress.String()) - acc.Contracts = map[string][]byte{ - "TestContract": contractCode, - } - gw.GetAccountAtBlockHeight.Return(acc, nil) + acc := tests.NewAccountWithAddress(serviceAddress.String()) + acc.Contracts = map[string][]byte{ + "TestContract": contractCode, } + gw.GetAccountAtBlockHeight.Return(acc, nil) }) di := &DependencyInstaller{ @@ -2809,10 +2856,6 @@ func TestBlockHeightPinning(t *testing.T) { err := di.Add(dep) assert.NoError(t, err) - // Verify that GetAccountAtBlockHeight was called only once - // With --update flag, we skip trying the old block and go straight to latest - assert.Equal(t, 1, callCount, "GetAccountAtBlockHeight should be called once (--update skips old block, goes directly to latest)") - // Verify the dependency was updated with latest version savedDep := state.Dependencies().ByName("TestContract") assert.NotNil(t, savedDep)