From 79d581081757dc58e1d388963460b511f5265a66 Mon Sep 17 00:00:00 2001 From: Augustus <14297860+augustbleeds@users.noreply.github.com> Date: Thu, 25 Apr 2024 07:01:32 -0700 Subject: [PATCH] Node Balance Monitoring for STOM (#414) --- integration-tests/go.mod | 8 +- integration-tests/go.sum | 14 ++- monitoring/cmd/monitoring/main.go | 26 ++++- monitoring/pkg/monitoring/config_chain.go | 27 +++-- monitoring/pkg/monitoring/config_node.go | 6 +- .../monitoring/exporter_contract_balance.go | 79 +++++++++++++ .../pkg/monitoring/exporter_prometheus.go | 2 +- monitoring/pkg/monitoring/metrics.go | 36 +++++- .../pkg/monitoring/source_contract_balance.go | 92 ++++++++++++++++ .../source_contract_balance_test.go | 60 ++++++++++ .../pkg/monitoring/source_envelope_test.go | 104 ++++++++++-------- .../pkg/monitoring/source_proxy_test.go | 25 +++-- .../pkg/monitoring/source_txresults_test.go | 8 +- monitoring/pkg/monitoring/testutils.go | 21 ++++ relayer/go.mod | 8 +- relayer/go.sum | 10 +- relayer/pkg/chainlink/erc20/client.go | 87 +++++++++++++++ relayer/pkg/chainlink/erc20/client_test.go | 99 +++++++++++++++++ .../pkg/chainlink/erc20/mocks/ERC20Reader.go | 102 +++++++++++++++++ .../pkg/chainlink/ocr2/mocks/OCR2Reader.go | 2 +- 20 files changed, 722 insertions(+), 94 deletions(-) create mode 100644 monitoring/pkg/monitoring/exporter_contract_balance.go create mode 100644 monitoring/pkg/monitoring/source_contract_balance.go create mode 100644 monitoring/pkg/monitoring/source_contract_balance_test.go create mode 100644 relayer/pkg/chainlink/erc20/client.go create mode 100644 relayer/pkg/chainlink/erc20/client_test.go create mode 100644 relayer/pkg/chainlink/erc20/mocks/ERC20Reader.go diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 4e33defa8..ef5cf544b 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -18,7 +18,7 @@ require ( github.com/smartcontractkit/chainlink-testing-framework v1.27.0 github.com/smartcontractkit/chainlink/integration-tests v0.0.0-20240215151806-009c99876c4c github.com/smartcontractkit/chainlink/v2 v2.9.0-beta0.0.20240215151806-009c99876c4c - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/testcontainers/testcontainers-go v0.28.0 go.uber.org/zap v1.26.0 golang.org/x/text v0.14.0 @@ -222,7 +222,7 @@ require ( github.com/gtank/merlin v0.1.1 // indirect github.com/gtank/ristretto255 v0.1.2 // indirect github.com/hashicorp/consul/api v1.25.1 // indirect - github.com/hashicorp/consul/sdk v0.14.1 // indirect + github.com/hashicorp/consul/sdk v0.16.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-envparse v0.1.0 // indirect @@ -355,7 +355,7 @@ require ( github.com/smartcontractkit/chainlink-feeds v0.0.0-20240119021347-3c541a78cdb8 // indirect github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240213161921-c4d342b761b0 // indirect github.com/smartcontractkit/chainlink-vrf v0.0.0-20231120191722-fef03814f868 // indirect - github.com/smartcontractkit/libocr v0.0.0-20240112202000-6359502d2ff1 // indirect + github.com/smartcontractkit/libocr v0.0.0-20240326191951-2bbe9382d052 // indirect github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230906073235-9e478e5e19f1 // indirect github.com/smartcontractkit/tdh2/go/tdh2 v0.0.0-20230906073235-9e478e5e19f1 // indirect github.com/smartcontractkit/wasp v0.4.2 // indirect @@ -369,7 +369,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.16.0 // indirect github.com/status-im/keycard-go v0.2.0 // indirect - github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/supranational/blst v0.3.11 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 5d516ad4c..de4289ef5 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -837,8 +837,8 @@ github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBt github.com/hashicorp/consul/api v1.25.1 h1:CqrdhYzc8XZuPnhIYZWH45toM0LB9ZeYr/gvpLVI3PE= github.com/hashicorp/consul/api v1.25.1/go.mod h1:iiLVwR/htV7mas/sy0O+XSuEnrdBUUydemjxcUrAt4g= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/consul/sdk v0.14.1 h1:ZiwE2bKb+zro68sWzZ1SgHF3kRMBZ94TwOCFRF4ylPs= -github.com/hashicorp/consul/sdk v0.14.1/go.mod h1:vFt03juSzocLRFo59NkeQHHmQa6+g7oU0pfzdI1mUhg= +github.com/hashicorp/consul/sdk v0.16.0 h1:SE9m0W6DEfgIVCJX7xU+iv/hUl4m/nxqMTnCdMxDpJ8= +github.com/hashicorp/consul/sdk v0.16.0/go.mod h1:7pxqqhqoaPqnBnzXD1StKed62LqJeClzVsUEy85Zr0A= github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A= github.com/hashicorp/cronexpr v1.1.2/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -1426,8 +1426,8 @@ github.com/smartcontractkit/go-plugin v0.0.0-20231003134350-e49dad63b306 h1:ko88 github.com/smartcontractkit/go-plugin v0.0.0-20231003134350-e49dad63b306/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4= github.com/smartcontractkit/grpc-proxy v0.0.0-20230731113816-f1be6620749f h1:hgJif132UCdjo8u43i7iPN1/MFnu49hv7lFGFftCHKU= github.com/smartcontractkit/grpc-proxy v0.0.0-20230731113816-f1be6620749f/go.mod h1:MvMXoufZAtqExNexqi4cjrNYE9MefKddKylxjS+//n0= -github.com/smartcontractkit/libocr v0.0.0-20240112202000-6359502d2ff1 h1:3y9WsXkZ5lxFrmfH7DQHs/q308lylKId5l/3VC0QAdM= -github.com/smartcontractkit/libocr v0.0.0-20240112202000-6359502d2ff1/go.mod h1:kC0qmVPUaVkFqGiZMNhmRmjdphuUmeyLEdlWFOQzFWI= +github.com/smartcontractkit/libocr v0.0.0-20240326191951-2bbe9382d052 h1:1WFjrrVrWoQ9UpVMh7Mx4jDpzhmo1h8hFUKd9awIhIU= +github.com/smartcontractkit/libocr v0.0.0-20240326191951-2bbe9382d052/go.mod h1:SJEZCHgMCAzzBvo9vMV2DQ9onfEcIJCYSViyP4JI6c4= github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230906073235-9e478e5e19f1 h1:yiKnypAqP8l0OX0P3klzZ7SCcBUxy5KqTAKZmQOvSQE= github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230906073235-9e478e5e19f1/go.mod h1:q6f4fe39oZPdsh1i57WznEZgxd8siidMaSFq3wdPmVg= github.com/smartcontractkit/tdh2/go/tdh2 v0.0.0-20230906073235-9e478e5e19f1 h1:Dai1bn+Q5cpeGMQwRdjOdVjG8mmFFROVkSKuUgBErRQ= @@ -1476,8 +1476,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -1489,8 +1490,9 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= diff --git a/monitoring/cmd/monitoring/main.go b/monitoring/cmd/monitoring/main.go index dd232b0f8..eb3c718c4 100644 --- a/monitoring/cmd/monitoring/main.go +++ b/monitoring/cmd/monitoring/main.go @@ -6,6 +6,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/erc20" "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/ocr2" "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/starknet" @@ -50,6 +51,16 @@ func main() { log.Fatalw("failed to build a ocr2.Client", "error", err) } + strTokenClient, err := erc20.NewClient( + starknetClient, + logger.With(log, "component", "erc20-client"), + starknetConfig.GetStrkTokenAddress(), + ) + + if err != nil { + log.Fatalw("failed to build erc20-client", "error", err) + } + envelopeSourceFactory := monitoring.NewEnvelopeSourceFactory(ocr2Client) txResultsFactory := monitoring.NewTxResultsSourceFactory(ocr2Client) @@ -70,11 +81,20 @@ func main() { proxySourceFactory := monitoring.NewProxySourceFactory(ocr2Client) monitor.SourceFactories = append(monitor.SourceFactories, proxySourceFactory) - prometheusExporterFactory := monitoring.NewPrometheusExporterFactory( - monitoring.NewMetrics(logger.With(log, "component", "starknet-metrics")), - ) + metricsBuilder := monitoring.NewMetrics(logger.With(log, "component", "starknet-metrics-builder")) + + prometheusExporterFactory := monitoring.NewPrometheusExporterFactory(metricsBuilder) monitor.ExporterFactories = append(monitor.ExporterFactories, prometheusExporterFactory) + nodeBalancesSourceFactory := monitoring.NewNodeBalancesSourceFactory(strTokenClient) + monitor.NetworkSourceFactories = append(monitor.NetworkSourceFactories, nodeBalancesSourceFactory) + + nodeBalancesExporterFactory := monitoring.NewNodeBalancesExporterFactory( + logger.With(log, "node-balances-exporter"), + metricsBuilder, + ) + monitor.NetworkExporterFactories = append(monitor.NetworkExporterFactories, nodeBalancesExporterFactory) + monitor.Run() log.Info("monitor stopped") } diff --git a/monitoring/pkg/monitoring/config_chain.go b/monitoring/pkg/monitoring/config_chain.go index b7c44a6d5..17b41644b 100644 --- a/monitoring/pkg/monitoring/config_chain.go +++ b/monitoring/pkg/monitoring/config_chain.go @@ -6,6 +6,8 @@ import ( "os" "time" + "github.com/NethermindEth/juno/core/felt" + starknetutils "github.com/NethermindEth/starknet.go/utils" relayMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring" ) @@ -18,18 +20,20 @@ type StarknetConfig struct { readTimeout time.Duration pollInterval time.Duration linkTokenAddress string + strkTokenAddress *felt.Felt } var _ relayMonitoring.ChainConfig = StarknetConfig{} -func (s StarknetConfig) GetRPCEndpoint() string { return s.rpcEndpoint } -func (s StarknetConfig) GetRPCApiKey() string { return s.rpcApiKey } -func (s StarknetConfig) GetNetworkName() string { return s.networkName } -func (s StarknetConfig) GetNetworkID() string { return s.networkID } -func (s StarknetConfig) GetChainID() string { return s.chainID } -func (s StarknetConfig) GetReadTimeout() time.Duration { return s.readTimeout } -func (s StarknetConfig) GetPollInterval() time.Duration { return s.pollInterval } -func (s StarknetConfig) GetLinkTokenAddress() string { return s.linkTokenAddress } +func (s StarknetConfig) GetRPCEndpoint() string { return s.rpcEndpoint } +func (s StarknetConfig) GetRPCApiKey() string { return s.rpcApiKey } +func (s StarknetConfig) GetNetworkName() string { return s.networkName } +func (s StarknetConfig) GetNetworkID() string { return s.networkID } +func (s StarknetConfig) GetChainID() string { return s.chainID } +func (s StarknetConfig) GetReadTimeout() time.Duration { return s.readTimeout } +func (s StarknetConfig) GetPollInterval() time.Duration { return s.pollInterval } +func (s StarknetConfig) GetLinkTokenAddress() string { return s.linkTokenAddress } +func (s StarknetConfig) GetStrkTokenAddress() *felt.Felt { return s.strkTokenAddress } func (s StarknetConfig) ToMapping() map[string]interface{} { return map[string]interface{}{ @@ -85,6 +89,13 @@ func parseEnvVars(cfg *StarknetConfig) error { if value, isPresent := os.LookupEnv("STARKNET_LINK_TOKEN_ADDRESS"); isPresent { cfg.linkTokenAddress = value } + if value, isPresent := os.LookupEnv("STRK_TOKEN_ADDRESS"); isPresent { + feltValue, err := starknetutils.HexToFelt(value) + if err != nil { + return fmt.Errorf("failed to parse env var STRK_TOKEN_ADDRESS %w", err) + } + cfg.strkTokenAddress = feltValue + } return nil } diff --git a/monitoring/pkg/monitoring/config_node.go b/monitoring/pkg/monitoring/config_node.go index 4611ba6a0..37e52fb6a 100644 --- a/monitoring/pkg/monitoring/config_node.go +++ b/monitoring/pkg/monitoring/config_node.go @@ -7,7 +7,7 @@ import ( "github.com/smartcontractkit/libocr/offchainreporting2/types" - relayMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring" + commonMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring" ) type StarknetNodeConfig struct { @@ -27,13 +27,13 @@ func (s StarknetNodeConfig) GetAccount() types.Account { return types.Account(address) } -func StarknetNodesParser(buf io.ReadCloser) ([]relayMonitoring.NodeConfig, error) { +func StarknetNodesParser(buf io.ReadCloser) ([]commonMonitoring.NodeConfig, error) { rawNodes := []StarknetNodeConfig{} decoder := json.NewDecoder(buf) if err := decoder.Decode(&rawNodes); err != nil { return nil, fmt.Errorf("unable to unmarshal nodes config data: %w", err) } - nodes := make([]relayMonitoring.NodeConfig, len(rawNodes)) + nodes := make([]commonMonitoring.NodeConfig, len(rawNodes)) for i, rawNode := range rawNodes { nodes[i] = rawNode } diff --git a/monitoring/pkg/monitoring/exporter_contract_balance.go b/monitoring/pkg/monitoring/exporter_contract_balance.go new file mode 100644 index 000000000..df538abc5 --- /dev/null +++ b/monitoring/pkg/monitoring/exporter_contract_balance.go @@ -0,0 +1,79 @@ +package monitoring + +import ( + "context" + "math/big" + "sync" + + commonMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring" +) + +func NewNodeBalancesExporterFactory(log commonMonitoring.Logger, metrics Metrics) commonMonitoring.ExporterFactory { + return &nodeBalancesExporterFactory{ + log, + metrics, + } +} + +type nodeBalancesExporterFactory struct { + log commonMonitoring.Logger + metrics Metrics +} + +func (f *nodeBalancesExporterFactory) NewExporter(params commonMonitoring.ExporterParams) (commonMonitoring.Exporter, error) { + return &nodeBalancesExporter{ + log: f.log, + metrics: f.metrics, + chainConfig: params.ChainConfig, + }, nil +} + +type nodeBalancesExporter struct { + log commonMonitoring.Logger + metrics Metrics + chainConfig commonMonitoring.ChainConfig + addrsSet []ContractAddressWithBalance + addrsMu sync.Mutex +} + +func (e *nodeBalancesExporter) Export(ctx context.Context, data interface{}) { + balanceEnvelope, isBalanceEnvelope := data.(BalanceEnvelope) + if !isBalanceEnvelope { + return + } + + decimals := balanceEnvelope.Decimals + divisor := new(big.Int).Exp(new(big.Int).SetUint64(10), decimals, nil) // 10^(decimals) + + for _, c := range balanceEnvelope.Contracts { + balanceAns := new(big.Int).Div(c.Balance, divisor) + + e.metrics.SetBalance( + toFloat64(balanceAns), + c.Address.String(), + c.Name, + e.chainConfig.GetNetworkID(), + e.chainConfig.GetNetworkName(), + e.chainConfig.GetChainID()) + } + + e.addrsMu.Lock() + defer e.addrsMu.Unlock() + + e.addrsSet = balanceEnvelope.Contracts + +} + +func (e *nodeBalancesExporter) Cleanup(_ context.Context) { + e.addrsMu.Lock() + defer e.addrsMu.Unlock() + + for _, c := range e.addrsSet { + e.metrics.CleanupBalance(c.Address.String(), c.Name, e.chainConfig.GetNetworkID(), e.chainConfig.GetNetworkName(), e.chainConfig.GetChainID()) + } +} + +func toFloat64(bignum *big.Int) float64 { + val, _ := new(big.Float).SetInt(bignum).Float64() + return val +} diff --git a/monitoring/pkg/monitoring/exporter_prometheus.go b/monitoring/pkg/monitoring/exporter_prometheus.go index 7fa1fdc2a..402772e43 100644 --- a/monitoring/pkg/monitoring/exporter_prometheus.go +++ b/monitoring/pkg/monitoring/exporter_prometheus.go @@ -88,7 +88,7 @@ func (p *prometheusExporter) Cleanup(_ context.Context) { p.addressesMu.Lock() defer p.addressesMu.Unlock() for address := range p.addressesSet { - p.metrics.Cleanup( + p.metrics.CleanupProxy( address, p.feedConfig.GetContractAddress(), p.chainConfig.GetChainID(), diff --git a/monitoring/pkg/monitoring/metrics.go b/monitoring/pkg/monitoring/metrics.go index d936d0596..32baed70f 100644 --- a/monitoring/pkg/monitoring/metrics.go +++ b/monitoring/pkg/monitoring/metrics.go @@ -11,7 +11,9 @@ import ( type Metrics interface { SetProxyAnswersRaw(answer float64, proxyContractAddress, feedID, chainID, contractStatus, contractType, feedName, feedPath, networkID, networkName string) SetProxyAnswers(answer float64, proxyContractAddress, feedID, chainID, contractStatus, contractType, feedName, feedPath, networkID, networkName string) - Cleanup(proxyContractAddress, feedID, chainID, contractStatus, contractType, feedName, feedPath, networkID, networkName string) + CleanupProxy(proxyContractAddress, feedID, chainID, contractStatus, contractType, feedName, feedPath, networkID, networkName string) + SetBalance(answer float64, contractAddress, alias, networkId, networkName, chainID string) + CleanupBalance(contractAddress, alias, networkId, networkName, chainID string) } var ( @@ -29,6 +31,13 @@ var ( }, []string{"proxy_contract_address", "feed_id", "chain_id", "contract_status", "contract_type", "feed_name", "feed_path", "network_id", "network_name"}, ) + contractBalance = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "strk_contract_balance", + Help: "Reports the latest STRK balance of a contract address", + }, + []string{"contract_address", "alias", "network_id", "network_name"}, + ) ) // NewMetrics does wisott @@ -40,6 +49,29 @@ type defaultMetrics struct { log relayMonitoring.Logger } +func (d *defaultMetrics) SetBalance(answer float64, contractAddress, alias, networkId, networkName, chainID string) { + contractBalance.With(prometheus.Labels{ + "contract_address": contractAddress, + "alias": alias, + "network_id": networkId, + "network_name": networkName, + "chain_id": chainID, + }).Set(answer) +} + +func (d *defaultMetrics) CleanupBalance(contractAddress, alias, networkId, networkName, chainID string) { + labels := prometheus.Labels{ + "contract_address": contractAddress, + "alias": alias, + "network_id": networkId, + "network_name": networkName, + "chain_id": chainID, + } + if !contractBalance.Delete(labels) { + d.log.Errorw("failed to delete metric", "name", "strk_contract_balance", "labels", labels) + } +} + func (d *defaultMetrics) SetProxyAnswersRaw(answer float64, proxyContractAddress, feedID, chainID, contractStatus, contractType, feedName, feedPath, networkID, networkName string) { proxyAnswersRaw.With(prometheus.Labels{ "proxy_contract_address": proxyContractAddress, @@ -68,7 +100,7 @@ func (d *defaultMetrics) SetProxyAnswers(answer float64, proxyContractAddress, f }).Set(answer) } -func (d *defaultMetrics) Cleanup( +func (d *defaultMetrics) CleanupProxy( proxyContractAddress, feedID, chainID, contractStatus, contractType string, feedName, feedPath, networkID, networkName string, ) { diff --git a/monitoring/pkg/monitoring/source_contract_balance.go b/monitoring/pkg/monitoring/source_contract_balance.go new file mode 100644 index 000000000..1c7d59d39 --- /dev/null +++ b/monitoring/pkg/monitoring/source_contract_balance.go @@ -0,0 +1,92 @@ +package monitoring + +import ( + "context" + "fmt" + "math/big" + + "github.com/NethermindEth/juno/core/felt" + starknetutils "github.com/NethermindEth/starknet.go/utils" + commonMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring" + "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/erc20" +) + +type ContractAddress struct { + Address *felt.Felt + Name string +} + +type ContractAddressWithBalance struct { + ContractAddress + Balance *big.Int +} + +type BalanceEnvelope struct { + Contracts []ContractAddressWithBalance + Decimals *big.Int +} + +func NewContractAddress(address string, name string) (ContractAddress, error) { + ans := ContractAddress{} + addr, err := starknetutils.HexToFelt(address) + if err != nil { + return ans, fmt.Errorf("error parsing contract address: %w", err) + } + ans.Address = addr + ans.Name = name + return ans, nil +} + +type nodeBalancesSourceFactory struct { + erc20Reader erc20.ERC20Reader +} + +func NewNodeBalancesSourceFactory(erc20Reader erc20.ERC20Reader) *nodeBalancesSourceFactory { + return &nodeBalancesSourceFactory{ + erc20Reader: erc20Reader, + } +} + +func (f *nodeBalancesSourceFactory) NewSource( + _ commonMonitoring.ChainConfig, + rddNodes []commonMonitoring.NodeConfig, +) (commonMonitoring.Source, error) { + var addrs []ContractAddress + + for _, n := range rddNodes { + addr, err := NewContractAddress(string(n.GetAccount()), n.GetName()) + if err != nil { + return nil, err + } + addrs = append(addrs, addr) + } + + return &contractBalancesSource{erc20Reader: f.erc20Reader, contracts: addrs}, nil +} + +func (f *nodeBalancesSourceFactory) GetType() string { + return "nodeBalances" +} + +// contract balances sources can be potentially reused for other contracts (not just the node account contracts) +type contractBalancesSource struct { + erc20Reader erc20.ERC20Reader + contracts []ContractAddress +} + +func (s *contractBalancesSource) Fetch(ctx context.Context) (interface{}, error) { + var cAns []ContractAddressWithBalance + for _, c := range s.contracts { + balance, err := s.erc20Reader.BalanceOf(ctx, c.Address) + if err != nil { + return nil, fmt.Errorf("could not fetch address balance %w", err) + } + cAns = append(cAns, ContractAddressWithBalance{c, balance}) + } + dAns, err := s.erc20Reader.Decimals(ctx) + if err != nil { + return nil, fmt.Errorf("could not fetch decimals %w", err) + } + + return BalanceEnvelope{Contracts: cAns, Decimals: dAns}, nil +} diff --git a/monitoring/pkg/monitoring/source_contract_balance_test.go b/monitoring/pkg/monitoring/source_contract_balance_test.go new file mode 100644 index 000000000..fafc3d801 --- /dev/null +++ b/monitoring/pkg/monitoring/source_contract_balance_test.go @@ -0,0 +1,60 @@ +package monitoring + +import ( + "context" + "math/big" + "testing" + + starknetutils "github.com/NethermindEth/starknet.go/utils" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + erc20Mocks "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/erc20/mocks" +) + +func TestContractBalancesSource(t *testing.T) { + // This test makes sure that the mapping between the response from the ocr2.Client + // method calls and the output of the Proxy source is correct. + + chainConfig := generateChainConfig() + nodeConfig := generateNodeConfig() + + // ocr2Reader := ocr2Mocks.NewOCR2Reader(t) + // ocr2Reader.On( + // "LatestRoundData", + // mock.Anything, // ctx + // proxyContractAddressFelt, + // ).Return(ocr2ClientLatestRoundDataResponseForProxy, nil).Once() + + erc20Reader := erc20Mocks.NewERC20Reader(t) + + for _, x := range nodeConfig { + nodeAddressFelt, err := starknetutils.HexToFelt(string(x.GetAccount())) + require.NoError(t, err) + + erc20Reader.On( + "BalanceOf", + mock.Anything, // ctx + nodeAddressFelt, // address + ).Return(new(big.Int).SetUint64(777), nil) + } + + erc20Reader.On( + "Decimals", + mock.Anything, // ctx + ).Return(new(big.Int).SetUint64(18), nil) + + factory := NewNodeBalancesSourceFactory(erc20Reader) + source, err := factory.NewSource(chainConfig, nodeConfig) + require.NoError(t, err) + rawBalanceEnvelope, err := source.Fetch(context.Background()) + require.NoError(t, err) + balanceEnvelope, ok := rawBalanceEnvelope.(BalanceEnvelope) + require.True(t, ok) + + require.Equal(t, balanceEnvelope.Decimals.Uint64(), uint64(18)) + + require.Equal(t, balanceEnvelope.Contracts[0].Balance.Uint64(), uint64(777)) + require.Equal(t, balanceEnvelope.Contracts[1].Balance.Uint64(), uint64(777)) + +} diff --git a/monitoring/pkg/monitoring/source_envelope_test.go b/monitoring/pkg/monitoring/source_envelope_test.go index dfb9b7034..77e329f20 100644 --- a/monitoring/pkg/monitoring/source_envelope_test.go +++ b/monitoring/pkg/monitoring/source_envelope_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/NethermindEth/juno/core/felt" starknetutils "github.com/NethermindEth/starknet.go/utils" "github.com/smartcontractkit/libocr/offchainreporting2/types" "github.com/stretchr/testify/mock" @@ -27,46 +28,81 @@ func TestEnvelopeSource(t *testing.T) { chainConfig := generateChainConfig() feedConfig := generateFeedConfig() + feedContractAddressFelt, err := starknetutils.HexToFelt(feedConfig.ContractAddress) + require.NoError(t, err) + + transmitterContractAddressFelt, err := starknetutils.HexToFelt("0x16715b5cc943835f196b7caf5a8aeb0e85b3f975dc43c14c90d0376e87eead1") + require.NoError(t, err) + + ocr2ClientNewTransmissionEventAtResponse := []ocr2.NewTransmissionEvent{ + { + RoundId: 0xf5b, + LatestAnswer: bigIntFromString("-900000000"), + Transmitter: transmitterContractAddressFelt, + LatestTimestamp: time.Date(2022, time.September, 27, 18, 51, 0, 0, time.Local), + Observers: []uint8{0x1, 0x2, 0x3, 0x4}, + ObservationsLen: 0x4, + Observations: []*big.Int{ + bigIntFromString("3618502788666131213697322783095070105623107215331596699973092056134972020481"), + bigIntFromString("3618502788666131213697322783095070105623107215331596699973092056134972020481"), + bigIntFromString("3618502788666131213697322783095070105623107215331596699973092056134972020481"), + bigIntFromString("3618502788666131213697322783095070105623107215331596699973092056134972020481"), + }, + JuelsPerFeeCoin: big.NewInt(451000), + GasPrice: big.NewInt(1), + ConfigDigest: types.ConfigDigest{0x0, 0x4, 0x18, 0xe5, 0x44, 0xab, 0xa8, 0x18, 0x15, 0xa5, 0x2b, 0xf0, 0x11, 0x58, 0xc6, 0x9b, 0x38, 0x8a, 0x48, 0x9f, 0x76, 0xd, 0xd8, 0x3d, 0x84, 0x3f, 0x1d, 0x31, 0x22, 0xdb, 0x78, 0xa}, + Epoch: 0x519, + Round: 0x5, + Reimbursement: big.NewInt(0), + }, + } + ocr2Reader := ocr2Mocks.NewOCR2Reader(t) ocr2Reader.On( "LatestRoundData", mock.Anything, // ctx - feedConfig.ContractAddress, + feedContractAddressFelt, ).Return(ocr2ClientLatestRoundDataResponse, nil).Once() ocr2Reader.On( "NewTransmissionsFromEventsAt", mock.Anything, // ctx - feedConfig.ContractAddress, + feedContractAddressFelt, ocr2ClientLatestRoundDataResponse.BlockNumber, ).Return(ocr2ClientNewTransmissionEventAtResponse, nil).Once() ocr2Reader.On( "LatestConfigDetails", mock.Anything, // ctx - feedConfig.ContractAddress, + feedContractAddressFelt, ).Return(ocr2ClientLatestConfigDetailsResponse, nil).Once() ocr2Reader.On( "ConfigFromEventAt", mock.Anything, // ctx - feedConfig.ContractAddress, + feedContractAddressFelt, ocr2ClientLatestConfigDetailsResponse.Block, ).Return(ocr2ClientConfigFromEventAtResponse, nil).Once() ocr2Reader.On( "LinkAvailableForPayment", mock.Anything, // ctx - feedConfig.ContractAddress, + feedContractAddressFelt, ).Return(ocr2ClientLinkAvailableForPaymentResponse, nil).Once() baseReader := starknetMocks.NewReader(t) ocr2Reader.On("BaseReader").Return(baseReader) + + linkTokenAddressFelt, err := starknetutils.HexToFelt(chainConfig.GetLinkTokenAddress()) + require.NoError(t, err) + + accountBalanceFelt, err := starknetutils.HexToFelt("0x56bc75e2d63100000") + require.NoError(t, err) + starknetReaderCallContractBalanceOfResponse := []*felt.Felt{accountBalanceFelt, &felt.Zero} + baseReader.On( "CallContract", mock.Anything, // ctx starknet.CallOps{ - ContractAddress: chainConfig.GetLinkTokenAddress(), - Selector: "balance_of", - Calldata: []string{ - starknetutils.HexToBN(feedConfig.ContractAddress).String(), - }, + ContractAddress: linkTokenAddressFelt, + Selector: starknetutils.GetSelectorFromNameFelt("balance_of"), + Calldata: []*felt.Felt{feedContractAddressFelt}, }, ).Return(starknetReaderCallContractBalanceOfResponse, nil) @@ -79,6 +115,7 @@ func TestEnvelopeSource(t *testing.T) { require.True(t, ok) require.Equal(t, expectedEnvelope, envelope) + } var ( @@ -89,28 +126,6 @@ var ( StartedAt: time.Date(2022, time.September, 27, 18, 50, 0, 0, time.Local), UpdatedAt: time.Date(2022, time.September, 27, 18, 51, 0, 0, time.Local), } - ocr2ClientNewTransmissionEventAtResponse = []ocr2.NewTransmissionEvent{ - { - RoundId: 0xf5b, - LatestAnswer: bigIntFromString("-900000000"), - Transmitter: starknetutils.StrToFelt("634447934223750826572902672583054702307815157196919304685470566142330202833"), - LatestTimestamp: time.Date(2022, time.September, 27, 18, 51, 0, 0, time.Local), - Observers: []uint8{0x1, 0x2, 0x3, 0x4}, - ObservationsLen: 0x4, - Observations: []*big.Int{ - bigIntFromString("3618502788666131213697322783095070105623107215331596699973092056134972020481"), - bigIntFromString("3618502788666131213697322783095070105623107215331596699973092056134972020481"), - bigIntFromString("3618502788666131213697322783095070105623107215331596699973092056134972020481"), - bigIntFromString("3618502788666131213697322783095070105623107215331596699973092056134972020481"), - }, - JuelsPerFeeCoin: big.NewInt(451000), - GasPrice: big.NewInt(1), - ConfigDigest: types.ConfigDigest{0x0, 0x4, 0x18, 0xe5, 0x44, 0xab, 0xa8, 0x18, 0x15, 0xa5, 0x2b, 0xf0, 0x11, 0x58, 0xc6, 0x9b, 0x38, 0x8a, 0x48, 0x9f, 0x76, 0xd, 0xd8, 0x3d, 0x84, 0x3f, 0x1d, 0x31, 0x22, 0xdb, 0x78, 0xa}, - Epoch: 0x519, - Round: 0x5, - Reimbursement: big.NewInt(0), - }, - } ocr2ClientLatestConfigDetailsResponse = ocr2.ContractConfigDetails{ Block: 0x11, Digest: types.ConfigDigest{0x0, 0x4, 0x18, 0xe5, 0x44, 0xab, 0xa8, 0x18, 0x15, 0xa5, 0x2b, 0xf0, 0x11, 0x58, 0xc6, 0x9b, 0x38, 0x8a, 0x48, 0x9f, 0x76, 0xd, 0xd8, 0x3d, 0x84, 0x3f, 0x1d, 0x31, 0x22, 0xdb, 0x78, 0xa}, @@ -120,11 +135,11 @@ var ( ConfigDigest: types.ConfigDigest{0x0, 0x4, 0x18, 0xe5, 0x44, 0xab, 0xa8, 0x18, 0x15, 0xa5, 0x2b, 0xf0, 0x11, 0x58, 0xc6, 0x9b, 0x38, 0x8a, 0x48, 0x9f, 0x76, 0xd, 0xd8, 0x3d, 0x84, 0x3f, 0x1d, 0x31, 0x22, 0xdb, 0x78, 0xa}, ConfigCount: 0x1, Signers: []types.OnchainPublicKey{ - types.OnchainPublicKey{0x6, 0x43, 0x41, 0xfa, 0xc3, 0x1, 0xc6, 0x2c, 0x38, 0xa9, 0xef, 0xdb, 0x86, 0xf6, 0xa2, 0x5a, 0x34, 0xd2, 0x4, 0x4f, 0x29, 0x2e, 0x94, 0xfb, 0xe4, 0x78, 0xa6, 0x67, 0x19, 0xb3, 0x80, 0x9e}, - types.OnchainPublicKey{0x1, 0xe2, 0xe1, 0x45, 0x47, 0x3, 0x7d, 0xb0, 0xd2, 0xe, 0xc6, 0xc9, 0x4b, 0xc7, 0x91, 0xea, 0xf2, 0xc9, 0x98, 0xad, 0x92, 0x79, 0xbb, 0xd, 0x21, 0x80, 0x15, 0x14, 0xd0, 0x6f, 0xa5, 0x7e}, - types.OnchainPublicKey{0x3, 0xb6, 0xcb, 0xd7, 0xbd, 0x52, 0x2d, 0xc8, 0xb0, 0xb4, 0x15, 0x3d, 0x60, 0x44, 0xec, 0xa7, 0x7e, 0x3e, 0xcf, 0xde, 0xe0, 0xc9, 0x5d, 0x20, 0x75, 0x50, 0x61, 0xf4, 0xbc, 0x7b, 0xf5, 0x4d}, - types.OnchainPublicKey{0x2, 0x4a, 0xa1, 0x21, 0x5b, 0xf4, 0xa3, 0xbb, 0x13, 0xea, 0x19, 0x57, 0x74, 0x28, 0xb7, 0xbe, 0xb5, 0xb9, 0x28, 0xb5, 0x74, 0x96, 0x78, 0xfa, 0x46, 0x87, 0x3b, 0x62, 0x7b, 0x22, 0x2a, 0x14}, - types.OnchainPublicKey{0x2, 0xa9, 0xc8, 0x4f, 0x88, 0x14, 0x17, 0xb5, 0xc9, 0xd1, 0x3b, 0x80, 0x2a, 0xc9, 0x93, 0xc5, 0x2c, 0x82, 0x88, 0x62, 0x32, 0xf7, 0x4e, 0x47, 0x5a, 0x92, 0xcc, 0x1a, 0xa, 0x1, 0x10, 0xda}, + {0x6, 0x43, 0x41, 0xfa, 0xc3, 0x1, 0xc6, 0x2c, 0x38, 0xa9, 0xef, 0xdb, 0x86, 0xf6, 0xa2, 0x5a, 0x34, 0xd2, 0x4, 0x4f, 0x29, 0x2e, 0x94, 0xfb, 0xe4, 0x78, 0xa6, 0x67, 0x19, 0xb3, 0x80, 0x9e}, + {0x1, 0xe2, 0xe1, 0x45, 0x47, 0x3, 0x7d, 0xb0, 0xd2, 0xe, 0xc6, 0xc9, 0x4b, 0xc7, 0x91, 0xea, 0xf2, 0xc9, 0x98, 0xad, 0x92, 0x79, 0xbb, 0xd, 0x21, 0x80, 0x15, 0x14, 0xd0, 0x6f, 0xa5, 0x7e}, + {0x3, 0xb6, 0xcb, 0xd7, 0xbd, 0x52, 0x2d, 0xc8, 0xb0, 0xb4, 0x15, 0x3d, 0x60, 0x44, 0xec, 0xa7, 0x7e, 0x3e, 0xcf, 0xde, 0xe0, 0xc9, 0x5d, 0x20, 0x75, 0x50, 0x61, 0xf4, 0xbc, 0x7b, 0xf5, 0x4d}, + {0x2, 0x4a, 0xa1, 0x21, 0x5b, 0xf4, 0xa3, 0xbb, 0x13, 0xea, 0x19, 0x57, 0x74, 0x28, 0xb7, 0xbe, 0xb5, 0xb9, 0x28, 0xb5, 0x74, 0x96, 0x78, 0xfa, 0x46, 0x87, 0x3b, 0x62, 0x7b, 0x22, 0x2a, 0x14}, + {0x2, 0xa9, 0xc8, 0x4f, 0x88, 0x14, 0x17, 0xb5, 0xc9, 0xd1, 0x3b, 0x80, 0x2a, 0xc9, 0x93, 0xc5, 0x2c, 0x82, 0x88, 0x62, 0x32, 0xf7, 0x4e, 0x47, 0x5a, 0x92, 0xcc, 0x1a, 0xa, 0x1, 0x10, 0xda}, }, Transmitters: []types.Account{ "0x033c95af529827a2372743bfc820b4d0bd08605fda5e089d1a7ad3a33caa48fc", @@ -140,9 +155,8 @@ var ( }, ConfigBlock: 0x11, } - starknetReaderCallContractBalanceOfResponse = []string{"0x56bc75e2d63100000", "0x0"} - ocr2ClientLinkAvailableForPaymentResponse = bigIntFromString("99999991552000000000") - expectedEnvelope = relayMonitoring.Envelope{ + ocr2ClientLinkAvailableForPaymentResponse = bigIntFromString("99999991552000000000") + expectedEnvelope = relayMonitoring.Envelope{ ConfigDigest: types.ConfigDigest{0x0, 0x4, 0x18, 0xe5, 0x44, 0xab, 0xa8, 0x18, 0x15, 0xa5, 0x2b, 0xf0, 0x11, 0x58, 0xc6, 0x9b, 0x38, 0x8a, 0x48, 0x9f, 0x76, 0xd, 0xd8, 0x3d, 0x84, 0x3f, 0x1d, 0x31, 0x22, 0xdb, 0x78, 0xa}, Epoch: 0x519, Round: 0x5, @@ -152,11 +166,11 @@ var ( ConfigDigest: types.ConfigDigest{0x0, 0x4, 0x18, 0xe5, 0x44, 0xab, 0xa8, 0x18, 0x15, 0xa5, 0x2b, 0xf0, 0x11, 0x58, 0xc6, 0x9b, 0x38, 0x8a, 0x48, 0x9f, 0x76, 0xd, 0xd8, 0x3d, 0x84, 0x3f, 0x1d, 0x31, 0x22, 0xdb, 0x78, 0xa}, ConfigCount: 0x1, Signers: []types.OnchainPublicKey{ - types.OnchainPublicKey{0x6, 0x43, 0x41, 0xfa, 0xc3, 0x1, 0xc6, 0x2c, 0x38, 0xa9, 0xef, 0xdb, 0x86, 0xf6, 0xa2, 0x5a, 0x34, 0xd2, 0x4, 0x4f, 0x29, 0x2e, 0x94, 0xfb, 0xe4, 0x78, 0xa6, 0x67, 0x19, 0xb3, 0x80, 0x9e}, - types.OnchainPublicKey{0x1, 0xe2, 0xe1, 0x45, 0x47, 0x3, 0x7d, 0xb0, 0xd2, 0xe, 0xc6, 0xc9, 0x4b, 0xc7, 0x91, 0xea, 0xf2, 0xc9, 0x98, 0xad, 0x92, 0x79, 0xbb, 0xd, 0x21, 0x80, 0x15, 0x14, 0xd0, 0x6f, 0xa5, 0x7e}, - types.OnchainPublicKey{0x3, 0xb6, 0xcb, 0xd7, 0xbd, 0x52, 0x2d, 0xc8, 0xb0, 0xb4, 0x15, 0x3d, 0x60, 0x44, 0xec, 0xa7, 0x7e, 0x3e, 0xcf, 0xde, 0xe0, 0xc9, 0x5d, 0x20, 0x75, 0x50, 0x61, 0xf4, 0xbc, 0x7b, 0xf5, 0x4d}, - types.OnchainPublicKey{0x2, 0x4a, 0xa1, 0x21, 0x5b, 0xf4, 0xa3, 0xbb, 0x13, 0xea, 0x19, 0x57, 0x74, 0x28, 0xb7, 0xbe, 0xb5, 0xb9, 0x28, 0xb5, 0x74, 0x96, 0x78, 0xfa, 0x46, 0x87, 0x3b, 0x62, 0x7b, 0x22, 0x2a, 0x14}, - types.OnchainPublicKey{0x2, 0xa9, 0xc8, 0x4f, 0x88, 0x14, 0x17, 0xb5, 0xc9, 0xd1, 0x3b, 0x80, 0x2a, 0xc9, 0x93, 0xc5, 0x2c, 0x82, 0x88, 0x62, 0x32, 0xf7, 0x4e, 0x47, 0x5a, 0x92, 0xcc, 0x1a, 0xa, 0x1, 0x10, 0xda}, + {0x6, 0x43, 0x41, 0xfa, 0xc3, 0x1, 0xc6, 0x2c, 0x38, 0xa9, 0xef, 0xdb, 0x86, 0xf6, 0xa2, 0x5a, 0x34, 0xd2, 0x4, 0x4f, 0x29, 0x2e, 0x94, 0xfb, 0xe4, 0x78, 0xa6, 0x67, 0x19, 0xb3, 0x80, 0x9e}, + {0x1, 0xe2, 0xe1, 0x45, 0x47, 0x3, 0x7d, 0xb0, 0xd2, 0xe, 0xc6, 0xc9, 0x4b, 0xc7, 0x91, 0xea, 0xf2, 0xc9, 0x98, 0xad, 0x92, 0x79, 0xbb, 0xd, 0x21, 0x80, 0x15, 0x14, 0xd0, 0x6f, 0xa5, 0x7e}, + {0x3, 0xb6, 0xcb, 0xd7, 0xbd, 0x52, 0x2d, 0xc8, 0xb0, 0xb4, 0x15, 0x3d, 0x60, 0x44, 0xec, 0xa7, 0x7e, 0x3e, 0xcf, 0xde, 0xe0, 0xc9, 0x5d, 0x20, 0x75, 0x50, 0x61, 0xf4, 0xbc, 0x7b, 0xf5, 0x4d}, + {0x2, 0x4a, 0xa1, 0x21, 0x5b, 0xf4, 0xa3, 0xbb, 0x13, 0xea, 0x19, 0x57, 0x74, 0x28, 0xb7, 0xbe, 0xb5, 0xb9, 0x28, 0xb5, 0x74, 0x96, 0x78, 0xfa, 0x46, 0x87, 0x3b, 0x62, 0x7b, 0x22, 0x2a, 0x14}, + {0x2, 0xa9, 0xc8, 0x4f, 0x88, 0x14, 0x17, 0xb5, 0xc9, 0xd1, 0x3b, 0x80, 0x2a, 0xc9, 0x93, 0xc5, 0x2c, 0x82, 0x88, 0x62, 0x32, 0xf7, 0x4e, 0x47, 0x5a, 0x92, 0xcc, 0x1a, 0xa, 0x1, 0x10, 0xda}, }, Transmitters: []types.Account{ "0x033c95af529827a2372743bfc820b4d0bd08605fda5e089d1a7ad3a33caa48fc", diff --git a/monitoring/pkg/monitoring/source_proxy_test.go b/monitoring/pkg/monitoring/source_proxy_test.go index 806357c8d..ef70b404a 100644 --- a/monitoring/pkg/monitoring/source_proxy_test.go +++ b/monitoring/pkg/monitoring/source_proxy_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/smartcontractkit/libocr/offchainreporting2/types" + starknetutils "github.com/NethermindEth/starknet.go/utils" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -21,12 +21,15 @@ func TestProxySource(t *testing.T) { chainConfig := generateChainConfig() feedConfig := generateFeedConfig() + proxyContractAddressFelt, err := starknetutils.HexToFelt(feedConfig.ProxyAddress) + require.NoError(t, err) + ocr2Reader := ocr2Mocks.NewOCR2Reader(t) ocr2Reader.On( - "LatestTransmissionDetails", + "LatestRoundData", mock.Anything, // ctx - feedConfig.ContractAddress, - ).Return(ocr2ClientLatestTransmissionDetailsResponseForProxy, nil).Once() + proxyContractAddressFelt, + ).Return(ocr2ClientLatestRoundDataResponseForProxy, nil).Once() factory := NewProxySourceFactory(ocr2Reader) source, err := factory.NewSource(chainConfig, feedConfig) @@ -37,17 +40,17 @@ func TestProxySource(t *testing.T) { require.True(t, ok) require.Equal(t, - ocr2ClientLatestTransmissionDetailsResponseForProxy.LatestAnswer.String(), + ocr2ClientLatestRoundDataResponseForProxy.Answer.String(), proxyData.Answer.String(), ) } var ( - ocr2ClientLatestTransmissionDetailsResponseForProxy = ocr2.TransmissionDetails{ - Digest: types.ConfigDigest{0x0, 0x4, 0x18, 0xe5, 0x44, 0xab, 0xa8, 0x18, 0x15, 0xa5, 0x2b, 0xf0, 0x11, 0x58, 0xc6, 0x9b, 0x38, 0x8a, 0x48, 0x9f, 0x76, 0xd, 0xd8, 0x3d, 0x84, 0x3f, 0x1d, 0x31, 0x22, 0xdb, 0x78, 0xa}, - Epoch: 0x1, - Round: 0x9, - LatestAnswer: big.NewInt(10000), - LatestTimestamp: time.Now(), + ocr2ClientLatestRoundDataResponseForProxy = ocr2.RoundData{ + RoundID: 9, + Answer: big.NewInt(10000), + BlockNumber: 777, + StartedAt: time.Now(), + UpdatedAt: time.Now(), } ) diff --git a/monitoring/pkg/monitoring/source_txresults_test.go b/monitoring/pkg/monitoring/source_txresults_test.go index d19465626..556c39d0a 100644 --- a/monitoring/pkg/monitoring/source_txresults_test.go +++ b/monitoring/pkg/monitoring/source_txresults_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + starknetutils "github.com/NethermindEth/starknet.go/utils" relayMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring" "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/ocr2" @@ -20,16 +21,19 @@ func TestTxResultsSource(t *testing.T) { chainConfig := generateChainConfig() feedConfig := generateFeedConfig() + feedContractAddressFelt, err := starknetutils.HexToFelt(feedConfig.ContractAddress) + require.NoError(t, err) + ocr2Reader := ocr2Mocks.NewOCR2Reader(t) ocr2Reader.On( "LatestRoundData", mock.Anything, // ctx - feedConfig.ContractAddress, + feedContractAddressFelt, ).Return(ocr2ClientLatestRoundDataResponseForTxResults1, nil).Once() ocr2Reader.On( "LatestRoundData", mock.Anything, // ctx - feedConfig.ContractAddress, + feedContractAddressFelt, ).Return(ocr2ClientLatestRoundDataResponseForTxResults2, nil).Once() factory := NewTxResultsSourceFactory(ocr2Reader) diff --git a/monitoring/pkg/monitoring/testutils.go b/monitoring/pkg/monitoring/testutils.go index 40d2a924b..31d04436e 100644 --- a/monitoring/pkg/monitoring/testutils.go +++ b/monitoring/pkg/monitoring/testutils.go @@ -5,8 +5,29 @@ import ( "math/big" "math/rand" "time" + + commonMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring" ) +func generateNodeConfig() []commonMonitoring.NodeConfig { + starknetNodes := []StarknetNodeConfig{ + { + ID: "node-0", + NodeAddress: []string{generateAddr()}, + }, + { + ID: "node-1", + NodeAddress: []string{generateAddr()}, + }, + } + + nodes := make([]commonMonitoring.NodeConfig, len(starknetNodes)) + for i, starknetNode := range starknetNodes { + nodes[i] = starknetNode + } + return nodes +} + func generateChainConfig() StarknetConfig { return StarknetConfig{ rpcEndpoint: "http://starknet/6969", diff --git a/relayer/go.mod b/relayer/go.mod index 37b885b91..3163168cd 100644 --- a/relayer/go.mod +++ b/relayer/go.mod @@ -8,12 +8,12 @@ require ( github.com/NethermindEth/juno v0.3.1 github.com/NethermindEth/starknet.go v0.7.1-0.20240401080518-34a506f3cfdb github.com/ethereum/go-ethereum v1.13.8 - github.com/hashicorp/go-plugin v1.5.2 + github.com/hashicorp/go-plugin v1.6.0 github.com/pelletier/go-toml/v2 v2.1.1 github.com/pkg/errors v0.9.1 github.com/smartcontractkit/chainlink-common v0.1.7-0.20240213113935-001c2f4befd4 github.com/smartcontractkit/libocr v0.0.0-20240112202000-6359502d2ff1 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.26.0 golang.org/x/exp v0.0.0-20231127185646-65229373498e @@ -58,7 +58,7 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.0-rc.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect - github.com/hashicorp/yamux v0.0.0-20200609203250-aecfd211c9ce // indirect + github.com/hashicorp/yamux v0.1.1 // indirect github.com/holiman/uint256 v1.2.4 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/klauspost/compress v1.17.2 // indirect @@ -83,7 +83,7 @@ require ( github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/supranational/blst v0.3.11 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/test-go/testify v1.1.4 // indirect diff --git a/relayer/go.sum b/relayer/go.sum index 08ac115be..4f44abe58 100644 --- a/relayer/go.sum +++ b/relayer/go.sum @@ -271,8 +271,8 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/yamux v0.0.0-20200609203250-aecfd211c9ce h1:7UnVY3T/ZnHUrfviiAgIUjg2PXxsQfs5bphsG8F7Keo= -github.com/hashicorp/yamux v0.0.0-20200609203250-aecfd211c9ce/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU= @@ -454,8 +454,9 @@ github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobt github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -464,8 +465,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= diff --git a/relayer/pkg/chainlink/erc20/client.go b/relayer/pkg/chainlink/erc20/client.go new file mode 100644 index 000000000..8e5b087c9 --- /dev/null +++ b/relayer/pkg/chainlink/erc20/client.go @@ -0,0 +1,87 @@ +package erc20 + +import ( + "context" + "fmt" + "math/big" + + "github.com/NethermindEth/juno/core/felt" + starknetutils "github.com/NethermindEth/starknet.go/utils" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/starknet" +) + +//go:generate mockery --name ERC20Reader --output ./mocks/ +type ERC20Reader interface { + BalanceOf(context.Context, *felt.Felt) (*big.Int, error) + Decimals(context.Context) (*big.Int, error) + + BaseReader() starknet.Reader +} + +var _ ERC20Reader = (*Client)(nil) + +type Client struct { + r starknet.Reader + lggr logger.Logger + tokenAddress *felt.Felt + decimals *big.Int +} + +func NewClient(reader starknet.Reader, lggr logger.Logger, tokenAddress *felt.Felt) (*Client, error) { + return &Client{ + r: reader, + lggr: lggr, + tokenAddress: tokenAddress, + // lazy initialize decimals + decimals: nil, + }, nil +} + +func (c *Client) BalanceOf(ctx context.Context, accountAddress *felt.Felt) (*big.Int, error) { + ops := starknet.CallOps{ + ContractAddress: c.tokenAddress, + Selector: starknetutils.GetSelectorFromNameFelt("balance_of"), + Calldata: []*felt.Felt{accountAddress}, + } + + balanceRes, err := c.r.CallContract(ctx, ops) + if err != nil { + return nil, fmt.Errorf("couldn't call balance_of on erc20: %w", err) + } + + if len(balanceRes) != 1 { + return nil, fmt.Errorf("unexpected data returned from balance_of on erc20") + } + + return starknetutils.FeltToBigInt(balanceRes[0]), nil + +} + +func (c *Client) Decimals(ctx context.Context) (*big.Int, error) { + if c.decimals == nil { + + ops := starknet.CallOps{ + ContractAddress: c.tokenAddress, + Selector: starknetutils.GetSelectorFromNameFelt("decimals"), + } + + decimalsRes, err := c.r.CallContract(ctx, ops) + + if err != nil { + return nil, fmt.Errorf("couldn't call decimals on erc20: %w", err) + } + + if len(decimalsRes) != 1 { + return nil, fmt.Errorf("unexpected data returned from decimals on erc20") + } + + c.decimals = starknetutils.FeltToBigInt(decimalsRes[0]) + } + + return c.decimals, nil +} + +func (c *Client) BaseReader() starknet.Reader { + return c.r +} diff --git a/relayer/pkg/chainlink/erc20/client_test.go b/relayer/pkg/chainlink/erc20/client_test.go new file mode 100644 index 000000000..015fd43c2 --- /dev/null +++ b/relayer/pkg/chainlink/erc20/client_test.go @@ -0,0 +1,99 @@ +package erc20 + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/NethermindEth/juno/core/felt" + starknetutils "github.com/NethermindEth/starknet.go/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/starknet" +) + +func TestERC20Client(t *testing.T) { + chainID := "SN_SEPOLIA" + lggr := logger.Test(t) + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + req, _ := io.ReadAll(r.Body) + fmt.Println(r.RequestURI, r.URL, string(req)) + + var out []byte + + switch { + case r.RequestURI == "/": + type Request struct { + Selector string `json:"entry_point_selector"` + } + type Call struct { + Method string `json:"method"` + Params []json.RawMessage `json:"params"` + } + + call := Call{} + require.NoError(t, json.Unmarshal(req, &call)) + + switch call.Method { + case "starknet_call": + raw := call.Params[0] + reqdata := Request{} + err := json.Unmarshal([]byte(raw), &reqdata) + require.NoError(t, err) + + fmt.Printf("%v %v\n", reqdata.Selector, starknetutils.GetSelectorFromNameFelt("latest_transmission_details").String()) + switch reqdata.Selector { + case starknetutils.GetSelectorFromNameFelt("decimals").String(): + // latest transmission details response + out = []byte(`{"result":["0x1"]}`) + case starknetutils.GetSelectorFromNameFelt("balance_of").String(): + // latest transmission details response + out = []byte(`{"result":["0x0"]}`) + default: + require.False(t, true, "unsupported contract method %s", reqdata.Selector) + } + default: + require.False(t, true, "unsupported RPC method") + } + default: + require.False(t, true, "unsupported endpoint") + } + + _, err := w.Write(out) + require.NoError(t, err) + })) + defer mockServer.Close() + + url := mockServer.URL + duration := 10 * time.Second + reader, err := starknet.NewClient(chainID, url, "", lggr, &duration) + require.NoError(t, err) + client, err := NewClient(reader, lggr, &felt.Zero) + assert.NoError(t, err) + + // contractAddress, err := starknetutils.HexToFelt(ocr2ContractAddress) + // require.NoError(t, err) + + t.Run("get balance", func(t *testing.T) { + balance, err := client.BalanceOf(context.Background(), &felt.Zero) + require.NoError(t, err) + require.Equal(t, uint64(0), balance.Uint64()) + // require.Equal(t, new(big.Int), balance) + }) + + t.Run("get decimals", func(t *testing.T) { + decimals, err := client.Decimals(context.Background()) + require.NoError(t, err) + require.Equal(t, uint64(1), decimals.Uint64()) + }) + +} diff --git a/relayer/pkg/chainlink/erc20/mocks/ERC20Reader.go b/relayer/pkg/chainlink/erc20/mocks/ERC20Reader.go new file mode 100644 index 000000000..a884a455e --- /dev/null +++ b/relayer/pkg/chainlink/erc20/mocks/ERC20Reader.go @@ -0,0 +1,102 @@ +// Code generated by mockery v2.22.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + big "math/big" + + felt "github.com/NethermindEth/juno/core/felt" + + mock "github.com/stretchr/testify/mock" + + starknet "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/starknet" +) + +// ERC20Reader is an autogenerated mock type for the ERC20Reader type +type ERC20Reader struct { + mock.Mock +} + +// BalanceOf provides a mock function with given fields: _a0, _a1 +func (_m *ERC20Reader) BalanceOf(_a0 context.Context, _a1 *felt.Felt) (*big.Int, error) { + ret := _m.Called(_a0, _a1) + + var r0 *big.Int + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *felt.Felt) (*big.Int, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *felt.Felt) *big.Int); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *felt.Felt) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BaseReader provides a mock function with given fields: +func (_m *ERC20Reader) BaseReader() starknet.Reader { + ret := _m.Called() + + var r0 starknet.Reader + if rf, ok := ret.Get(0).(func() starknet.Reader); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(starknet.Reader) + } + } + + return r0 +} + +// Decimals provides a mock function with given fields: _a0 +func (_m *ERC20Reader) Decimals(_a0 context.Context) (*big.Int, error) { + ret := _m.Called(_a0) + + var r0 *big.Int + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*big.Int, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(context.Context) *big.Int); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewERC20Reader interface { + mock.TestingT + Cleanup(func()) +} + +// NewERC20Reader creates a new instance of ERC20Reader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewERC20Reader(t mockConstructorTestingTNewERC20Reader) *ERC20Reader { + mock := &ERC20Reader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/relayer/pkg/chainlink/ocr2/mocks/OCR2Reader.go b/relayer/pkg/chainlink/ocr2/mocks/OCR2Reader.go index ea85761de..c8f14eb81 100644 --- a/relayer/pkg/chainlink/ocr2/mocks/OCR2Reader.go +++ b/relayer/pkg/chainlink/ocr2/mocks/OCR2Reader.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.29.0. DO NOT EDIT. +// Code generated by mockery v2.22.1. DO NOT EDIT. package mocks