diff --git a/GNUmakefile b/GNUmakefile index 775d204269..829075c30d 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -44,6 +44,10 @@ install-chainlink: operator-ui ## Install the chainlink binary. install-chainlink-cover: operator-ui ## Install the chainlink binary with cover flag. go install -cover $(GOFLAGS) . +.PHONY: install-chainlink-delve +install-chainlink-delve: operator-ui ## Install the chainlink binary. + go install $(GOFLAGS) -gcflags "all=-N -l" . + .PHONY: chainlink chainlink: ## Build the chainlink binary. go build $(GOFLAGS) . diff --git a/core/chainlink.debug.Dockerfile b/core/chainlink.debug.Dockerfile new file mode 100644 index 0000000000..2e9229cd4c --- /dev/null +++ b/core/chainlink.debug.Dockerfile @@ -0,0 +1,97 @@ +# Build image: Chainlink binary +FROM golang:1.22-bullseye as buildgo +RUN go version +WORKDIR /chainlink + +COPY GNUmakefile package.json ./ +COPY tools/bin/ldflags ./tools/bin/ + +ADD go.mod go.sum ./ +RUN go mod download + +# Env vars needed for chainlink build +ARG COMMIT_SHA + +# Build chainlink bin with cover flag https://go.dev/doc/build-cover#FAQ +ARG GO_COVER_FLAG=false + +COPY . . + +RUN apt-get update && apt-get install -y jq + +# Build the golang binary +RUN if [ "$GO_COVER_FLAG" = "true" ]; then \ + make install-chainlink-cover; \ + else \ + make install-chainlink-delve; \ + fi + +# Link LOOP Plugin source dirs with simple names +RUN go list -m -f "{{.Dir}}" github.com/smartcontractkit/chainlink-feeds | xargs -I % ln -s % /chainlink-feeds +RUN go list -m -f "{{.Dir}}" github.com/smartcontractkit/chainlink-solana | xargs -I % ln -s % /chainlink-solana + +# Build image: Plugins +FROM golang:1.22-bullseye as buildplugins +RUN go version + +WORKDIR /chainlink-feeds +COPY --from=buildgo /chainlink-feeds . +RUN go install ./cmd/chainlink-feeds + +WORKDIR /chainlink-solana +COPY --from=buildgo /chainlink-solana . +RUN go install ./pkg/solana/cmd/chainlink-solana + +# Final image: ubuntu with chainlink binary +FROM golang:1.22-bullseye + +ARG CHAINLINK_USER=root +ENV DEBIAN_FRONTEND noninteractive +RUN apt-get update && apt-get install -y ca-certificates gnupg lsb-release curl + +# Install Postgres for CLI tools, needed specifically for DB backups +RUN curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \ + && echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" |tee /etc/apt/sources.list.d/pgdg.list \ + && apt-get update && apt-get install -y postgresql-client-16 \ + && apt-get clean all \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=buildgo /go/bin/chainlink /usr/local/bin/ + +# Install (but don't enable) LOOP Plugins +COPY --from=buildplugins /go/bin/chainlink-feeds /usr/local/bin/ +COPY --from=buildplugins /go/bin/chainlink-solana /usr/local/bin/ + +# Dependency of CosmWasm/wasmd +COPY --from=buildgo /go/pkg/mod/github.com/\!cosm\!wasm/wasmvm@v*/internal/api/libwasmvm.*.so /usr/lib/ +RUN chmod 755 /usr/lib/libwasmvm.*.so + +RUN if [ ${CHAINLINK_USER} != root ]; then \ + useradd --uid 14933 --create-home ${CHAINLINK_USER}; \ + fi +USER ${CHAINLINK_USER} +WORKDIR /home/${CHAINLINK_USER} +RUN mkdir -p go +# explicit set the cache dir. needed so both root and non-root user has an explicit location +ENV XDG_CACHE_HOME /home/${CHAINLINK_USER}/.cache +RUN mkdir -p ${XDG_CACHE_HOME} + +# Set up env and dir for go coverage profiling https://go.dev/doc/build-cover#FAQ +ARG GO_COVER_DIR="/var/tmp/go-coverage" +ENV GOCOVERDIR=${GO_COVER_DIR} +RUN mkdir -p $GO_COVER_DIR + +# Install dlv +ENV GOPATH=/home/${CHAINLINK_USER}/go +ENV PATH=$PATH:$GOPATH/bin +RUN go install github.com/go-delve/delve/cmd/dlv@latest + +EXPOSE 6688 +ENTRYPOINT ["chainlink"] + +HEALTHCHECK CMD curl -f http://localhost:6688/health || exit 1 + +# Delve port +EXPOSE 40000 + +CMD ["dlv", "exec", "/usr/local/bin/chainlink", "--accept-multiclient", "--headless", "--listen=0.0.0.0:40000", "--api-version=2", "--", "local", "node"] diff --git a/core/services/ocr2/delegate.go b/core/services/ocr2/delegate.go index 35b3ef7650..f6410c5b28 100644 --- a/core/services/ocr2/delegate.go +++ b/core/services/ocr2/delegate.go @@ -7,23 +7,20 @@ import ( "fmt" "log" "strconv" + "strings" "time" - "gopkg.in/guregu/null.v4" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" - "github.com/smartcontractkit/chainlink-common/pkg/types/core" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" - "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" + "google.golang.org/grpc" + "gopkg.in/guregu/null.v4" + chainselectors "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/libocr/commontypes" libocr2 "github.com/smartcontractkit/libocr/offchainreporting2plus" "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - "google.golang.org/grpc" ocr2keepers20 "github.com/smartcontractkit/chainlink-automation/pkg/v2" ocr2keepers20config "github.com/smartcontractkit/chainlink-automation/pkg/v2/config" @@ -38,10 +35,11 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/loop/reportingplugins/ocr3" "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" "github.com/smartcontractkit/chainlink-common/pkg/types" + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" llotypes "github.com/smartcontractkit/chainlink-common/pkg/types/llo" "github.com/smartcontractkit/chainlink-common/pkg/utils/mailbox" datastreamsllo "github.com/smartcontractkit/chainlink-data-streams/llo" - "github.com/smartcontractkit/chainlink/v2/core/bridges" "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" coreconfig "github.com/smartcontractkit/chainlink/v2/core/config" @@ -51,8 +49,11 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/keystore" "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" "github.com/smartcontractkit/chainlink/v2/core/services/llo" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipcommit" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipexec" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" + ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/functions" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/generic" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager" @@ -62,7 +63,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/median" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/mercury" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ocr2keeper" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/autotelemetry21" ocr2keeper21core "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/core" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/validate" @@ -78,8 +78,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/synchronization" "github.com/smartcontractkit/chainlink/v2/core/services/telemetry" "github.com/smartcontractkit/chainlink/v2/plugins" - - ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" ) type ErrJobSpecNoRelayer struct { @@ -1611,7 +1609,77 @@ func (d *Delegate) newServicesCCIPCommit(ctx context.Context, lggr logger.Sugare MetricsRegisterer: prometheus.WrapRegistererWith(map[string]string{"job_name": jb.Name.ValueOrZero()}, prometheus.DefaultRegisterer), } - return ccipcommit.NewCommitServices(ctx, d.ds, srcProvider, dstProvider, d.legacyChains, jb, lggr, d.pipelineRunner, oracleArgsNoPlugin, d.isNewlyCreatedJob, int64(srcChainID), dstChainID, logError) + var priceGetter ccip.AllTokensPriceGetter + withPipeline := strings.Trim(pluginJobSpecConfig.TokenPricesUSDPipeline, "\n\t ") != "" + if withPipeline { + priceGetter, err = ccip.NewPipelineGetter(pluginJobSpecConfig.TokenPricesUSDPipeline, d.pipelineRunner, jb.ID, jb.ExternalJobID, jb.Name.ValueOrZero(), lggr) + if err != nil { + return nil, fmt.Errorf("creating pipeline price getter: %w", err) + } + } else { + // Use dynamic price getter. + if pluginJobSpecConfig.PriceGetterConfig == nil { + return nil, fmt.Errorf("priceGetterConfig is nil") + } + + // Configure contract readers for all chains specified in the aggregator configurations. + // Some lanes (e.g. Wemix/Kroma) requires other clients than source and destination, since they use feeds from other chains. + aggregatorChainsToContracts := make(map[uint64][]common.Address) + for _, aggCfg := range pluginJobSpecConfig.PriceGetterConfig.AggregatorPrices { + if _, ok := aggregatorChainsToContracts[aggCfg.ChainID]; !ok { + aggregatorChainsToContracts[aggCfg.ChainID] = make([]common.Address, 0) + } + + aggregatorChainsToContracts[aggCfg.ChainID] = append(aggregatorChainsToContracts[aggCfg.ChainID], aggCfg.AggregatorContractAddress) + } + + contractReaders := map[uint64]types.ContractReader{} + + for chainID, aggregatorContracts := range aggregatorChainsToContracts { + relayID := types.RelayID{Network: spec.Relay, ChainID: strconv.FormatUint(chainID, 10)} + relay, rerr := d.RelayGetter.Get(relayID) + if rerr != nil { + return nil, fmt.Errorf("get relay by id=%v: %w", relayID, err) + } + + contractsConfig := make(map[string]evmrelaytypes.ChainContractReader, len(aggregatorContracts)) + for i := range aggregatorContracts { + contractsConfig[fmt.Sprintf("%v_%v", ccip.OFFCHAIN_AGGREGATOR, i)] = evmrelaytypes.ChainContractReader{ + ContractABI: ccip.OffChainAggregatorABI, + Configs: map[string]*evmrelaytypes.ChainReaderDefinition{ + "decimals": { // CR consumers choose an alias + ChainSpecificName: "decimals", + }, + "latestRoundData": { + ChainSpecificName: "latestRoundData", + }, + }, + } + } + contractReaderConfig := evmrelaytypes.ChainReaderConfig{ + Contracts: contractsConfig, + } + + contractReaderConfigJsonBytes, jerr := json.Marshal(contractReaderConfig) + if jerr != nil { + return nil, fmt.Errorf("marshal contract reader config: %w", jerr) + } + + contractReader, cerr := relay.NewContractReader(ctx, contractReaderConfigJsonBytes) + if cerr != nil { + return nil, fmt.Errorf("new ccip commit contract reader %w", cerr) + } + + contractReaders[chainID] = contractReader + } + + priceGetter, err = ccip.NewDynamicPriceGetter(*pluginJobSpecConfig.PriceGetterConfig, contractReaders) + if err != nil { + return nil, fmt.Errorf("creating dynamic price getter: %w", err) + } + } + + return ccipcommit.NewCommitServices(ctx, d.ds, srcProvider, dstProvider, priceGetter, jb, lggr, d.pipelineRunner, oracleArgsNoPlugin, d.isNewlyCreatedJob, int64(srcChainID), dstChainID, logError) } func newCCIPCommitPluginBytes(isSourceProvider bool, sourceStartBlock uint64, destStartBlock uint64) config.CommitPluginConfig { diff --git a/core/services/ocr2/plugins/ccip/ccipcommit/initializers.go b/core/services/ocr2/plugins/ccip/ccipcommit/initializers.go index 7636b94413..57c84bd7b4 100644 --- a/core/services/ocr2/plugins/ccip/ccipcommit/initializers.go +++ b/core/services/ocr2/plugins/ccip/ccipcommit/initializers.go @@ -3,14 +3,9 @@ package ccipcommit import ( "context" "encoding/json" - "fmt" "math/big" - "strings" "time" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/pricegetter" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/rpclib" - "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" libocr2 "github.com/smartcontractkit/libocr/offchainreporting2plus" @@ -27,7 +22,6 @@ import ( db "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdb" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" - "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip" @@ -48,7 +42,7 @@ var defaultNewReportingPluginRetryConfig = ccipdata.RetryConfig{ MaxRetries: (6 * 4) + 10, } -func NewCommitServices(ctx context.Context, ds sqlutil.DataSource, srcProvider commontypes.CCIPCommitProvider, dstProvider commontypes.CCIPCommitProvider, chainSet legacyevm.LegacyChainContainer, jb job.Job, lggr logger.Logger, pr pipeline.Runner, argsNoPlugin libocr2.OCR2OracleArgs, new bool, sourceChainID int64, destChainID int64, logError func(string)) ([]job.ServiceCtx, error) { +func NewCommitServices(ctx context.Context, ds sqlutil.DataSource, srcProvider commontypes.CCIPCommitProvider, dstProvider commontypes.CCIPCommitProvider, priceGetter ccip.AllTokensPriceGetter, jb job.Job, lggr logger.Logger, pr pipeline.Runner, argsNoPlugin libocr2.OCR2OracleArgs, new bool, sourceChainID int64, destChainID int64, logError func(string)) ([]job.ServiceCtx, error) { spec := jb.OCR2OracleSpec var pluginConfig ccipconfig.CommitPluginJobSpecConfig @@ -75,45 +69,6 @@ func NewCommitServices(ctx context.Context, ds sqlutil.DataSource, srcProvider c commitStoreReader = ccip.NewProviderProxyCommitStoreReader(srcCommitStore, dstCommitStore) commitLggr := lggr.Named("CCIPCommit").With("sourceChain", sourceChainID, "destChain", destChainID) - var priceGetter pricegetter.AllTokensPriceGetter - withPipeline := strings.Trim(pluginConfig.TokenPricesUSDPipeline, "\n\t ") != "" - if withPipeline { - priceGetter, err = pricegetter.NewPipelineGetter(pluginConfig.TokenPricesUSDPipeline, pr, jb.ID, jb.ExternalJobID, jb.Name.ValueOrZero(), lggr) - if err != nil { - return nil, fmt.Errorf("creating pipeline price getter: %w", err) - } - } else { - // Use dynamic price getter. - if pluginConfig.PriceGetterConfig == nil { - return nil, fmt.Errorf("priceGetterConfig is nil") - } - - // Build price getter clients for all chains specified in the aggregator configurations. - // Some lanes (e.g. Wemix/Kroma) requires other clients than source and destination, since they use feeds from other chains. - priceGetterClients := map[uint64]pricegetter.DynamicPriceGetterClient{} - for _, aggCfg := range pluginConfig.PriceGetterConfig.AggregatorPrices { - chainID := aggCfg.ChainID - // Retrieve the chain. - chain, _, err2 := ccipconfig.GetChainByChainID(chainSet, chainID) - if err2 != nil { - return nil, fmt.Errorf("retrieving chain for chainID %d: %w", chainID, err2) - } - caller := rpclib.NewDynamicLimitedBatchCaller( - lggr, - chain.Client(), - rpclib.DefaultRpcBatchSizeLimit, - rpclib.DefaultRpcBatchBackOffMultiplier, - rpclib.DefaultMaxParallelRpcCalls, - ) - priceGetterClients[chainID] = pricegetter.NewDynamicPriceGetterClient(caller) - } - - priceGetter, err = pricegetter.NewDynamicPriceGetter(*pluginConfig.PriceGetterConfig, priceGetterClients) - if err != nil { - return nil, fmt.Errorf("creating dynamic price getter: %w", err) - } - } - offRampReader, err := dstProvider.NewOffRampReader(ctx, pluginConfig.OffRamp) if err != nil { return nil, err diff --git a/core/services/ocr2/plugins/ccip/exportinternal.go b/core/services/ocr2/plugins/ccip/exportinternal.go index fac935925c..ffd770c2ae 100644 --- a/core/services/ocr2/plugins/ccip/exportinternal.go +++ b/core/services/ocr2/plugins/ccip/exportinternal.go @@ -5,13 +5,17 @@ import ( "math/big" "time" + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/ethereum/go-ethereum/common" + "github.com/google/uuid" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/internal/gethwrappers2/generated/offchainaggregator" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipcalc" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" @@ -21,8 +25,13 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/pricegetter" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/rpclib" + "github.com/smartcontractkit/chainlink/v2/core/services/pipeline" ) +const OFFCHAIN_AGGREGATOR = "OffchainAggregator" +const DECIMALS_METHOD_NAME = "decimals" +const LATEST_ROUND_DATA_METHOD_NAME = "latestRoundData" + func GenericAddrToEvm(addr ccip.Address) (common.Address, error) { return ccipcalc.GenericAddrToEvm(addr) } @@ -71,12 +80,18 @@ type DynamicPriceGetterClient = pricegetter.DynamicPriceGetterClient type DynamicPriceGetter = pricegetter.DynamicPriceGetter +type AllTokensPriceGetter = pricegetter.AllTokensPriceGetter + +func NewPipelineGetter(source string, runner pipeline.Runner, jobID int32, externalJobID uuid.UUID, name string, lggr logger.Logger) (*pricegetter.PipelineGetter, error) { + return pricegetter.NewPipelineGetter(source, runner, jobID, externalJobID, name, lggr) +} + func NewDynamicPriceGetterClient(batchCaller rpclib.EvmBatchCaller) DynamicPriceGetterClient { return pricegetter.NewDynamicPriceGetterClient(batchCaller) } -func NewDynamicPriceGetter(cfg config.DynamicPriceGetterConfig, evmClients map[uint64]DynamicPriceGetterClient) (*DynamicPriceGetter, error) { - return pricegetter.NewDynamicPriceGetter(cfg, evmClients) +func NewDynamicPriceGetter(cfg config.DynamicPriceGetterConfig, contractReaders map[uint64]types.ContractReader) (*DynamicPriceGetter, error) { + return pricegetter.NewDynamicPriceGetter(cfg, contractReaders) } func NewDynamicLimitedBatchCaller( @@ -133,3 +148,5 @@ func NewCommitOffchainConfig( ) ccip.CommitOffchainConfig { return ccipdata.NewCommitOffchainConfig(gasPriceDeviationPPB, gasPriceHeartBeat, tokenPriceDeviationPPB, tokenPriceHeartBeat, inflightCacheExpiry, priceReportingDisabled) } + +const OffChainAggregatorABI = offchainaggregator.OffchainAggregatorABI diff --git a/core/services/ocr2/plugins/ccip/internal/pricegetter/evm.go b/core/services/ocr2/plugins/ccip/internal/pricegetter/evm.go index ac4002f53f..48ad9f3298 100644 --- a/core/services/ocr2/plugins/ccip/internal/pricegetter/evm.go +++ b/core/services/ocr2/plugins/ccip/internal/pricegetter/evm.go @@ -7,20 +7,22 @@ import ( "math/big" "strings" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/rpclib" - "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" + "go.uber.org/multierr" + "github.com/smartcontractkit/chainlink-common/pkg/types" cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" - + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/aggregator_v3_interface" "github.com/smartcontractkit/chainlink/v2/core/internal/gethwrappers2/generated/offchainaggregator" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipcalc" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/rpclib" ) -const decimalsMethodName = "decimals" -const latestRoundDataMethodName = "latestRoundData" +const OFFCHAIN_AGGREGATOR = "OffchainAggregator" +const DECIMALS_METHOD_NAME = "decimals" +const LATEST_ROUND_DATA_METHOD_NAME = "latestRoundData" func init() { // Ensure existence of latestRoundData method on the Aggregator contract. @@ -28,8 +30,8 @@ func init() { if err != nil { panic(err) } - ensureMethodOnContract(aggregatorABI, decimalsMethodName) - ensureMethodOnContract(aggregatorABI, latestRoundDataMethodName) + ensureMethodOnContract(aggregatorABI, DECIMALS_METHOD_NAME) + ensureMethodOnContract(aggregatorABI, LATEST_ROUND_DATA_METHOD_NAME) } func ensureMethodOnContract(abi abi.ABI, methodName string) { @@ -49,9 +51,9 @@ func NewDynamicPriceGetterClient(batchCaller rpclib.EvmBatchCaller) DynamicPrice } type DynamicPriceGetter struct { - cfg config.DynamicPriceGetterConfig - evmClients map[uint64]DynamicPriceGetterClient - aggregatorAbi abi.ABI + cfg config.DynamicPriceGetterConfig + contractReaders map[uint64]types.ContractReader + aggregatorAbi abi.ABI } func NewDynamicPriceGetterConfig(configJson string) (config.DynamicPriceGetterConfig, error) { @@ -69,7 +71,7 @@ func NewDynamicPriceGetterConfig(configJson string) (config.DynamicPriceGetterCo // NewDynamicPriceGetter build a DynamicPriceGetter from a configuration and a map of chain ID to batch callers. // A batch caller should be provided for all retrieved prices. -func NewDynamicPriceGetter(cfg config.DynamicPriceGetterConfig, evmClients map[uint64]DynamicPriceGetterClient) (*DynamicPriceGetter, error) { +func NewDynamicPriceGetter(cfg config.DynamicPriceGetterConfig, contractReaders map[uint64]types.ContractReader) (*DynamicPriceGetter, error) { if err := cfg.Validate(); err != nil { return nil, fmt.Errorf("validating dynamic price getter config: %w", err) } @@ -77,13 +79,13 @@ func NewDynamicPriceGetter(cfg config.DynamicPriceGetterConfig, evmClients map[u if err != nil { return nil, fmt.Errorf("parsing offchainaggregator abi: %w", err) } - priceGetter := DynamicPriceGetter{cfg, evmClients, aggregatorAbi} + priceGetter := DynamicPriceGetter{cfg, contractReaders, aggregatorAbi} return &priceGetter, nil } // FilterConfiguredTokens implements the PriceGetter interface. // It filters a list of token addresses for only those that have a price resolution rule configured on the PriceGetterConfig -func (d *DynamicPriceGetter) FilterConfiguredTokens(ctx context.Context, tokens []cciptypes.Address) (configured []cciptypes.Address, unconfigured []cciptypes.Address, err error) { +func (d *DynamicPriceGetter) FilterConfiguredTokens(_ context.Context, tokens []cciptypes.Address) (configured []cciptypes.Address, unconfigured []cciptypes.Address, err error) { configured = []cciptypes.Address{} unconfigured = []cciptypes.Address{} for _, tk := range tokens { @@ -103,7 +105,7 @@ func (d *DynamicPriceGetter) FilterConfiguredTokens(ctx context.Context, tokens return configured, unconfigured, nil } -// It returns the prices of all tokens defined in the price getter. +// GetJobSpecTokenPricesUSD returns the prices of all tokens defined in the price getter. func (d *DynamicPriceGetter) GetJobSpecTokenPricesUSD(ctx context.Context) (map[cciptypes.Address]*big.Int, error) { return d.TokenPricesUSD(ctx, d.getAllTokensDefined()) } @@ -144,60 +146,104 @@ func (d *DynamicPriceGetter) performBatchCalls(ctx context.Context, batchCallsPe } // performBatchCall performs a batch call on a given chain to retrieve token prices. -func (d *DynamicPriceGetter) performBatchCall(ctx context.Context, chainID uint64, batchCalls *batchCallsForChain, prices map[cciptypes.Address]*big.Int) error { - // Retrieve the EVM caller for the chain. - client, exists := d.evmClients[chainID] - if !exists { - return fmt.Errorf("evm caller for chain %d not found", chainID) - } - evmCaller := client.BatchCaller - +func (d *DynamicPriceGetter) performBatchCall(ctx context.Context, chainID uint64, batchCalls *batchCallsForChain, prices map[cciptypes.Address]*big.Int) (err error) { nbDecimalCalls := len(batchCalls.decimalCalls) nbLatestRoundDataCalls := len(batchCalls.decimalCalls) + nbCalls := len(batchCalls.decimalCalls) - // Perform batched call (all decimals calls followed by latest round data calls). - calls := make([]rpclib.EvmCall, 0, nbDecimalCalls+nbLatestRoundDataCalls) - calls = append(calls, batchCalls.decimalCalls...) - calls = append(calls, batchCalls.latestRoundDataCalls...) + // Retrieve contract reader for the chain + contractReader := d.contractReaders[chainID] - results, err := evmCaller.BatchCall(ctx, 0, calls) + // Bind contract reader to the contract addresses necessary for the batch calls + bindings := make([]types.BoundContract, 0) + for i, call := range batchCalls.decimalCalls { + bindings = append(bindings, types.BoundContract{ + Address: string(ccipcalc.EvmAddrToGeneric(call.ContractAddress())), + Name: fmt.Sprintf("%v_%v", OFFCHAIN_AGGREGATOR, i), + }) + } + + err = contractReader.Bind(ctx, bindings) if err != nil { - return fmt.Errorf("batch call on chain %d failed: %w", chainID, err) + return fmt.Errorf("binding contracts failed: %w", err) } - // Extract results. - decimals := make([]uint8, 0, nbDecimalCalls) - latestRounds := make([]*big.Int, 0, nbLatestRoundDataCalls) + // Construct request, adding a decimals and latestRound req per contract name + var decimalsReq uint8 + batchGetLatestValuesRequest := make(map[string]types.ContractBatch) + for i, call := range batchCalls.decimalCalls { + contractName := fmt.Sprintf("%v_%v", OFFCHAIN_AGGREGATOR, i) + batchGetLatestValuesRequest[contractName] = append(batchGetLatestValuesRequest[contractName], types.BatchRead{ + ReadName: call.MethodName(), + ReturnVal: &decimalsReq, + }) + } - for i, res := range results[0:nbDecimalCalls] { - v, err1 := rpclib.ParseOutput[uint8](res, 0) - if err1 != nil { - callSignature := batchCalls.decimalCalls[i].String() - return fmt.Errorf("parse contract output while calling %v on chain %d: %w", callSignature, chainID, err1) - } - decimals = append(decimals, v) + for i, call := range batchCalls.latestRoundDataCalls { + contractName := fmt.Sprintf("%v_%v", OFFCHAIN_AGGREGATOR, i) + batchGetLatestValuesRequest[contractName] = append(batchGetLatestValuesRequest[contractName], types.BatchRead{ + ReadName: call.MethodName(), + ReturnVal: &aggregator_v3_interface.LatestRoundData{}, + }) + } + + // Perform call + result, err2 := contractReader.BatchGetLatestValues(ctx, batchGetLatestValuesRequest) + if err2 != nil { + return fmt.Errorf("BatchGetLatestValues failed %w", err2) } - for i, res := range results[nbDecimalCalls : nbDecimalCalls+nbLatestRoundDataCalls] { - // latestRoundData function has multiple outputs (roundId,answer,startedAt,updatedAt,answeredInRound). - // we want the second one (answer, at idx=1). - v, err1 := rpclib.ParseOutput[*big.Int](res, 1) - if err1 != nil { - callSignature := batchCalls.latestRoundDataCalls[i].String() - return fmt.Errorf("parse contract output while calling %v on chain %d: %w", callSignature, chainID, err1) + // Extract results + // give result the contract name (key ordering not guaranteed to match that of the request) + // and then you get slice of responses + decimalsCR := make([]uint8, 0, nbDecimalCalls) + latestRoundCR := make([]aggregator_v3_interface.LatestRoundData, 0, nbDecimalCalls) + var respErr error + for j := range nbCalls { + contractName := fmt.Sprintf("%v_%v", OFFCHAIN_AGGREGATOR, j) + offchainAggregatorRespSlice := result[contractName] + + for i, read := range offchainAggregatorRespSlice { + val, readErr := read.GetResult() + if readErr != nil { + respErr = multierr.Append(respErr, fmt.Errorf("error with method call %v: %w", batchCalls.decimalCalls[i].MethodName(), readErr)) + continue + } + if read.ReadName == DECIMALS_METHOD_NAME { + decimal, ok := val.(*uint8) + if !ok { + return fmt.Errorf("expected type uint8 for method call %v on contract %v: %w", batchCalls.decimalCalls[i].MethodName(), batchCalls.decimalCalls[i].ContractAddress(), readErr) + } + + decimalsCR = append(decimalsCR, *decimal) + } else if read.ReadName == LATEST_ROUND_DATA_METHOD_NAME { + latestRoundDataRes, ok := val.(*aggregator_v3_interface.LatestRoundData) + if !ok { + return fmt.Errorf("expected type latestRoundDataConfig for method call %v on contract %v: %w", batchCalls.latestRoundDataCalls[i/2].MethodName(), batchCalls.latestRoundDataCalls[i/2].ContractAddress(), readErr) + } + + latestRoundCR = append(latestRoundCR, *latestRoundDataRes) + } } - latestRounds = append(latestRounds, v) + } + if respErr != nil { + return respErr + } + + latestRoundAnswerCR := make([]*big.Int, 0, nbLatestRoundDataCalls) + for i := range nbLatestRoundDataCalls { + latestRoundAnswerCR = append(latestRoundAnswerCR, latestRoundCR[i].Answer) } // Normalize and store prices. for i := range batchCalls.tokenOrder { // Normalize to 1e18. - if decimals[i] < 18 { - latestRounds[i].Mul(latestRounds[i], big.NewInt(0).Exp(big.NewInt(10), big.NewInt(18-int64(decimals[i])), nil)) - } else if decimals[i] > 18 { - latestRounds[i].Div(latestRounds[i], big.NewInt(0).Exp(big.NewInt(10), big.NewInt(int64(decimals[i])-18), nil)) + if decimalsCR[i] < 18 { + latestRoundAnswerCR[i].Mul(latestRoundAnswerCR[i], big.NewInt(0).Exp(big.NewInt(10), big.NewInt(18-int64(decimalsCR[i])), nil)) + } else if decimalsCR[i] > 18 { + latestRoundAnswerCR[i].Div(latestRoundAnswerCR[i], big.NewInt(0).Exp(big.NewInt(10), big.NewInt(int64(decimalsCR[i])-18), nil)) } - prices[ccipcalc.EvmAddrToGeneric(batchCalls.tokenOrder[i])] = latestRounds[i] + prices[ccipcalc.EvmAddrToGeneric(batchCalls.tokenOrder[i])] = latestRoundAnswerCR[i] } return nil } @@ -225,12 +271,12 @@ func (d *DynamicPriceGetter) preparePricesAndBatchCallsPerChain(tokens []cciptyp chainCalls := batchCallsPerChain[aggCfg.ChainID] chainCalls.decimalCalls = append(chainCalls.decimalCalls, rpclib.NewEvmCall( d.aggregatorAbi, - decimalsMethodName, + DECIMALS_METHOD_NAME, aggCfg.AggregatorContractAddress, )) chainCalls.latestRoundDataCalls = append(chainCalls.latestRoundDataCalls, rpclib.NewEvmCall( d.aggregatorAbi, - latestRoundDataMethodName, + LATEST_ROUND_DATA_METHOD_NAME, aggCfg.AggregatorContractAddress, )) chainCalls.tokenOrder = append(chainCalls.tokenOrder, tk) diff --git a/core/services/ocr2/plugins/ccip/internal/pricegetter/evm_test.go b/core/services/ocr2/plugins/ccip/internal/pricegetter/evm_test.go index 78de269968..755bcb84eb 100644 --- a/core/services/ocr2/plugins/ccip/internal/pricegetter/evm_test.go +++ b/core/services/ocr2/plugins/ccip/internal/pricegetter/evm_test.go @@ -1,9 +1,14 @@ package pricegetter import ( + "fmt" "math/big" "testing" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/targets/mocks" + + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -23,6 +28,7 @@ import ( type testParameters struct { cfg config.DynamicPriceGetterConfig evmClients map[uint64]DynamicPriceGetterClient + contractReaders map[uint64]types.ContractReader tokens []common.Address expectedTokenPrices map[common.Address]big.Int expectedTokenPricesForAll map[common.Address]big.Int @@ -92,13 +98,13 @@ func TestDynamicPriceGetterWithEmptyInput(t *testing.T) { }, { name: "get_all_tokens_static_only", - param: testGetAllTokensStaticOnly(), + param: testGetAllTokensStaticOnly(t), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - pg, err := NewDynamicPriceGetter(test.param.cfg, test.param.evmClients) + pg, err := NewDynamicPriceGetter(test.param.cfg, test.param.contractReaders) if test.param.invalidConfigErrorExpected { require.Error(t, err) return @@ -200,6 +206,12 @@ func testParamAggregatorOnly(t *testing.T) testParameters { uint64(103): mockClient(t, []uint8{18}, []aggregator_v3_interface.LatestRoundData{round3}), uint64(104): mockClient(t, []uint8{20}, []aggregator_v3_interface.LatestRoundData{round4}), } + contractReaders := map[uint64]types.ContractReader{ + uint64(101): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), + uint64(102): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), + uint64(103): mockCR(t, []uint8{18}, []aggregator_v3_interface.LatestRoundData{round3}), + uint64(104): mockCR(t, []uint8{20}, []aggregator_v3_interface.LatestRoundData{round4}), + } expectedTokenPrices := map[common.Address]big.Int{ TK1: *multExp(round1.Answer, 10), // expected in 1e18 format. TK2: *multExp(round2.Answer, 10), // expected in 1e18 format. @@ -209,6 +221,7 @@ func testParamAggregatorOnly(t *testing.T) testParameters { return testParameters{ cfg: cfg, evmClients: evmClients, + contractReaders: contractReaders, tokens: []common.Address{TK1, TK2, TK3, TK4}, expectedTokenPrices: expectedTokenPrices, invalidConfigErrorExpected: false, @@ -261,6 +274,10 @@ func testParamAggregatorOnlyMulti(t *testing.T) testParameters { uint64(101): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), uint64(102): mockClient(t, []uint8{8, 8}, []aggregator_v3_interface.LatestRoundData{round2, round3}), } + contractReaders := map[uint64]types.ContractReader{ + uint64(101): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), + uint64(102): mockCR(t, []uint8{8, 8}, []aggregator_v3_interface.LatestRoundData{round2, round3}), + } expectedTokenPrices := map[common.Address]big.Int{ TK1: *multExp(round1.Answer, 10), TK2: *multExp(round2.Answer, 10), @@ -269,6 +286,7 @@ func testParamAggregatorOnlyMulti(t *testing.T) testParameters { return testParameters{ cfg: cfg, evmClients: evmClients, + contractReaders: contractReaders, invalidConfigErrorExpected: false, tokens: []common.Address{TK1, TK2, TK3}, expectedTokenPrices: expectedTokenPrices, @@ -295,6 +313,7 @@ func testParamStaticOnly() testParameters { } // Real LINK/USD example from OP. evmClients := map[uint64]DynamicPriceGetterClient{} + contractReaders := map[uint64]types.ContractReader{} expectedTokenPrices := map[common.Address]big.Int{ TK1: *cfg.StaticPrices[TK1].Price, TK2: *cfg.StaticPrices[TK2].Price, @@ -303,6 +322,7 @@ func testParamStaticOnly() testParameters { return testParameters{ cfg: cfg, evmClients: evmClients, + contractReaders: contractReaders, tokens: []common.Address{TK1, TK2, TK3}, expectedTokenPrices: expectedTokenPrices, } @@ -347,6 +367,10 @@ func testParamNoAggregatorForToken(t *testing.T) testParameters { uint64(101): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), uint64(102): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), } + contractReaders := map[uint64]types.ContractReader{ + uint64(101): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), + uint64(102): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), + } expectedTokenPrices := map[common.Address]big.Int{ TK1: *round1.Answer, TK2: *round2.Answer, @@ -356,6 +380,7 @@ func testParamNoAggregatorForToken(t *testing.T) testParameters { return testParameters{ cfg: cfg, evmClients: evmClients, + contractReaders: contractReaders, tokens: []common.Address{TK1, TK2, TK3, TK4}, expectedTokenPrices: expectedTokenPrices, priceResolutionErrorExpected: true, @@ -401,6 +426,10 @@ func testParamAggregatorAndStaticValid(t *testing.T) testParameters { uint64(101): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), uint64(102): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), } + contractReaders := map[uint64]types.ContractReader{ + uint64(101): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), + uint64(102): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), + } expectedTokenPrices := map[common.Address]big.Int{ TK1: *multExp(round1.Answer, 10), TK2: *multExp(round2.Answer, 10), @@ -409,6 +438,7 @@ func testParamAggregatorAndStaticValid(t *testing.T) testParameters { return testParameters{ cfg: cfg, evmClients: evmClients, + contractReaders: contractReaders, tokens: []common.Address{TK1, TK2, TK3}, expectedTokenPrices: expectedTokenPrices, } @@ -465,9 +495,15 @@ func testParamAggregatorAndStaticTokenCollision(t *testing.T) testParameters { uint64(102): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), uint64(103): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round3}), } + contractReaders := map[uint64]types.ContractReader{ + uint64(101): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), + uint64(102): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), + uint64(103): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round3}), + } return testParameters{ cfg: cfg, evmClients: evmClients, + contractReaders: contractReaders, tokens: []common.Address{TK1, TK2, TK3}, invalidConfigErrorExpected: true, } @@ -506,11 +542,16 @@ func testParamBatchCallReturnsErr(t *testing.T) testParameters { BatchCaller: mockErrCaller(t), }, } + contractReaders := map[uint64]types.ContractReader{ + uint64(101): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), + uint64(102): mockErrCR(t), + } return testParameters{ - cfg: cfg, - evmClients: evmClients, - tokens: []common.Address{TK1, TK2, TK3}, - evmCallErr: true, + cfg: cfg, + evmClients: evmClients, + contractReaders: contractReaders, + tokens: []common.Address{TK1, TK2, TK3}, + evmCallErr: true, } } @@ -566,6 +607,11 @@ func testLessInputsThanDefinedPrices(t *testing.T) testParameters { uint64(102): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), uint64(103): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round3}), } + contractReaders := map[uint64]types.ContractReader{ + uint64(101): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), + uint64(102): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), + uint64(103): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round3}), + } expectedTokenPrices := map[common.Address]big.Int{ TK1: *multExp(round1.Answer, 10), TK2: *multExp(round2.Answer, 10), @@ -574,6 +620,7 @@ func testLessInputsThanDefinedPrices(t *testing.T) testParameters { return testParameters{ cfg: cfg, evmClients: evmClients, + contractReaders: contractReaders, tokens: []common.Address{TK1, TK2, TK3}, expectedTokenPrices: expectedTokenPrices, } @@ -631,6 +678,11 @@ func testGetAllTokensAggregatorAndStatic(t *testing.T) testParameters { uint64(102): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), uint64(103): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round3}), } + contractReaders := map[uint64]types.ContractReader{ + uint64(101): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), + uint64(102): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), + uint64(103): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round3}), + } expectedTokenPricesForAll := map[common.Address]big.Int{ TK1: *multExp(round1.Answer, 10), TK2: *multExp(round2.Answer, 10), @@ -641,6 +693,7 @@ func testGetAllTokensAggregatorAndStatic(t *testing.T) testParameters { cfg: cfg, evmClients: evmClients, expectedTokenPricesForAll: expectedTokenPricesForAll, + contractReaders: contractReaders, } } @@ -691,6 +744,12 @@ func testGetAllTokensAggregatorOnly(t *testing.T) testParameters { uint64(102): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), uint64(103): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round3}), } + contractReaders := map[uint64]types.ContractReader{ + uint64(101): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), + uint64(102): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), + uint64(103): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round3}), + } + expectedTokenPricesForAll := map[common.Address]big.Int{ TK1: *multExp(round1.Answer, 10), TK2: *multExp(round2.Answer, 10), @@ -700,10 +759,11 @@ func testGetAllTokensAggregatorOnly(t *testing.T) testParameters { cfg: cfg, evmClients: evmClients, expectedTokenPricesForAll: expectedTokenPricesForAll, + contractReaders: contractReaders, } } -func testGetAllTokensStaticOnly() testParameters { +func testGetAllTokensStaticOnly(t *testing.T) testParameters { cfg := config.DynamicPriceGetterConfig{ AggregatorPrices: map[common.Address]config.AggregatorPriceConfig{}, StaticPrices: map[common.Address]config.StaticPriceConfig{ @@ -723,6 +783,7 @@ func testGetAllTokensStaticOnly() testParameters { } evmClients := map[uint64]DynamicPriceGetterClient{} + contractReaders := map[uint64]types.ContractReader{} expectedTokenPricesForAll := map[common.Address]big.Int{ TK1: *cfg.StaticPrices[TK1].Price, TK2: *cfg.StaticPrices[TK2].Price, @@ -731,6 +792,7 @@ func testGetAllTokensStaticOnly() testParameters { return testParameters{ cfg: cfg, evmClients: evmClients, + contractReaders: contractReaders, expectedTokenPricesForAll: expectedTokenPricesForAll, } } @@ -760,12 +822,54 @@ func mockCaller(t *testing.T, decimals []uint8, rounds []aggregator_v3_interface return caller } +func mockCR(t *testing.T, decimals []uint8, rounds []aggregator_v3_interface.LatestRoundData) *mocks.ChainReader { + caller := mocks.NewChainReader(t) + + // Mock batch calls per chain: all decimals calls then all latestRoundData calls. + // bGLVR = batchGetLatestValueResult + //nolint:all + var bGLVR types.BatchGetLatestValuesResult + bGLVR = make(map[string]types.ContractBatchResults, 1) + + for i := range len(decimals) { + bGLVR[fmt.Sprintf("%v_%v", OFFCHAIN_AGGREGATOR, i)] = make([]types.BatchReadResult, 0, 2) + } + for i, d := range decimals { + contractName := fmt.Sprintf("%v_%v", OFFCHAIN_AGGREGATOR, i) + readRes := types.BatchReadResult{ + ReadName: DECIMALS_METHOD_NAME, + } + readRes.SetResult(&d, nil) + bGLVR[contractName] = append(bGLVR[contractName], readRes) + } + + for i, r := range rounds { + contractName := fmt.Sprintf("%v_%v", OFFCHAIN_AGGREGATOR, i) + readRes := types.BatchReadResult{ + ReadName: LATEST_ROUND_DATA_METHOD_NAME, + } + readRes.SetResult(&r, nil) + bGLVR[contractName] = append(bGLVR[contractName], readRes) + } + + caller.On("Bind", mock.Anything, mock.Anything).Return(nil).Maybe() + caller.On("BatchGetLatestValues", mock.Anything, mock.Anything).Return(bGLVR, nil).Maybe() + return caller +} + func mockErrCaller(t *testing.T) *rpclibmocks.EvmBatchCaller { caller := rpclibmocks.NewEvmBatchCaller(t) caller.On("BatchCall", mock.Anything, uint64(0), mock.Anything).Return(nil, assert.AnError).Maybe() return caller } +func mockErrCR(t *testing.T) *mocks.ChainReader { + caller := mocks.NewChainReader(t) + caller.On("Bind", mock.Anything, mock.Anything).Return(nil).Maybe() + caller.On("BatchGetLatestValues", mock.Anything, mock.Anything).Return(nil, assert.AnError).Maybe() + return caller +} + // multExp returns the result of multiplying x by 10^e. func multExp(x *big.Int, e int64) *big.Int { return big.NewInt(0).Mul(x, big.NewInt(0).Exp(big.NewInt(10), big.NewInt(e), nil)) diff --git a/core/services/ocr2/plugins/ccip/internal/rpclib/evm.go b/core/services/ocr2/plugins/ccip/internal/rpclib/evm.go index 6c4aabb435..9fac459546 100644 --- a/core/services/ocr2/plugins/ccip/internal/rpclib/evm.go +++ b/core/services/ocr2/plugins/ccip/internal/rpclib/evm.go @@ -280,6 +280,10 @@ func (c EvmCall) String() string { return fmt.Sprintf("%s: %s(%+v)", c.contractAddress.String(), c.methodName, c.args) } +func (c EvmCall) ContractAddress() common.Address { + return c.contractAddress +} + func EVMCallsToString(calls []EvmCall) string { callString := "" for _, call := range calls { diff --git a/integration-tests/ccip-tests/Makefile b/integration-tests/ccip-tests/Makefile index d33d956915..1acd566af3 100644 --- a/integration-tests/ccip-tests/Makefile +++ b/integration-tests/ccip-tests/Makefile @@ -71,3 +71,10 @@ test_smoke_ccip_default: set_config .PHONY: build_ccip_image build_ccip_image: docker build -f ../../core/chainlink.Dockerfile --build-arg COMMIT_SHA=$(git rev-parse HEAD) --build-arg CHAINLINK_USER=chainlink -t $(image):$(tag) ../../ + +# image: the name for the chainlink image being built, example: image=chainlink +# tag: the tag for the chainlink image being built, example: tag=latest +# example usage: make build_ccip_image image=chainlink-ccip tag=latest +.PHONY: build_ccip_debug_image +build_ccip_debug_image: + docker build -f ../../core/chainlink.debug.Dockerfile --build-arg COMMIT_SHA=$(git rev-parse HEAD) --build-arg CHAINLINK_USER=chainlink -t $(image):$(tag) ../../