diff --git a/example/Makefile b/example/Makefile index 51e9b741..4e861f3e 100644 --- a/example/Makefile +++ b/example/Makefile @@ -1,5 +1,5 @@ -build: chain-indexing-app -chain-indexing-app: +build: example-app +example-app: go build ./app/example-app/ clean: rm -i example-app diff --git a/example/app/example-app/config/bridgesapi.go b/example/app/example-app/config/bridgesapi.go new file mode 100644 index 00000000..f16ce225 --- /dev/null +++ b/example/app/example-app/config/bridgesapi.go @@ -0,0 +1,60 @@ +package config + +import ( + "github.com/crypto-com/chain-indexing/external/bridges/parsers" + "github.com/crypto-com/chain-indexing/external/bridges/parsers/cronos" + "github.com/crypto-com/chain-indexing/infrastructure/httpapi/handlers" +) + +type BridgesAPIConfig struct { + Networks []BridgeNetworkConfig `yaml:"networks"` + Chains []handlers.BridgeChainConfig `yaml:"chains"` +} + +type BridgeNetworkConfig struct { + ChainName string `yaml:"chain_name" mapstructure:"chain_name"` + // Chain network unique abbreviation, used in URL query params + Abbreviation handlers.NetworkAbbreviation `yaml:"abbreviation" mapstructure:"abbreviation"` + MaybeAddressHookKey *string `yaml:"maybe_address_hook_key"` + MaybeCronosAccountAddressPrefix *string `yaml:"maybe_cronos_account_address_prefix"` +} + +var BridgeAddressHooks = (func() map[string]func( + networkConfig *BridgeNetworkConfig, +) handlers.AddressHook { + hooks := make(map[string]func(config *BridgeNetworkConfig) handlers.AddressHook, 0) + + hooks["DefaultLowercaseAddressHook"] = func(_ *BridgeNetworkConfig) handlers.AddressHook { + return parsers.DefaultLowercaseAddressHook + } + hooks["DefaultCronosEVMAddressHookGenerator"] = func(config *BridgeNetworkConfig) handlers.AddressHook { + return cronos.DefaultCronosEVMAddressHookGenerator(*config.MaybeCronosAccountAddressPrefix) + } + + return hooks +})() + +func ParseBridgesConfig(rawConfig *BridgesAPIConfig) handlers.BridgesConfig { + config := handlers.BridgesConfig{ + Networks: make([]handlers.BridgeNetworkConfig, 0, len(rawConfig.Networks)), + Chains: rawConfig.Chains, + } + + for i, network := range rawConfig.Networks { + if network.MaybeAddressHookKey == nil { + config.Networks = append(config.Networks, handlers.BridgeNetworkConfig{ + ChainName: network.ChainName, + Abbreviation: network.Abbreviation, + }) + } else { + hook := BridgeAddressHooks[*network.MaybeAddressHookKey](&rawConfig.Networks[i]) + config.Networks = append(config.Networks, handlers.BridgeNetworkConfig{ + ChainName: network.ChainName, + Abbreviation: network.Abbreviation, + MaybeAddressHook: &hook, + }) + } + } + + return config +} diff --git a/example/app/example-app/config.go b/example/app/example-app/config/config.go similarity index 96% rename from example/app/example-app/config.go rename to example/app/example-app/config/config.go index 0deb7ebf..ec139dbf 100644 --- a/example/app/example-app/config.go +++ b/example/app/example-app/config/config.go @@ -1,4 +1,4 @@ -package main +package config import ( "github.com/crypto-com/chain-indexing/bootstrap/config" @@ -6,6 +6,7 @@ import ( type CustomConfig struct { ServerGithubAPI ServerGithubAPIConfig `yaml:"server_github_api"` + BridgeAPI BridgesAPIConfig `yaml:"bridges_api"` } type ServerGithubAPIConfig struct { diff --git a/example/app/example-app/cronjobs.go b/example/app/example-app/cronjobs.go index 20e4c9b0..5ff4d581 100644 --- a/example/app/example-app/cronjobs.go +++ b/example/app/example-app/cronjobs.go @@ -65,7 +65,7 @@ func InitCronJob(name string, params InitCronJobParams) projection_entity.CronJo params.Logger.Panicf(err.Error()) } - return bridge_activity_matcher.New(params.Logger, params.RdbConn, migrationHelper, config) + return bridge_activity_matcher.New(config, params.Logger, params.RdbConn, migrationHelper) // register more cronjobs here default: panic(fmt.Sprintf("Unrecognized cron job: %s", name)) diff --git a/example/app/example-app/projections.go b/example/app/example-app/projections.go index 287a7013..a6df0c76 100644 --- a/example/app/example-app/projections.go +++ b/example/app/example-app/projections.go @@ -28,15 +28,18 @@ import ( "github.com/crypto-com/chain-indexing/projection/proposal" "github.com/crypto-com/chain-indexing/projection/transaction" "github.com/crypto-com/chain-indexing/projection/validator" + "github.com/crypto-com/chain-indexing/projection/validator_delegation" "github.com/crypto-com/chain-indexing/projection/validatorstats" "github.com/ettle/strcase" + + appconfig "github.com/crypto-com/chain-indexing/example/app/example-app/config" ) func initProjections( logger applogger.Logger, rdbConn rdb.Conn, config *configuration.Config, - customConfig *CustomConfig, + customConfig *appconfig.CustomConfig, ) []projection_entity.Projection { if !config.IndexService.Enable { return []projection_entity.Projection{} @@ -60,9 +63,10 @@ func initProjections( ExtraConfigs: config.IndexService.Projection.ExtraConfigs, - CosmosAppClient: cosmosAppClient, - AccountAddressPrefix: config.Blockchain.AccountAddressPrefix, - ConsNodeAddressPrefix: config.Blockchain.ConNodeAddressPrefix, + CosmosAppClient: cosmosAppClient, + AccountAddressPrefix: config.Blockchain.AccountAddressPrefix, + ValidatorAddressPrefix: config.Blockchain.ValidatorAddressPrefix, + ConsNodeAddressPrefix: config.Blockchain.ConNodeAddressPrefix, GithubAPIUser: config.IndexService.GithubAPI.Username, GithubAPIToken: config.IndexService.GithubAPI.Token, @@ -320,7 +324,28 @@ func InitProjection(name string, params InitProjectionParams) projection_entity. params.Logger.Panicf(err.Error()) } - return bridge_pending_activity.NewBridgePendingActivity(params.Logger, params.RdbConn, migrationHelper, config) + return bridge_pending_activity.New(config, params.Logger, params.RdbConn, migrationHelper) + case "ValidatorDelegation": + sourceURL := github_migrationhelper.GenerateDefaultSourceURL(name, githubMigrationHelperConfig) + databaseURL := migrationhelper.GenerateDefaultDatabaseURL(name, connString) + migrationHelper := github_migrationhelper.NewGithubMigrationHelper(sourceURL, databaseURL) + + customConfig, err := validator_delegation.CustomConfigFromInterface(params.ExtraConfigs[name]) + if err != nil { + params.Logger.Panicf(err.Error()) + } + + config, err := validator_delegation.PrepareConfig( + customConfig, + params.AccountAddressPrefix, + params.ValidatorAddressPrefix, + params.ConsNodeAddressPrefix, + ) + if err != nil { + params.Logger.Panicf(err.Error()) + } + + return validator_delegation.NewValidatorDelegation(params.Logger, params.RdbConn, config, migrationHelper) } return nil @@ -348,9 +373,10 @@ type InitProjectionParams struct { ExtraConfigs map[string]interface{} - CosmosAppClient cosmosapp.Client - AccountAddressPrefix string - ConsNodeAddressPrefix string + CosmosAppClient cosmosapp.Client + AccountAddressPrefix string + ValidatorAddressPrefix string + ConsNodeAddressPrefix string GithubAPIUser string GithubAPIToken string diff --git a/example/app/example-app/routes/routes.go b/example/app/example-app/routes/routes.go index 45a75823..87aeaec2 100644 --- a/example/app/example-app/routes/routes.go +++ b/example/app/example-app/routes/routes.go @@ -6,17 +6,20 @@ import ( "github.com/crypto-com/chain-indexing/appinterface/tendermint" "github.com/crypto-com/chain-indexing/bootstrap" "github.com/crypto-com/chain-indexing/bootstrap/config" - custom_httpapi_handlers "github.com/crypto-com/chain-indexing/example/httpapi/handlers" applogger "github.com/crypto-com/chain-indexing/external/logger" cosmosapp_infrastructure "github.com/crypto-com/chain-indexing/infrastructure/cosmosapp" httpapi_handlers "github.com/crypto-com/chain-indexing/infrastructure/httpapi/handlers" tendermint_infrastructure "github.com/crypto-com/chain-indexing/infrastructure/tendermint" + + appconfig "github.com/crypto-com/chain-indexing/example/app/example-app/config" + custom_httpapi_handlers "github.com/crypto-com/chain-indexing/example/httpapi/handlers" ) func InitRouteRegistry( logger applogger.Logger, rdbConn rdb.Conn, config *config.Config, + customConfig *appconfig.CustomConfig, ) bootstrap.RouteRegistry { var cosmosAppClient cosmosapp.Client if config.CosmosApp.Insecure { @@ -335,6 +338,7 @@ func InitRouteRegistry( logger, rdbConn.ToHandle(), accountAddressPrefix, + appconfig.ParseBridgesConfig(&customConfig.BridgeAPI), ) routes = append(routes, Route{ @@ -366,5 +370,45 @@ func InitRouteRegistry( }, ) + validatorDelegationHandler := httpapi_handlers.NewValidatorDelegation( + logger, + config.Blockchain.AccountAddressPrefix, + config.Blockchain.ValidatorAddressPrefix, + config.Blockchain.ConNodeAddressPrefix, + rdbConn.ToHandle(), + ) + routes = append(routes, + Route{ + Method: GET, + path: "api/test/validators", + handler: validatorDelegationHandler.ListValidator, + }, + Route{ + Method: GET, + path: "api/test/validators/{address}", + handler: validatorDelegationHandler.FindValidatorByAddress, + }, + Route{ + Method: GET, + path: "api/test/validators/{address}/delegations", + handler: validatorDelegationHandler.ListDelegationByValidator, + }, + Route{ + Method: GET, + path: "api/test/validators/{address}/unbonding_delegations", + handler: validatorDelegationHandler.ListUnbondingDelegationByValidator, + }, + Route{ + Method: GET, + path: "api/test/validators/{srcValAddress}/redelegations", + handler: validatorDelegationHandler.ListRedelegationBySrcValidator, + }, + Route{ + Method: GET, + path: "api/test/delegators/{address}/delegations", + handler: validatorDelegationHandler.ListDelegationByDelegator, + }, + ) + return &RouteRegistry{routes: routes} } diff --git a/example/app/example-app/run.go b/example/app/example-app/run.go index 3d128f74..f8d3ca08 100644 --- a/example/app/example-app/run.go +++ b/example/app/example-app/run.go @@ -5,15 +5,17 @@ import ( "os" "path/filepath" - "github.com/crypto-com/chain-indexing/bootstrap" - configuration "github.com/crypto-com/chain-indexing/bootstrap/config" - "github.com/crypto-com/chain-indexing/example/app/example-app/routes" - "github.com/crypto-com/chain-indexing/example/internal/filereader/yaml" "github.com/urfave/cli/v2" + "github.com/crypto-com/chain-indexing/bootstrap" + configuration "github.com/crypto-com/chain-indexing/bootstrap/config" applogger "github.com/crypto-com/chain-indexing/external/logger" "github.com/crypto-com/chain-indexing/external/primptr" "github.com/crypto-com/chain-indexing/infrastructure" + + appconfig "github.com/crypto-com/chain-indexing/example/app/example-app/config" + "github.com/crypto-com/chain-indexing/example/app/example-app/routes" + "github.com/crypto-com/chain-indexing/example/internal/filereader/yaml" ) func run(args []string) error { @@ -100,13 +102,13 @@ func run(args []string) error { return fmt.Errorf("error config from yaml: %v", err) } - var customConfig CustomConfig + var customConfig appconfig.CustomConfig err = yaml.FromYAMLFile(configPath, &customConfig) if err != nil { return fmt.Errorf("error custom config from yaml: %v", err) } - cliConfig := CLIConfig{ + cliConfig := appconfig.CLIConfig{ LogLevel: ctx.String("logLevel"), DatabaseHost: ctx.String("dbHost"), @@ -131,7 +133,7 @@ func run(args []string) error { cliConfig.DatabasePort = primptr.Int32(int32(ctx.Int("dbPort"))) } - OverrideByCLIConfig(&config, &cliConfig) + appconfig.OverrideByCLIConfig(&config, &cliConfig) // Create logger logLevel := parseLogLevel(config.Logger.Level) @@ -144,7 +146,7 @@ func run(args []string) error { initProjections(logger, app.GetRDbConn(), &config, &customConfig), initCronJobs(logger, app.GetRDbConn(), &config), ) - app.InitHTTPAPIServer(routes.InitRouteRegistry(logger, app.GetRDbConn(), &config)) + app.InitHTTPAPIServer(routes.InitRouteRegistry(logger, app.GetRDbConn(), &config, &customConfig)) app.Run() diff --git a/example/config/config.yaml b/example/config/config.yaml index 08629f4b..5086aa57 100644 --- a/example/config/config.yaml +++ b/example/config/config.yaml @@ -28,13 +28,14 @@ index_service: "Validator", "ValidatorStats", "NFT", - # "CryptoComNFT", + # "CryptoComNFT", "IBCChannel", - # "IBCChannelTxMsgTrace", + # "IBCChannelTxMsgTrace", "IBCChannelMessage", "BridgePendingActivity", "Example", - ] + # "ValidatorDelegation", + ] extra_configs: BridgePendingActivity: this_chain_id: "testnet-croeseid-4" @@ -42,8 +43,13 @@ index_service: counterparty_chain_name: "Cronos" channel_id: "channel-131" starting_height: 899374 + ValidatorDelegation: + unbonding_time: "2419200000000000ns" + slash_fraction_double_sign: "0.050000000000000000" + slash_fraction_downtime: "0.000000000000000000" + default_power_reduction: "1000000" cronjob: - enables: [ ] + enables: [] cosmos_version_enabled_height: v0_42_7: 0 github_api: @@ -59,9 +65,10 @@ http_service: # A list of origins a cross-domain request is allowed to be requested from # Default value '[]' disables CORS support # Use '["*"]' to allow request from any origin - cors_allowed_origins: [ ] - cors_allowed_methods: [ "HEAD", "GET" ] - cors_allowed_headers: [ "Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time" ] + cors_allowed_origins: [] + cors_allowed_methods: ["HEAD", "GET"] + cors_allowed_headers: + ["Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time"] tendermint_app: http_rpc_url: "https://testnet-croeseid-4.crypto.org:26657" @@ -105,4 +112,4 @@ prometheus: # Custom config for example server_github_api: - migration_repo_ref: "" \ No newline at end of file + migration_repo_ref: "" diff --git a/example/go.mod b/example/go.mod index c3a910d1..c5c8a147 100644 --- a/example/go.mod +++ b/example/go.mod @@ -6,13 +6,10 @@ replace github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.2-alp replace github.com/rs/zerolog => github.com/rs/zerolog v1.23.0 -require ( - github.com/BurntSushi/toml v0.4.1 - github.com/urfave/cli/v2 v2.3.0 -) +require github.com/urfave/cli/v2 v2.3.0 require ( - github.com/crypto-com/chain-indexing v1.3.0 + github.com/crypto-com/chain-indexing v1.3.5-0.20220221024425-38ae3fe094d6 github.com/ettle/strcase v0.1.1 github.com/valyala/fasthttp v1.17.0 gopkg.in/yaml.v2 v2.4.0 @@ -38,9 +35,10 @@ require ( github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.1.1 // indirect github.com/confio/ics23/go v0.6.6 // indirect - github.com/cosmos/cosmos-sdk v0.44.2 // indirect + github.com/cosmos/btcutil v1.0.4 // indirect + github.com/cosmos/cosmos-sdk v0.45.0 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect - github.com/cosmos/iavl v0.17.1 // indirect + github.com/cosmos/iavl v0.17.3 // indirect github.com/cosmos/ibc-go v1.2.1 // indirect github.com/cosmos/ledger-cosmos-go v0.11.1 // indirect github.com/cosmos/ledger-go v0.9.2 // indirect @@ -55,7 +53,6 @@ require ( github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/dvsekhvalnov/jose2go v0.0.0-20200901110807-248326c1351b // indirect - github.com/enigmampc/btcutil v1.0.3-0.20200723161021-e2fb6adb2a25 // indirect github.com/ethereum/go-ethereum v1.10.3 // indirect github.com/fasthttp/router v1.3.3 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect @@ -74,8 +71,10 @@ require ( github.com/google/uuid v1.3.0 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect + github.com/gravity-devs/liquidity v1.4.5 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.0.1 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/gtank/merlin v0.1.1 // indirect github.com/gtank/ristretto255 v0.1.2 // indirect @@ -150,7 +149,7 @@ require ( github.com/tendermint/btcd v0.1.1 // indirect github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 // indirect github.com/tendermint/go-amino v0.16.0 // indirect - github.com/tendermint/tendermint v0.34.13 // indirect + github.com/tendermint/tendermint v0.34.14 // indirect github.com/tendermint/tm-db v0.6.4 // indirect github.com/tharsis/ethermint v0.7.1 // indirect github.com/tklauser/go-sysconf v0.3.7 // indirect @@ -167,7 +166,7 @@ require ( golang.org/x/text v0.3.7 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/genproto v0.0.0-20211013025323-ce878158c4d4 // indirect - google.golang.org/grpc v1.41.0 // indirect + google.golang.org/grpc v1.42.0 // indirect google.golang.org/protobuf v1.27.1 // indirect gopkg.in/ini.v1 v1.63.2 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect diff --git a/example/go.sum b/example/go.sum index 42de6b74..18d32b65 100644 --- a/example/go.sum +++ b/example/go.sum @@ -95,7 +95,6 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d h1:nalkkPQcITbvhmL4+C4cKA87NW0tfm3Kl9VXRoPywFg= @@ -329,15 +328,19 @@ github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/coinbase/rosetta-sdk-go v0.6.10 h1:rgHD/nHjxLh0lMEdfGDqpTtlvtSBwULqrrZ2qPdNaCM= github.com/coinbase/rosetta-sdk-go v0.6.10/go.mod h1:J/JFMsfcePrjJZkwQFLh+hJErkAmdm9Iyy3D5Y0LfXo= +github.com/coinbase/rosetta-sdk-go v0.7.0 h1:lmTO/JEpCvZgpbkOITL95rA80CPKb5CtMzLaqF2mCNg= +github.com/coinbase/rosetta-sdk-go v0.7.0/go.mod h1:7nD3oBPIiHqhRprqvMgPoGxe/nyq3yftRmpsy29coWE= github.com/confio/ics23/go v0.0.0-20200817220745-f173e6211efb/go.mod h1:E45NqnlpxGnpfTWL/xauN7MRwEE28T4Dd4uraToOaKg= github.com/confio/ics23/go v0.6.3/go.mod h1:E45NqnlpxGnpfTWL/xauN7MRwEE28T4Dd4uraToOaKg= github.com/confio/ics23/go v0.6.6 h1:pkOy18YxxJ/r0XFDCnrl4Bjv6h4LkBSpLS6F38mrKL8= @@ -451,10 +454,13 @@ github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cosmos/btcutil v1.0.4 h1:n7C2ngKXo7UC9gNyMNLbzqz7Asuf+7Qv4gnX/rOdQ44= +github.com/cosmos/btcutil v1.0.4/go.mod h1:Ffqc8Hn6TJUdDgHBwIZLtrLQC1KdJ9jGJl/TvgUaxbU= github.com/cosmos/cosmos-sdk v0.43.0/go.mod h1:ctcrTEAhei9s8O3KSNvL0dxe+fVQGp07QyRb/7H9JYE= github.com/cosmos/cosmos-sdk v0.44.1/go.mod h1:fwQJdw+aECatpTvQTo1tSfHEsxACdZYU80QCZUPnHr4= -github.com/cosmos/cosmos-sdk v0.44.2 h1:EWoj9h9Q9t7uqS3LyqzZWWwnSEodUJlYDMloDoPBD3Y= github.com/cosmos/cosmos-sdk v0.44.2/go.mod h1:fwQJdw+aECatpTvQTo1tSfHEsxACdZYU80QCZUPnHr4= +github.com/cosmos/cosmos-sdk v0.45.0 h1:DHD+CIRZ+cYgiLXuTEUL/aprnfPsWSwaww/fIZEsZlk= +github.com/cosmos/cosmos-sdk v0.45.0/go.mod h1:XXS/asyCqWNWkx2rW6pSuen+EVcpAFxq6khrhnZgHaQ= github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= @@ -462,8 +468,9 @@ github.com/cosmos/iavl v0.15.0-rc3.0.20201009144442-230e9bdf52cd/go.mod h1:3xOIa github.com/cosmos/iavl v0.15.0-rc5/go.mod h1:WqoPL9yPTQ85QBMT45OOUzPxG/U/JcJoN7uMjgxke/I= github.com/cosmos/iavl v0.15.3/go.mod h1:OLjQiAQ4fGD2KDZooyJG9yz+p2ao2IAYSbke8mVvSA4= github.com/cosmos/iavl v0.16.0/go.mod h1:2A8O/Jz9YwtjqXMO0CjnnbTYEEaovE8jWcwrakH3PoE= -github.com/cosmos/iavl v0.17.1 h1:b/Cl8h1PRMvsu24+TYNlKchIu7W6tmxIBGe6E9u2Ybw= github.com/cosmos/iavl v0.17.1/go.mod h1:7aisPZK8yCpQdy3PMvKeO+bhq1NwDjUwjzxwwROUxFk= +github.com/cosmos/iavl v0.17.3 h1:s2N819a2olOmiauVa0WAhoIJq9EhSXE9HDBAoR9k+8Y= +github.com/cosmos/iavl v0.17.3/go.mod h1:prJoErZFABYZGDHka1R6Oay4z9PrNeFFiMKHDAMOi4w= github.com/cosmos/ibc-go v1.0.0/go.mod h1:2wHKQUa+BLJMEyN635KrHfmTTwSNHBtXcqdY8JWGuXA= github.com/cosmos/ibc-go v1.0.1/go.mod h1:pfLnoW9yUdjSMw3rD0baIsqLBauVAlGFQ1zQ3HGK6J0= github.com/cosmos/ibc-go v1.2.1 h1:eWi8EzcgSwVipvhyQ7Rh1KfBe66C1ZdDSskeKMtIg0Q= @@ -480,8 +487,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/crypto-com/chain-indexing v1.2.1-0.20220111132225-b2712ba4fe23 h1:Pit1X4i8SXU/pUg60mo3tj/fKM+xYJ5j3TGtBK41D3k= -github.com/crypto-com/chain-indexing v1.2.1-0.20220111132225-b2712ba4fe23/go.mod h1:0KBBR7C2RZDavMpCBKlNtHkMZZ7tyndioZQRWUTD4YI= +github.com/crypto-com/chain-indexing v1.3.5-0.20220221024425-38ae3fe094d6 h1:qbOH3UOYef6faMg2OGjw6j/lZTM4ZkjA79ojPZ3RIWE= +github.com/crypto-com/chain-indexing v1.3.5-0.20220221024425-38ae3fe094d6/go.mod h1:rBXfe6eRJmmkPL/tuw3fAClnp54N99sHisyi5rvVYaE= github.com/crypto-org-chain/chain-main/v3 v3.0.0-croeseid h1:YAffq+tYiSqYXgIb11Vc6dvBtgSvRsf2g1wPaMFkQUA= github.com/crypto-org-chain/chain-main/v3 v3.0.0-croeseid/go.mod h1:92Z70bDbsScrWzIHB496p7nXKydtq5am9rqa0fCpbM8= github.com/crypto-org-chain/cronos v0.6.0-testnet h1:iFLwra4QMZ6HgIEB1P0vMCa9pnQTeXjwSuYbH4glxqA= @@ -564,7 +571,6 @@ github.com/elastic/gosigar v0.14.1/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0 github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/enigmampc/btcutil v1.0.3-0.20200723161021-e2fb6adb2a25 h1:2vLKys4RBU4pn2T/hjXMbvwTr1Cvy5THHrQkbeY9HRk= github.com/enigmampc/btcutil v1.0.3-0.20200723161021-e2fb6adb2a25/go.mod h1:hTr8+TLQmkUkgcuh3mcr5fjrT9c64ZzsBCdCEC6UppY= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -899,6 +905,8 @@ github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= github.com/graph-gophers/graphql-go v0.0.0-20191115155744-f33e81362277/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= github.com/graph-gophers/graphql-go v0.0.0-20201113091052-beb923fada29/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/gravity-devs/liquidity v1.4.5 h1:VhkcKima0HBztrrBdm4P0i1ey39KatyeB2u9AWJRCaI= +github.com/gravity-devs/liquidity v1.4.5/go.mod h1:TzUElAX4Wglt9x+8XWzPTMZGEwLrsTegXhk583Ht8RQ= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -915,6 +923,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqC github.com/grpc-ecosystem/grpc-gateway v1.14.7/go.mod h1:oYZKL012gGh6LMyg/xA7Q2yq6j8bu0wa+9w14EEthWU= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.0.1 h1:X2vfSnm1WC8HEo0MBHZg2TcuDUHJj6kd1TmEAQncnSA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.0.1/go.mod h1:oVMjMN64nzEcepv1kdZKgx1qNYt4Ro0Gqefiq2JWdis= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= @@ -1709,8 +1719,9 @@ github.com/tendermint/tendermint v0.34.0/go.mod h1:Aj3PIipBFSNO21r+Lq3TtzQ+uKESx github.com/tendermint/tendermint v0.34.10/go.mod h1:aeHL7alPh4uTBIJQ8mgFEE8VwJLXI1VD3rVOmH2Mcy0= github.com/tendermint/tendermint v0.34.11/go.mod h1:aeHL7alPh4uTBIJQ8mgFEE8VwJLXI1VD3rVOmH2Mcy0= github.com/tendermint/tendermint v0.34.12/go.mod h1:aeHL7alPh4uTBIJQ8mgFEE8VwJLXI1VD3rVOmH2Mcy0= -github.com/tendermint/tendermint v0.34.13 h1:fu+tsHudbOr5PvepjH0q47Jae59hQAvn3IqAHv2EbC8= github.com/tendermint/tendermint v0.34.13/go.mod h1:6RVVRBqwtKhA+H59APKumO+B7Nye4QXSFc6+TYxAxCI= +github.com/tendermint/tendermint v0.34.14 h1:GCXmlS8Bqd2Ix3TQCpwYLUNHe+Y+QyJsm5YE+S/FkPo= +github.com/tendermint/tendermint v0.34.14/go.mod h1:FrwVm3TvsVicI9Z7FlucHV6Znfd5KBc/Lpp69cCwtk0= github.com/tendermint/tm-db v0.6.2/go.mod h1:GYtQ67SUvATOcoY8/+x6ylk8Qo02BQyLrAs+yAcLvGI= github.com/tendermint/tm-db v0.6.3/go.mod h1:lfA1dL9/Y/Y8wwyPp2NMLyn5P5Ptr/gvDFNWtrCWSf8= github.com/tendermint/tm-db v0.6.4 h1:3N2jlnYQkXNQclQwd/eKV/NzlqPlfK21cpRRIx80XXQ= @@ -2418,6 +2429,7 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201111145450-ac7456db90a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -2486,8 +2498,10 @@ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.41.0 h1:f+PlOh7QV4iIJkPrx5NQ7qaNGFQ3OTse67yaDHfju4E= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= +google.golang.org/grpc v1.42.0 h1:XT2/MFpuPFsEX2fWh3YQtHkZ+WYZFQRfaUgLZYj/p6A= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.0.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.0.1/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/infrastructure/httpapi/handlers/validator_delegation.go b/infrastructure/httpapi/handlers/validator_delegation.go new file mode 100644 index 00000000..f0bf1cd0 --- /dev/null +++ b/infrastructure/httpapi/handlers/validator_delegation.go @@ -0,0 +1,262 @@ +package handlers + +import ( + "errors" + "fmt" + "strconv" + "strings" + + "github.com/valyala/fasthttp" + + "github.com/crypto-com/chain-indexing/appinterface/rdb" + applogger "github.com/crypto-com/chain-indexing/external/logger" + "github.com/crypto-com/chain-indexing/infrastructure/httpapi" + validator_delegation_view "github.com/crypto-com/chain-indexing/projection/validator_delegation/view" +) + +type ValidatorDelegation struct { + logger applogger.Logger + + accountAddressPrefix string + validatorAddressPrefix string + consNodeAddressPrefix string + + validatorsView validator_delegation_view.Validators + delegationsView validator_delegation_view.Delegations + unbondingDelegationsView validator_delegation_view.UnbondingDelegations + redelegationsView validator_delegation_view.Redelegations +} + +func NewValidatorDelegation( + logger applogger.Logger, + accountAddressPrefix string, + validatorAddressPrefix string, + consNodeAddressPrefix string, + rdbHandle *rdb.Handle, +) *ValidatorDelegation { + return &ValidatorDelegation{ + logger.WithFields(applogger.LogFields{ + "module": "ValidatorDelegationHandler", + }), + + accountAddressPrefix, + validatorAddressPrefix, + consNodeAddressPrefix, + + validator_delegation_view.NewValidatorsView(rdbHandle), + validator_delegation_view.NewDelegationsView(rdbHandle), + validator_delegation_view.NewUnbondingDelegationsView(rdbHandle), + validator_delegation_view.NewRedelegationsView(rdbHandle), + } +} + +func (handler *ValidatorDelegation) FindValidatorByAddress(ctx *fasthttp.RequestCtx) { + + height, err := parseInputHeight(ctx) + if err != nil { + httpapi.BadRequest(ctx, fmt.Errorf("error parsing input height: %v", err)) + return + } + + validatorAddress, ok := ctx.UserValue("address").(string) + if !ok { + httpapi.BadRequest(ctx, errors.New("error parsing input address")) + return + } + + var validator validator_delegation_view.ValidatorRow + var found bool + + if strings.HasPrefix(validatorAddress, handler.consNodeAddressPrefix) { + + validator, found, err = handler.validatorsView.FindByConsensusNodeAddr(validatorAddress, height) + if err != nil { + handler.logger.Errorf("error finding validator by ConsensusNodeAddress: %v", err) + httpapi.InternalServerError(ctx) + return + } + if !found { + httpapi.NotFound(ctx) + return + } + + } else if strings.HasPrefix(validatorAddress, handler.validatorAddressPrefix) { + + validator, found, err = handler.validatorsView.FindByOperatorAddr(validatorAddress, height) + if err != nil { + handler.logger.Errorf("error finding validaotr by OperatorAddress: %v", err) + httpapi.InternalServerError(ctx) + return + } + if !found { + httpapi.NotFound(ctx) + return + } + + } else { + httpapi.BadRequest(ctx, errors.New("invalid address")) + return + } + + httpapi.Success(ctx, validator) +} + +func (handler *ValidatorDelegation) ListValidator(ctx *fasthttp.RequestCtx) { + + pagination, err := httpapi.ParsePagination(ctx) + if err != nil { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + return + } + + height, err := parseInputHeight(ctx) + if err != nil { + httpapi.BadRequest(ctx, fmt.Errorf("error parsing input height: %v", err)) + return + } + + validators, paginationResult, err := handler.validatorsView.List(height, pagination) + if err != nil { + handler.logger.Errorf("error listing validators: %v", err) + httpapi.InternalServerError(ctx) + return + } + + httpapi.SuccessWithPagination(ctx, validators, paginationResult) +} + +func (handler *ValidatorDelegation) ListDelegationByValidator(ctx *fasthttp.RequestCtx) { + + pagination, err := httpapi.ParsePagination(ctx) + if err != nil { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + return + } + + height, err := parseInputHeight(ctx) + if err != nil { + httpapi.BadRequest(ctx, fmt.Errorf("error parsing input height: %v", err)) + return + } + + validatorAddress, ok := ctx.UserValue("address").(string) + if !ok { + httpapi.BadRequest(ctx, errors.New("error parsing input address")) + return + } + + delegations, paginationResult, err := handler.delegationsView.ListByValidatorAddr(validatorAddress, height, pagination) + if err != nil { + handler.logger.Errorf("error listing delegations by ValidatorAddress: %v", err) + httpapi.InternalServerError(ctx) + return + } + + httpapi.SuccessWithPagination(ctx, delegations, paginationResult) +} + +func (handler *ValidatorDelegation) ListDelegationByDelegator(ctx *fasthttp.RequestCtx) { + + pagination, err := httpapi.ParsePagination(ctx) + if err != nil { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + return + } + + height, err := parseInputHeight(ctx) + if err != nil { + httpapi.BadRequest(ctx, fmt.Errorf("error parsing input height: %v", err)) + return + } + + delegatorAddress, ok := ctx.UserValue("address").(string) + if !ok { + httpapi.BadRequest(ctx, errors.New("error parsing input address")) + return + } + + delegations, paginationResult, err := handler.delegationsView.ListByDelegatorAddr(delegatorAddress, height, pagination) + if err != nil { + handler.logger.Errorf("error listing delegations by DelegatorAddress: %v", err) + httpapi.InternalServerError(ctx) + return + } + + httpapi.SuccessWithPagination(ctx, delegations, paginationResult) +} + +func (handler *ValidatorDelegation) ListUnbondingDelegationByValidator(ctx *fasthttp.RequestCtx) { + + pagination, err := httpapi.ParsePagination(ctx) + if err != nil { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + return + } + + height, err := parseInputHeight(ctx) + if err != nil { + httpapi.BadRequest(ctx, fmt.Errorf("error parsing input height: %v", err)) + return + } + + validatorAddress, ok := ctx.UserValue("address").(string) + if !ok { + httpapi.BadRequest(ctx, errors.New("error parsing input address")) + return + } + + delegations, paginationResult, err := handler.unbondingDelegationsView.ListByValidatorWithPagination(validatorAddress, height, pagination) + if err != nil { + handler.logger.Errorf("error listing unbonding delegations by ValidatorAddress: %v", err) + httpapi.InternalServerError(ctx) + return + } + + httpapi.SuccessWithPagination(ctx, delegations, paginationResult) +} + +func (handler *ValidatorDelegation) ListRedelegationBySrcValidator(ctx *fasthttp.RequestCtx) { + + pagination, err := httpapi.ParsePagination(ctx) + if err != nil { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + return + } + + height, err := parseInputHeight(ctx) + if err != nil { + httpapi.BadRequest(ctx, fmt.Errorf("error parsing input height: %v", err)) + return + } + + srcValidatorAddress, ok := ctx.UserValue("srcValAddress").(string) + if !ok { + httpapi.BadRequest(ctx, errors.New("error parsing input address")) + return + } + + delegations, paginationResult, err := handler.redelegationsView.ListBySrcValidatorWithPagination(srcValidatorAddress, height, pagination) + if err != nil { + handler.logger.Errorf("error listing redelegations by SrcValidatorAddress: %v", err) + httpapi.InternalServerError(ctx) + return + } + + httpapi.SuccessWithPagination(ctx, delegations, paginationResult) +} + +func parseInputHeight(ctx *fasthttp.RequestCtx) (int64, error) { + + queryArgs := ctx.QueryArgs() + if queryArgs.Has("height") { + + heightInString := string(queryArgs.Peek("height")) + return strconv.ParseInt(heightInString, 10, 64) + + } else { + // TODO: If no input height, then use latest height as height + // return validator_delegation_view.LATEST_HEIGHT, nil + return 0, errors.New("No input height") + } + +} diff --git a/projection/validator_delegation/delegation.go b/projection/validator_delegation/delegation.go index 5c9bbdeb..cd4a6233 100644 --- a/projection/validator_delegation/delegation.go +++ b/projection/validator_delegation/delegation.go @@ -214,8 +214,8 @@ func (projection *ValidatorDelegation) unbond( isOperatorDelegation, err := utils.IsValAddrEqualsDelAddr( validatorAddress, delegatorAddress, - projection.config.validatorAddressPrefix, - projection.config.accountAddressPrefix, + projection.config.ValidatorAddressPrefix, + projection.config.AccountAddressPrefix, ) if err != nil { return coin.ZeroInt(), fmt.Errorf("error in checking if ValidatorAddr and DelegatorAddr are the same address: %v", err) diff --git a/projection/validator_delegation/handle_end_block.go b/projection/validator_delegation/handle_end_block.go index 1902c06d..34b6aac9 100644 --- a/projection/validator_delegation/handle_end_block.go +++ b/projection/validator_delegation/handle_end_block.go @@ -17,7 +17,7 @@ func (projection *ValidatorDelegation) handlePowerChanged( power string, ) error { - consensusNodeAddress, err := utils.GetConsensusNodeAddress(tendermintPubKey, projection.config.conNodeAddressPrefix) + consensusNodeAddress, err := utils.GetConsensusNodeAddress(tendermintPubKey, projection.config.ConNodeAddressPrefix) if err != nil { return fmt.Errorf("error getting consensusNodeAddress from tendermint pub key: %v", err) } @@ -40,7 +40,7 @@ func (projection *ValidatorDelegation) handlePowerChanged( // UnbondingHeight is the height when Unbonding start // UnbondingTime is the time when Unbonding is finished validator.UnbondingHeight = height - validator.UnbondingTime = blockTime.Add(projection.config.unbondingTime) + validator.UnbondingTime = blockTime.Add(projection.config.UnbondingTime) // Insert the validator to UnbondingValidators set if err := unbondingValidatorsView.Insert(validator.OperatorAddress, validator.UnbondingTime); err != nil { @@ -57,6 +57,7 @@ func (projection *ValidatorDelegation) handlePowerChanged( return fmt.Errorf("error removing unbonding validator entry: %v", err) } } + validator.Power = power // Update the validator if err := validatorsView.Update(validator); err != nil { @@ -98,6 +99,13 @@ func (projection *ValidatorDelegation) handleMatureUnbondingValidators( return fmt.Errorf("error updating validator: %v", err) } + // Remove validator if the shares is zero + if validator.Shares.IsZero() { + if err := validatorsView.Delete(validator); err != nil { + return fmt.Errorf("error deleting validator: %v", err) + } + } + } // Remove those mature UnbondingValidator entry diff --git a/projection/validator_delegation/migrations/20220117104331_create_view_vd_delegations.down.sql b/projection/validator_delegation/migrations/20220117104331_create_view_vd_delegations.down.sql new file mode 100644 index 00000000..b880b04c --- /dev/null +++ b/projection/validator_delegation/migrations/20220117104331_create_view_vd_delegations.down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS view_validator_delegation_delegations; + +DROP EXTENSION IF EXISTS btree_gist; \ No newline at end of file diff --git a/projection/validator_delegation/migrations/20220117104331_create_view_vd_delegations.up.sql b/projection/validator_delegation/migrations/20220117104331_create_view_vd_delegations.up.sql new file mode 100644 index 00000000..4112ce1b --- /dev/null +++ b/projection/validator_delegation/migrations/20220117104331_create_view_vd_delegations.up.sql @@ -0,0 +1,13 @@ +CREATE EXTENSION btree_gist; + +CREATE TABLE view_validator_delegation_delegations ( + id BIGSERIAL, + height INT8RANGE NOT NULL, + validator_address VARCHAR NOT NULL, + delegator_address VARCHAR NOT NULL, + shares VARCHAR NOT NULL, + PRIMARY KEY (id), + -- Below is a constraint and it is also an index. + -- It prevents a delegation record appear twice at any given height. + EXCLUDE USING gist (validator_address WITH =, delegator_address WITH =, height WITH &&) +); diff --git a/projection/validator_delegation/migrations/20220117110519_create_view_vd_evidences.down.sql b/projection/validator_delegation/migrations/20220117110519_create_view_vd_evidences.down.sql new file mode 100644 index 00000000..889ceef3 --- /dev/null +++ b/projection/validator_delegation/migrations/20220117110519_create_view_vd_evidences.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS view_validator_delegation_evidences; \ No newline at end of file diff --git a/projection/validator_delegation/migrations/20220117110519_create_view_vd_evidences.up.sql b/projection/validator_delegation/migrations/20220117110519_create_view_vd_evidences.up.sql new file mode 100644 index 00000000..e40b9c4a --- /dev/null +++ b/projection/validator_delegation/migrations/20220117110519_create_view_vd_evidences.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE view_validator_delegation_evidences ( + id BIGSERIAL, + height BIGINT NOT NULL, + tendermint_address VARCHAR NOT NULL, + infraction_height BIGINT NOT NULL, + raw_evidence JSONB NOT NULL, + PRIMARY KEY (id) +); diff --git a/projection/validator_delegation/migrations/20220117111405_create_view_vd_redelegation_queue.down.sql b/projection/validator_delegation/migrations/20220117111405_create_view_vd_redelegation_queue.down.sql new file mode 100644 index 00000000..1bfc4387 --- /dev/null +++ b/projection/validator_delegation/migrations/20220117111405_create_view_vd_redelegation_queue.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS view_validator_delegation_redelegation_queue; \ No newline at end of file diff --git a/projection/validator_delegation/migrations/20220117111405_create_view_vd_redelegation_queue.up.sql b/projection/validator_delegation/migrations/20220117111405_create_view_vd_redelegation_queue.up.sql new file mode 100644 index 00000000..3206eddd --- /dev/null +++ b/projection/validator_delegation/migrations/20220117111405_create_view_vd_redelegation_queue.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE view_validator_delegation_redelegation_queue ( + id BIGSERIAL, + completion_time BIGINT NOT NULL UNIQUE, + -- dvv_triplets is a list of (delegator_addr, src_validator_addr, dst_validator_addr) triplet + dvv_triplets JSONB NOT NULL, + PRIMARY KEY (id) +); diff --git a/projection/validator_delegation/migrations/20220117111937_create_view_vd_redelegations.down.sql b/projection/validator_delegation/migrations/20220117111937_create_view_vd_redelegations.down.sql new file mode 100644 index 00000000..b251e9e8 --- /dev/null +++ b/projection/validator_delegation/migrations/20220117111937_create_view_vd_redelegations.down.sql @@ -0,0 +1,3 @@ +DROP INDEX IF EXISTS view_validator_delegation_redelegations_validator_src_addr_index; + +DROP TABLE IF EXISTS view_validator_delegation_redelegations; \ No newline at end of file diff --git a/projection/validator_delegation/migrations/20220117111937_create_view_vd_redelegations.up.sql b/projection/validator_delegation/migrations/20220117111937_create_view_vd_redelegations.up.sql new file mode 100644 index 00000000..366a764e --- /dev/null +++ b/projection/validator_delegation/migrations/20220117111937_create_view_vd_redelegations.up.sql @@ -0,0 +1,19 @@ +CREATE TABLE view_validator_delegation_redelegations ( + id BIGSERIAL, + height INT8RANGE NOT NULL, + delegator_address VARCHAR NOT NULL, + validator_src_address VARCHAR NOT NULL, + validator_dst_address VARCHAR NOT NULL, + entries JSONB NOT NULL, + PRIMARY KEY (id), + -- Below is a constraint and it is also an index. + -- It prevents a delegation record appear twice at any given height. + EXCLUDE USING gist ( + delegator_address WITH =, + validator_src_address WITH =, + validator_dst_address WITH =, + height WITH && + ) +); + +CREATE INDEX view_validator_delegation_redelegations_validator_src_addr_index ON view_validator_delegation_redelegations USING gist (height, validator_src_address); diff --git a/projection/validator_delegation/migrations/20220117130920_create_view_vd_unbonding_delegation_queue.down.sql b/projection/validator_delegation/migrations/20220117130920_create_view_vd_unbonding_delegation_queue.down.sql new file mode 100644 index 00000000..3479d3aa --- /dev/null +++ b/projection/validator_delegation/migrations/20220117130920_create_view_vd_unbonding_delegation_queue.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS view_validator_delegation_unbonding_delegation_queue; \ No newline at end of file diff --git a/projection/validator_delegation/migrations/20220117130920_create_view_vd_unbonding_delegation_queue.up.sql b/projection/validator_delegation/migrations/20220117130920_create_view_vd_unbonding_delegation_queue.up.sql new file mode 100644 index 00000000..9d6fe925 --- /dev/null +++ b/projection/validator_delegation/migrations/20220117130920_create_view_vd_unbonding_delegation_queue.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE view_validator_delegation_unbonding_delegation_queue ( + id BIGSERIAL, + completion_time BIGINT NOT NULL UNIQUE, + -- dv_pairs is a list of (delegator_addr, validator_addr) pair + dv_pairs JSONB NOT NULL, + PRIMARY KEY (id) +); diff --git a/projection/validator_delegation/migrations/20220117131200_create_view_vd_unbonding_delegations.down.sql b/projection/validator_delegation/migrations/20220117131200_create_view_vd_unbonding_delegations.down.sql new file mode 100644 index 00000000..3f15922a --- /dev/null +++ b/projection/validator_delegation/migrations/20220117131200_create_view_vd_unbonding_delegations.down.sql @@ -0,0 +1,3 @@ +DROP INDEX IF EXISTS view_validator_delegation_unbonding_delegations_validator_addr_height_index; + +DROP TABLE IF EXISTS view_validator_delegation_unbonding_delegations; \ No newline at end of file diff --git a/projection/validator_delegation/migrations/20220117131200_create_view_vd_unbonding_delegations.up.sql b/projection/validator_delegation/migrations/20220117131200_create_view_vd_unbonding_delegations.up.sql new file mode 100644 index 00000000..fb07ba59 --- /dev/null +++ b/projection/validator_delegation/migrations/20220117131200_create_view_vd_unbonding_delegations.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE view_validator_delegation_unbonding_delegations ( + id BIGSERIAL, + height INT8RANGE NOT NULL, + delegator_address VARCHAR NOT NULL, + validator_address VARCHAR NOT NULL, + entries JSONB NOT NULL, + PRIMARY KEY (id), + -- Below is a constraint and it is also an index. + -- It prevents a delegation record appear twice at any given height. + EXCLUDE USING gist (delegator_address WITH =, validator_address WITH =, height WITH &&) +); + +CREATE INDEX view_validator_delegation_unbonding_delegations_validator_addr_height_index ON view_validator_delegation_unbonding_delegations USING gist (height, validator_address); diff --git a/projection/validator_delegation/migrations/20220117132223_create_view_vd_unbonding_validators.down.sql b/projection/validator_delegation/migrations/20220117132223_create_view_vd_unbonding_validators.down.sql new file mode 100644 index 00000000..63e4242e --- /dev/null +++ b/projection/validator_delegation/migrations/20220117132223_create_view_vd_unbonding_validators.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS view_validator_delegation_unbonding_validators; \ No newline at end of file diff --git a/projection/validator_delegation/migrations/20220117132223_create_view_vd_unbonding_validators.up.sql b/projection/validator_delegation/migrations/20220117132223_create_view_vd_unbonding_validators.up.sql new file mode 100644 index 00000000..eb57e666 --- /dev/null +++ b/projection/validator_delegation/migrations/20220117132223_create_view_vd_unbonding_validators.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE view_validator_delegation_unbonding_validators ( + id BIGSERIAL, + operator_address VARCHAR NOT NULL UNIQUE, + unbonding_time BIGINT NOT NULL, + PRIMARY KEY (id) +); diff --git a/projection/validator_delegation/migrations/20220117133017_create_view_vd_validators.down.sql b/projection/validator_delegation/migrations/20220117133017_create_view_vd_validators.down.sql new file mode 100644 index 00000000..72e4060f --- /dev/null +++ b/projection/validator_delegation/migrations/20220117133017_create_view_vd_validators.down.sql @@ -0,0 +1,3 @@ +DROP INDEX IF EXISTS view_validator_delegation_validators_height_index; + +DROP TABLE IF EXISTS view_validator_delegation_validators; \ No newline at end of file diff --git a/projection/validator_delegation/migrations/20220117133017_create_view_vd_validators.up.sql b/projection/validator_delegation/migrations/20220117133017_create_view_vd_validators.up.sql new file mode 100644 index 00000000..700b97ce --- /dev/null +++ b/projection/validator_delegation/migrations/20220117133017_create_view_vd_validators.up.sql @@ -0,0 +1,28 @@ +CREATE TABLE view_validator_delegation_validators ( + id BIGSERIAL, + height INT8RANGE NOT NULL, + operator_address VARCHAR NOT NULL, + consensus_node_address VARCHAR NOT NULL, + tendermint_address VARCHAR NOT NULL, + status VARCHAR NOT NULL, + jailed BOOLEAN NOT NULL, + power VARCHAR NOT NULL, + unbonding_height BIGINT NOT NULL, + unbonding_time BIGINT NOT NULL, + tokens VARCHAR NOT NULL, + shares VARCHAR NOT NULL, + min_self_delegation VARCHAR NOT NULL, + PRIMARY KEY (id), + -- Below is a constraint and it is also an index. + -- It prevents a delegation record appear twice at any given height. + EXCLUDE USING gist ( + operator_address WITH =, + height WITH && + ), + EXCLUDE USING gist ( + consensus_node_address WITH =, + height WITH && + ) +); + +CREATE INDEX view_validator_delegation_validators_height_index ON view_validator_delegation_validators USING gist (height); diff --git a/projection/validator_delegation/redelegation.go b/projection/validator_delegation/redelegation.go index 33dc74fd..d6149efd 100644 --- a/projection/validator_delegation/redelegation.go +++ b/projection/validator_delegation/redelegation.go @@ -105,7 +105,7 @@ func (projection *ValidatorDelegation) calculateRedelegationCompleteTime( switch { case !found || validator.IsBonded(): // the longest wait - just unbonding period from now - completionTime = blockTime.Add(projection.config.unbondingTime) + completionTime = blockTime.Add(projection.config.UnbondingTime) return completionTime, false, nil @@ -139,6 +139,10 @@ func (projection *ValidatorDelegation) setRedelegationEntry( } if found { red.AddEntry(creationHeight, completionTime, balance, sharesDst) + + if err := redelegationsView.Update(red); err != nil { + return red, fmt.Errorf("error updating reledegation record: %v", err) + } } else { red = view.NewRedelegationRow( delegatorAddress, @@ -149,10 +153,10 @@ func (projection *ValidatorDelegation) setRedelegationEntry( balance, sharesDst, ) - } - if err := redelegationsView.Upsert(red); err != nil { - return red, fmt.Errorf("error upserting reledegation record: %v", err) + if err := redelegationsView.Insert(red); err != nil { + return red, fmt.Errorf("error inserting reledegation record: %v", err) + } } return red, nil @@ -194,8 +198,8 @@ func (projection *ValidatorDelegation) completeRedelegation( } else { - if err := redelegationsView.Upsert(red); err != nil { - return fmt.Errorf("error upserting reledegation record: %v", err) + if err := redelegationsView.Update(red); err != nil { + return fmt.Errorf("error updating reledegation record: %v", err) } } diff --git a/projection/validator_delegation/slash.go b/projection/validator_delegation/slash.go index d9664f6c..2fa9adfe 100644 --- a/projection/validator_delegation/slash.go +++ b/projection/validator_delegation/slash.go @@ -52,7 +52,7 @@ func (projection *ValidatorDelegation) handleSlash( return fmt.Errorf("attempted to slash with a negative slash factor: %v", slashFactor) } - amount := utils.TokensFromConsensusPower(power, projection.config.defaultPowerReduction) + amount := utils.TokensFromConsensusPower(power, projection.config.DefaultPowerReduction) slashAmountDec := amount.ToDec().Mul(slashFactor) slashAmount := slashAmountDec.TruncateInt() @@ -193,8 +193,8 @@ func (projection *ValidatorDelegation) slashUnbondingDelegation( } unbondingDelegationsView := NewUnbondingDelegations(rdbTxHandle) - if err := unbondingDelegationsView.Upsert(unbondingDelegation); err != nil { - return coin.ZeroInt(), fmt.Errorf("error upserting unbonding delegation record: %v", err) + if err := unbondingDelegationsView.Update(unbondingDelegation); err != nil { + return coin.ZeroInt(), fmt.Errorf("error updating unbonding delegation record: %v", err) } return totalSlashAmount, nil diff --git a/projection/validator_delegation/unbonding_delegation.go b/projection/validator_delegation/unbonding_delegation.go index c1588ced..1714cefa 100644 --- a/projection/validator_delegation/unbonding_delegation.go +++ b/projection/validator_delegation/unbonding_delegation.go @@ -26,12 +26,16 @@ func (projection *ValidatorDelegation) setUnbondingDelegationEntry( } if found { ubd.AddEntry(creationHeight, completionTime, balance) + + if err := unbondingDelegationsView.Update(ubd); err != nil { + return ubd, fmt.Errorf("error updating unbonding delegation record: %v", err) + } } else { ubd = view.NewUnbondingDelegationRow(delegatorAddress, validatorAddress, creationHeight, completionTime, balance) - } - if err := unbondingDelegationsView.Upsert(ubd); err != nil { - return ubd, fmt.Errorf("error upserting unbonding delegation record: %v", err) + if err := unbondingDelegationsView.Insert(ubd); err != nil { + return ubd, fmt.Errorf("error inserting unbonding delegation record: %v", err) + } } return ubd, nil @@ -72,8 +76,8 @@ func (projection *ValidatorDelegation) completeUnbonding( } else { - if err := unbondingDelegationsView.Upsert(ubd); err != nil { - return fmt.Errorf("error upserting unbonding delegation: %v", err) + if err := unbondingDelegationsView.Update(ubd); err != nil { + return fmt.Errorf("error updating unbonding delegation: %v", err) } } diff --git a/projection/validator_delegation/validator.go b/projection/validator_delegation/validator.go index 5fedace2..ea9406ea 100644 --- a/projection/validator_delegation/validator.go +++ b/projection/validator_delegation/validator.go @@ -11,6 +11,75 @@ import ( "github.com/crypto-com/chain-indexing/usecase/coin" ) +func (projection *ValidatorDelegation) handleGenesisCreateNewValidator( + rdbTxHandle *rdb.Handle, + height int64, + validatorAddress string, + delegatorAddress string, + tendermintPubKey string, + amount coin.Int, + minSelfDelegationInString string, +) error { + + consensusNodeAddress, err := utils.GetConsensusNodeAddress(tendermintPubKey, projection.config.ConNodeAddressPrefix) + if err != nil { + return fmt.Errorf("error in GetConsensusNodeAddress: %v", err) + } + + tendermintAddress, err := utils.GetTendermintAddress(tendermintPubKey) + if err != nil { + return fmt.Errorf("error in GetTendermintAddress: %v", err) + } + + minSelfDelegation, ok := coin.NewIntFromString(minSelfDelegationInString) + if !ok { + return fmt.Errorf("Failed to parse minSelfDelegationInString: %v", minSelfDelegationInString) + } + + // Insert an ValidatorRow. + validatorsView := NewValidators(rdbTxHandle) + validatorRow := view.ValidatorRow{ + Height: height, + + OperatorAddress: validatorAddress, + ConsensusNodeAddress: consensusNodeAddress, + TendermintAddress: tendermintAddress, + + // For MsgCreateValidator in genesis block, we assumed they are always BONDED. + // Here, as genesis has no block_results and power_changed event, we just set the power as `1`, + // indicating that's a bonded validator. + // + // TODO: What if gen_txs contains more validators than maximum validators? + // In that case, may consider parsing `max_validators` from genesis. + // And only marked top `max_validators` validators as bonded. + Status: types.BONDED, + Jailed: false, + Power: "1", + + UnbondingHeight: int64(0), + UnbondingTime: utctime.UTCTime{}, + + Tokens: coin.ZeroInt(), + Shares: coin.ZeroDec(), + MinSelfDelegation: minSelfDelegation, + } + if err := validatorsView.Insert(validatorRow); err != nil { + return fmt.Errorf("error inserting ValidatorRow: %v", err) + } + + if _, err := projection.handleDelegate( + rdbTxHandle, + height, + validatorAddress, + delegatorAddress, + amount, + ); err != nil { + return fmt.Errorf("error handling Delegate: %v", err) + } + + return nil +} + func (projection *ValidatorDelegation) handleCreateNewValidator( rdbTxHandle *rdb.Handle, height int64, @@ -21,7 +90,7 @@ func (projection *ValidatorDelegation) handleCreateNewValidator( minSelfDelegationInString string, ) error { - consensusNodeAddress, err := utils.GetConsensusNodeAddress(tendermintPubKey, projection.config.conNodeAddressPrefix) + consensusNodeAddress, err := utils.GetConsensusNodeAddress(tendermintPubKey, projection.config.ConNodeAddressPrefix) if err != nil { return fmt.Errorf("error in GetConsensusNodeAddress: %v", err) } @@ -57,7 +126,7 @@ func (projection *ValidatorDelegation) handleCreateNewValidator( MinSelfDelegation: minSelfDelegation, } if err := validatorsView.Insert(validatorRow); err != nil { - return fmt.Errorf("error validatorsView.Insert(): %v", err) + return fmt.Errorf("error inserting ValidatorRow: %v", err) } if _, err := projection.handleDelegate( @@ -67,7 +136,7 @@ func (projection *ValidatorDelegation) handleCreateNewValidator( delegatorAddress, amount, ); err != nil { - return fmt.Errorf("error in projection.handleDelegate(): %v", err) + return fmt.Errorf("error handling Delegate: %v", err) } return nil diff --git a/projection/validator_delegation/validator_delegation.go b/projection/validator_delegation/validator_delegation.go index 7075ce1c..ea479b21 100644 --- a/projection/validator_delegation/validator_delegation.go +++ b/projection/validator_delegation/validator_delegation.go @@ -1,10 +1,13 @@ package validator_delegation import ( + "errors" "fmt" "strconv" "time" + "github.com/mitchellh/mapstructure" + "github.com/crypto-com/chain-indexing/appinterface/projection/rdbprojectionbase" "github.com/crypto-com/chain-indexing/appinterface/rdb" event_entity "github.com/crypto-com/chain-indexing/entity/event" @@ -20,6 +23,83 @@ import ( var _ projection_entity.Projection = &ValidatorDelegation{} +type Config struct { + AccountAddressPrefix string + ValidatorAddressPrefix string + ConNodeAddressPrefix string + // set in genesis `unbonding_time` + UnbondingTime time.Duration + // set in genesis `slash_fraction_double_sign` + SlashFractionDoubleSign coin.Dec + // set in genesis `slash_fraction_downtime` + SlashFractionDowntime coin.Dec + // PowerReduction - is the amount of staking tokens required for 1 unit of consensus-engine power. + // Currently, this returns a global variable that the app developer can tweak. + // https://github.com/cosmos/cosmos-sdk/blob/0cb7fd081e05317ed7a2f13b0e142349a163fe4d/x/staking/keeper/params.go#L46 + DefaultPowerReduction coin.Int +} + +type CustomConfig struct { + UnbondingTime time.Duration `mapstructure:"unbonding_time"` + SlashFractionDoubleSign string `mapstructure:"slash_fraction_double_sign"` + SlashFractionDowntime string `mapstructure:"slash_fraction_downtime"` + DefaultPowerReduction string `mapstructure:"default_power_reduction"` +} + +func CustomConfigFromInterface(data interface{}) (CustomConfig, error) { + customConfig := CustomConfig{} + + decoderConfig := &mapstructure.DecoderConfig{ + WeaklyTypedInput: true, + DecodeHook: mapstructure.ComposeDecodeHookFunc( + mapstructure.StringToTimeDurationHookFunc(), + ), + Result: &customConfig, + } + decoder, decoderErr := mapstructure.NewDecoder(decoderConfig) + if decoderErr != nil { + return customConfig, fmt.Errorf("error creating projection config decoder: %v", decoderErr) + } + + if err := decoder.Decode(data); err != nil { + return customConfig, fmt.Errorf("error decoding projection ValidatorDelegation config: %v", err) + } + + return customConfig, nil +} + +func PrepareConfig( + customConfig CustomConfig, + accountAddressPrefix string, + validatorAddressPrefix string, + conNodeAddressPrefix string, +) (Config, error) { + + config := Config{} + config.AccountAddressPrefix = accountAddressPrefix + config.ValidatorAddressPrefix = validatorAddressPrefix + config.ConNodeAddressPrefix = conNodeAddressPrefix + config.UnbondingTime = customConfig.UnbondingTime + + var err error + var ok bool + + config.SlashFractionDoubleSign, err = coin.NewDecFromStr(customConfig.SlashFractionDoubleSign) + if err != nil { + return config, fmt.Errorf("error parsing slashFractionDoubleSign from RawConfig: %v", err) + } + config.SlashFractionDowntime, err = coin.NewDecFromStr(customConfig.SlashFractionDowntime) + if err != nil { + return config, fmt.Errorf("error parsing slashFractionDowntime from RawConfig: %v", err) + } + config.DefaultPowerReduction, ok = coin.NewIntFromString(customConfig.DefaultPowerReduction) + if !ok { + return config, errors.New("error parsing defaultPowerReduction from RawConfig") + } + + return config, nil +} + var ( NewValidators = view.NewValidatorsView NewUnbondingValidators = view.NewUnbondingValidatorsView @@ -43,19 +123,6 @@ type ValidatorDelegation struct { migrationHelper migrationhelper.MigrationHelper } -type Config struct { - accountAddressPrefix string - validatorAddressPrefix string - conNodeAddressPrefix string - unbondingTime time.Duration // set in genesis `unbonding_time` - slashFractionDoubleSign coin.Dec // set in genesis `slash_fraction_double_sign` - slashFractionDowntime coin.Dec // set in genesis `slash_fraction_downtime` - // PowerReduction - is the amount of staking tokens required for 1 unit of consensus-engine power. - // Currently, this returns a global variable that the app developer can tweak. - // https://github.com/cosmos/cosmos-sdk/blob/0cb7fd081e05317ed7a2f13b0e142349a163fe4d/x/staking/keeper/params.go#L46 - defaultPowerReduction coin.Int -} - func NewValidatorDelegation( logger applogger.Logger, rdbConn rdb.Conn, @@ -136,42 +203,11 @@ func (projection *ValidatorDelegation) HandleEvents(height int64, events []event } } - // Genesis block height is 0. - // - // If this is NOT a Genesis block, then always clone the following rows in previous height: - // - ValidatorRow - // - DelegationRow - // - UnbondingDelegationRow - // - RedelegationRow - // - // This is to keep track historical states in all height. - if height > 0 { - validatorsView := NewValidators(rdbTxHandle) - if err := validatorsView.Clone(height-1, height); err != nil { - return fmt.Errorf("error in cloning validator records in previous height: %v", err) - } - - delegationsView := NewDelegations(rdbTxHandle) - if err := delegationsView.Clone(height-1, height); err != nil { - return fmt.Errorf("error in cloning delegation records in previous height: %v", err) - } - - unbondingDelegationsView := NewUnbondingDelegations(rdbTxHandle) - if err := unbondingDelegationsView.Clone(height-1, height); err != nil { - return fmt.Errorf("error in cloning unbonding delegation records in previous height: %v", err) - } - - redelegationsView := NewRedelegations(rdbTxHandle) - if err := redelegationsView.Clone(height-1, height); err != nil { - return fmt.Errorf("error in cloning redelegation records in previous height: %v", err) - } - } - // Handle events in Genesis block for _, event := range events { if typedEvent, ok := event.(*event_usecase.CreateGenesisValidator); ok { - if err := projection.handleCreateNewValidator( + if err := projection.handleGenesisCreateNewValidator( rdbTxHandle, height, typedEvent.ValidatorAddress, @@ -256,7 +292,7 @@ func (projection *ValidatorDelegation) HandleEvents(height int64, events []event typedEvent.ConsensusNodeAddress, distributionHeight, power, - projection.config.slashFractionDowntime, + projection.config.SlashFractionDowntime, ); err != nil { return fmt.Errorf("error in handling slash event with missing_signature: %v", err) } @@ -295,7 +331,7 @@ func (projection *ValidatorDelegation) HandleEvents(height int64, events []event typedEvent.ConsensusNodeAddress, distributionHeight, power, - projection.config.slashFractionDoubleSign, + projection.config.SlashFractionDoubleSign, ); err != nil { return fmt.Errorf("error in handling slash event with double_sign: %v", err) } diff --git a/projection/validator_delegation/view/delegations.go b/projection/validator_delegation/view/delegations.go index 2f7b9324..8fffb36f 100644 --- a/projection/validator_delegation/view/delegations.go +++ b/projection/validator_delegation/view/delegations.go @@ -1,14 +1,15 @@ package view import ( - "github.com/crypto-com/chain-indexing/appinterface/pagination" + "errors" + "fmt" + + pagination_appinterface "github.com/crypto-com/chain-indexing/appinterface/pagination" "github.com/crypto-com/chain-indexing/appinterface/rdb" "github.com/crypto-com/chain-indexing/usecase/coin" ) type Delegations interface { - Clone(previousHeight int64, currentHeight int64) error - Insert(row DelegationRow) error Update(row DelegationRow) error Delete(row DelegationRow) error @@ -18,13 +19,13 @@ type Delegations interface { ListByValidatorAddr( validatorAddress string, height int64, - pagination *pagination.Pagination, - ) ([]DelegationRow, *pagination.PaginationResult, error) + pagination *pagination_appinterface.Pagination, + ) ([]DelegationRow, *pagination_appinterface.PaginationResult, error) ListByDelegatorAddr( delegatorAddress string, height int64, - pagination *pagination.Pagination, - ) ([]DelegationRow, *pagination.PaginationResult, error) + pagination *pagination_appinterface.Pagination, + ) ([]DelegationRow, *pagination_appinterface.PaginationResult, error) } type DelegationsView struct { @@ -37,23 +38,151 @@ func NewDelegationsView(handle *rdb.Handle) Delegations { } } -func (view *DelegationsView) Clone(previousHeight, currentHeight int64) error { +func (view *DelegationsView) Insert(row DelegationRow) error { - return nil -} + sql, sqlArgs, err := view.rdb.StmtBuilder. + Insert( + "view_validator_delegation_delegations", + ). + Columns( + "height", + "validator_address", + "delegator_address", + "shares", + ). + Values( + fmt.Sprintf("[%v,)", row.Height), + row.ValidatorAddress, + row.DelegatorAddress, + row.Shares.String(), + ). + ToSql() -func (view *DelegationsView) Insert(row DelegationRow) error { + if err != nil { + return fmt.Errorf("error building delegation insertion sql: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + result, err := view.rdb.Exec(sql, sqlArgs...) + if err != nil { + return fmt.Errorf("error inserting delegation into the table: %v: %w", err, rdb.ErrWrite) + } + if result.RowsAffected() != 1 { + return fmt.Errorf("error inserting delegation into the table: rows inserted: %v: %w", result.RowsAffected(), rdb.ErrWrite) + } return nil } func (view *DelegationsView) Update(row DelegationRow) error { + // Check if there is an record's lower bound start with this height. + found, err := view.isExistedByLowerBoundHeight(row.ValidatorAddress, row.DelegatorAddress, row.Height) + if err != nil { + return fmt.Errorf("error in checking new delegation record existence at this height: %v", err) + } + + if found { + // If there is a record that height = `[row.Height,)`, then update the existed one + + sql, sqlArgs, err := view.rdb.StmtBuilder. + Update( + "view_validator_delegation_delegations", + ). + SetMap(map[string]interface{}{ + "shares": row.Shares.String(), + }). + Where( + "validator_address = ? AND delegator_address = ? AND height @> ?::int8", + row.ValidatorAddress, + row.DelegatorAddress, + row.Height, + ). + ToSql() + + if err != nil { + return fmt.Errorf("error building delegation update sql: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + result, err := view.rdb.Exec(sql, sqlArgs...) + if err != nil { + return fmt.Errorf("error updating delegation into the table: %v: %w", err, rdb.ErrWrite) + } + if result.RowsAffected() != 1 { + return fmt.Errorf("error updating delegation into the table: row updated: %v: %w", result.RowsAffected(), rdb.ErrWrite) + } + + } else { + // Else, update the previous record's height range, then insert a new record + + err := view.updateUpperBoundHeight(row) + if err != nil { + return fmt.Errorf("error updating delegation.Height upper bound: %v", err) + } + err = view.Insert(row) + if err != nil { + return fmt.Errorf("error inserting a new record for this delegation: %v", err) + } + + } + return nil } +func (view *DelegationsView) isExistedByLowerBoundHeight(validatorAddress, delegatorAddress string, height int64) (bool, error) { + + sql, sqlArgs, err := view.rdb.StmtBuilder. + Select( + "COUNT(*)", + ). + From("view_validator_delegation_delegations"). + Where( + "validator_address = ? AND delegator_address = ? AND height = ?", + validatorAddress, + delegatorAddress, + fmt.Sprintf("[%v,)", height), + ). + ToSql() + if err != nil { + return false, fmt.Errorf("error building sql to check delegation at specific height: %v: %w", err, rdb.ErrPrepare) + } + + var count int64 + if err = view.rdb.QueryRow(sql, sqlArgs...).Scan( + &count, + ); err != nil { + return false, fmt.Errorf("error scanning count: %v: %w", err, rdb.ErrQuery) + } + + return count == 1, nil +} + func (view *DelegationsView) Delete(row DelegationRow) error { + return view.updateUpperBoundHeight(row) +} + +func (view *DelegationsView) updateUpperBoundHeight(row DelegationRow) error { + + // Set the upper bound for record height: `[, row.Height)`. + sql, sqlErr := view.rdb.StmtBuilder.ReplacePlaceholders(` + UPDATE view_validator_delegation_delegations + SET height = int8range(lower(height), ?, '[)') + WHERE validator_address = ? AND delegator_address = ? AND height @> ?::int8 + `) + if sqlErr != nil { + return fmt.Errorf("error building delegation upper(height) update sql: %v: %w", sqlErr, rdb.ErrBuildSQLStmt) + } + sqlArgs := []interface{}{ + row.Height, + row.ValidatorAddress, + row.DelegatorAddress, + row.Height, + } + + if _, execErr := view.rdb.Exec(sql, sqlArgs...); execErr != nil { + return fmt.Errorf("error executing delegation upper(height)update sql: %v: %w", execErr, rdb.ErrWrite) + } + return nil } @@ -63,33 +192,176 @@ func (view *DelegationsView) FindBy( height int64, ) (DelegationRow, bool, error) { - // TODO need to handle the case when row NOT exist + sql, sqlArgs, err := view.rdb.StmtBuilder. + Select( + "shares", + ). + From("view_validator_delegation_delegations"). + Where( + "validator_address = ? AND delegator_address = ? AND height @> ?::int8", + validatorAddress, + delegatorAddress, + height, + ). + ToSql() + if err != nil { + return DelegationRow{}, false, fmt.Errorf("error building select delegation sql: %v: %w", err, rdb.ErrPrepare) + } + + var delegation DelegationRow + delegation.Height = height + delegation.ValidatorAddress = validatorAddress + delegation.DelegatorAddress = delegatorAddress + + var sharesInString string + if err = view.rdb.QueryRow(sql, sqlArgs...).Scan( + &sharesInString, + ); err != nil { + if errors.Is(err, rdb.ErrNoRows) { + // When the row is not found, do not return error + return DelegationRow{}, false, nil + } + return DelegationRow{}, false, fmt.Errorf("error scanning delegation: %v: %w", err, rdb.ErrQuery) + } + + delegation.Shares, err = coin.NewDecFromStr(sharesInString) + if err != nil { + return DelegationRow{}, false, fmt.Errorf("error parsing shares to coin.Dec: %v: %w", err, rdb.ErrQuery) + } - return DelegationRow{}, false, nil + return delegation, true, nil } func (view *DelegationsView) ListByValidatorAddr( validatorAddress string, height int64, - pagination *pagination.Pagination, -) ([]DelegationRow, *pagination.PaginationResult, error) { + pagination *pagination_appinterface.Pagination, +) ([]DelegationRow, *pagination_appinterface.PaginationResult, error) { + + stmtBuilder := view.rdb.StmtBuilder. + Select( + "delegator_address", + "shares", + ). + From( + "view_validator_delegation_delegations", + ). + Where( + "validator_address = ? AND height @> ?::int8", + validatorAddress, + height, + ) + + rDbPagination := rdb.NewRDbPaginationBuilder( + pagination, + view.rdb, + ).BuildStmt(stmtBuilder) + sql, sqlArgs, err := rDbPagination.ToStmtBuilder().ToSql() + if err != nil { + return nil, nil, fmt.Errorf("error building delegations select SQL: %v, %w", err, rdb.ErrBuildSQLStmt) + } + + rowsResult, err := view.rdb.Query(sql, sqlArgs...) + if err != nil { + return nil, nil, fmt.Errorf("error executing delegations select SQL: %v: %w", err, rdb.ErrQuery) + } + defer rowsResult.Close() + + delegations := make([]DelegationRow, 0) + for rowsResult.Next() { + var delegation DelegationRow + delegation.Height = height + delegation.ValidatorAddress = validatorAddress + + var sharesInString string + if err = rowsResult.Scan( + &delegation.DelegatorAddress, + &sharesInString, + ); err != nil { + return nil, nil, fmt.Errorf("error scanning delegation row: %v: %w", err, rdb.ErrQuery) + } + + delegation.Shares, err = coin.NewDecFromStr(sharesInString) + if err != nil { + return nil, nil, fmt.Errorf("error parsing shares to coin.Dec: %v: %w", err, rdb.ErrQuery) + } - return nil, nil, nil + delegations = append(delegations, delegation) + } + + paginationResult, err := rDbPagination.Result() + if err != nil { + return nil, nil, fmt.Errorf("error preparing pagination result: %v", err) + } + + return delegations, paginationResult, nil } func (view *DelegationsView) ListByDelegatorAddr( delegatorAddress string, height int64, - pagination *pagination.Pagination, -) ([]DelegationRow, *pagination.PaginationResult, error) { + pagination *pagination_appinterface.Pagination, +) ([]DelegationRow, *pagination_appinterface.PaginationResult, error) { + + stmtBuilder := view.rdb.StmtBuilder. + Select( + "validator_address", + "shares", + ). + From( + "view_validator_delegation_delegations", + ). + Where( + "delegator_address = ? AND height @> ?::int8", + delegatorAddress, + height, + ) + + rDbPagination := rdb.NewRDbPaginationBuilder( + pagination, + view.rdb, + ).BuildStmt(stmtBuilder) + sql, sqlArgs, err := rDbPagination.ToStmtBuilder().ToSql() + if err != nil { + return nil, nil, fmt.Errorf("error building delegations select SQL: %v, %w", err, rdb.ErrBuildSQLStmt) + } + + rowsResult, err := view.rdb.Query(sql, sqlArgs...) + if err != nil { + return nil, nil, fmt.Errorf("error executing delegations select SQL: %v: %w", err, rdb.ErrQuery) + } + defer rowsResult.Close() + + delegations := make([]DelegationRow, 0) + for rowsResult.Next() { + var delegation DelegationRow + delegation.Height = height + delegation.DelegatorAddress = delegatorAddress + + var sharesInString string + if err = rowsResult.Scan( + &delegation.ValidatorAddress, + &sharesInString, + ); err != nil { + return nil, nil, fmt.Errorf("error scanning delegation row: %v: %w", err, rdb.ErrQuery) + } + + delegation.Shares, err = coin.NewDecFromStr(sharesInString) + if err != nil { + return nil, nil, fmt.Errorf("error parsing shares to coin.Dec: %v: %w", err, rdb.ErrQuery) + } + + delegations = append(delegations, delegation) + } + + paginationResult, err := rDbPagination.Result() + if err != nil { + return nil, nil, fmt.Errorf("error preparing pagination result: %v", err) + } - return nil, nil, nil + return delegations, paginationResult, nil } -// TODO: -// - UNIQUE(height, validatorAddress, delegatorAddress) -// - INDEX(height, validatorAddress) -// - INDEX(height, delegatorAddress) type DelegationRow struct { Height int64 `json:"height"` ValidatorAddress string `json:"validatorAddress"` diff --git a/projection/validator_delegation/view/evidences.go b/projection/validator_delegation/view/evidences.go index a435dde8..818b45f3 100644 --- a/projection/validator_delegation/view/evidences.go +++ b/projection/validator_delegation/view/evidences.go @@ -1,8 +1,11 @@ package view import ( + "fmt" + "github.com/crypto-com/chain-indexing/appinterface/rdb" "github.com/crypto-com/chain-indexing/usecase/model" + jsoniter "github.com/json-iterator/go" ) type Evidences interface { @@ -22,15 +25,79 @@ func NewEvidencesView(handle *rdb.Handle) Evidences { func (view *EvidencesView) Insert(row EvidenceRow) error { + rawEvidenceJSON, err := jsoniter.MarshalToString(row.RawEvidence) + if err != nil { + return fmt.Errorf("error JSON marshalling RawEvidence for insertion: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + sql, sqlArgs, err := view.rdb.StmtBuilder. + Insert( + "view_validator_delegation_evidences", + ). + Columns( + "height", + "tendermint_address", + "infraction_height", + "raw_evidence", + ). + Values( + row.Height, + row.TendermintAddress, + row.InfractionHeight, + rawEvidenceJSON, + ). + ToSql() + + if err != nil { + return fmt.Errorf("error building evidence insertion sql: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + result, err := view.rdb.Exec(sql, sqlArgs...) + if err != nil { + return fmt.Errorf("error inserting evidence into the table: %v: %w", err, rdb.ErrWrite) + } + if result.RowsAffected() != 1 { + return fmt.Errorf("error inserting evidence into the table: no rows inserted: %w", rdb.ErrWrite) + } + return nil } func (view *EvidencesView) FindBy(height int64, tendermintAddress string) (EvidenceRow, error) { - // TODO: It seems it is possible that tendermint could put two evidences on a same height. - // Maybe in this function, we should SELECT all matched rows and then return the first one in the list. + sql, sqlArgs, err := view.rdb.StmtBuilder. + Select( + "infraction_height", + "raw_evidence", + ). + From("view_validator_delegation_evidences"). + Where( + "tendermint_address = ? AND height = ?", + tendermintAddress, + height, + ). + ToSql() + if err != nil { + return EvidenceRow{}, fmt.Errorf("error building select evidence sql: %v: %w", err, rdb.ErrPrepare) + } + + var evidence EvidenceRow + evidence.Height = height + evidence.TendermintAddress = tendermintAddress + + var rawEvidenceJSON string + if err = view.rdb.QueryRow(sql, sqlArgs...).Scan( + &evidence.InfractionHeight, + &rawEvidenceJSON, + ); err != nil { + return EvidenceRow{}, fmt.Errorf("error scanning evidence: %v: %w", err, rdb.ErrQuery) + } + + if err = jsoniter.UnmarshalFromString(rawEvidenceJSON, &evidence.RawEvidence); err != nil { + return EvidenceRow{}, fmt.Errorf("error unmarshalling RawEvidence JSON: %v: %w", err, rdb.ErrQuery) + } - return EvidenceRow{}, nil + return evidence, nil } // NOTES: Don't add UNIQUE constraint on (Height, TendermintAddress) now. diff --git a/projection/validator_delegation/view/redelegation_queue.go b/projection/validator_delegation/view/redelegation_queue.go index 47978033..33e60116 100644 --- a/projection/validator_delegation/view/redelegation_queue.go +++ b/projection/validator_delegation/view/redelegation_queue.go @@ -1,8 +1,12 @@ package view import ( + "errors" + "fmt" + "github.com/crypto-com/chain-indexing/appinterface/rdb" "github.com/crypto-com/chain-indexing/external/utctime" + jsoniter "github.com/json-iterator/go" ) type RedelegationQueue interface { @@ -23,28 +27,150 @@ func NewRedelegationQueueView(handle *rdb.Handle) RedelegationQueue { func (view *RedelegationQueueView) Upsert(row RedelegationQueueRow) error { + dvvTripletsJSON, err := jsoniter.MarshalToString(row.DVVTriplets) + if err != nil { + return fmt.Errorf("error JSON marshalling DVVTriplets for upsertion: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + sql, sqlArgs, err := view.rdb.StmtBuilder. + Insert( + "view_validator_delegation_redelegation_queue", + ). + Columns( + "completion_time", + "dvv_triplets", + ). + Values( + view.rdb.Tton(&row.CompletionTime), + dvvTripletsJSON, + ). + Suffix("ON CONFLICT(completion_time) DO UPDATE SET dvv_triplets = EXCLUDED.dvv_triplets"). + ToSql() + + if err != nil { + return fmt.Errorf("error building RedelegationQueueRow upsertion sql: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + result, err := view.rdb.Exec(sql, sqlArgs...) + if err != nil { + return fmt.Errorf("error upserting RedelegationQueueRow into the table: %v: %w", err, rdb.ErrWrite) + } + if result.RowsAffected() != 1 { + return fmt.Errorf("error upserting RedelegationQueueRow into the table: no rows inserted: %w", rdb.ErrWrite) + } + return nil } func (view *RedelegationQueueView) FindBy(completionTime utctime.UTCTime) (RedelegationQueueRow, bool, error) { - // TODO handle the row not found + sql, sqlArgs, err := view.rdb.StmtBuilder. + Select( + "dvv_triplets", + ). + From("view_validator_delegation_redelegation_queue"). + Where( + "completion_time = ?", + view.rdb.Tton(&completionTime), + ). + ToSql() + if err != nil { + return RedelegationQueueRow{}, false, fmt.Errorf("error building select RedelegationQueueRow sql: %v: %w", err, rdb.ErrPrepare) + } + + var row RedelegationQueueRow + row.CompletionTime = completionTime + + var dvvTripletsJSON string + if err = view.rdb.QueryRow(sql, sqlArgs...).Scan( + &dvvTripletsJSON, + ); err != nil { + if errors.Is(err, rdb.ErrNoRows) { + // When the row is not found, do not return error + return RedelegationQueueRow{}, false, nil + } + return RedelegationQueueRow{}, false, fmt.Errorf("error scanning RedelegationQueueRow: %v: %w", err, rdb.ErrQuery) + } - return RedelegationQueueRow{}, true, nil + if err = jsoniter.UnmarshalFromString(dvvTripletsJSON, &row.DVVTriplets); err != nil { + return RedelegationQueueRow{}, false, fmt.Errorf("error unmarshalling DVVTriplets JSON: %v: %w", err, rdb.ErrQuery) + } + + return row, true, nil } func (view *RedelegationQueueView) DequeueAllMatureRedelegationQueue(blockTime utctime.UTCTime) ([]DVVTriplet, error) { - // TODO find all mature RedelegationQueueRow, then concate their DVVTriplets + // Find all mature RedelegationQueueRow, then concate their DVVTriplets + + sql, sqlArgs, err := view.rdb.StmtBuilder. + Select( + "dvv_triplets", + ). + From( + "view_validator_delegation_redelegation_queue", + ). + Where( + "completion_time <= ?", + view.rdb.Tton(&blockTime), + ). + ToSql() + if err != nil { + return nil, fmt.Errorf("error building mature RedelegationQueueRow select SQL: %v, %w", err, rdb.ErrBuildSQLStmt) + } + + rowsResult, err := view.rdb.Query(sql, sqlArgs...) + if err != nil { + return nil, fmt.Errorf("error executing mature RedelegationQueueRow select SQL: %v: %w", err, rdb.ErrQuery) + } + defer rowsResult.Close() + + matureDVVTriplets := make([]DVVTriplet, 0) + for rowsResult.Next() { + + var dvvTripletsJSON string + if err = rowsResult.Scan( + &dvvTripletsJSON, + ); err != nil { + return nil, fmt.Errorf("error scanning RedelegationQueueRow: %v: %w", err, rdb.ErrQuery) + } - // Optional TODO: de-duplicate, a same DVVTriplet could appear multiple times, we should avoid that + var dvvTriplets []DVVTriplet + if err = jsoniter.UnmarshalFromString(dvvTripletsJSON, &dvvTriplets); err != nil { + return nil, fmt.Errorf("error unmarshalling DVVTriplets JSON: %v: %w", err, rdb.ErrQuery) + } - // TODO Delete the mature rows + matureDVVTriplets = append(matureDVVTriplets, dvvTriplets...) + } + + // Optional TODO: De-duplicate the return slice. A same DVVTriplet could appear multiple times, we could avoid it here. + // At the moment, we are following CosmosSDK implementation, so not doing the de-duplicate. + + // Delete the mature rows + + sql, sqlArgs, err = view.rdb.StmtBuilder. + Delete( + "view_validator_delegation_redelegation_queue", + ). + Where( + "completion_time <= ?", + view.rdb.Tton(&blockTime), + ). + ToSql() + if err != nil { + return nil, fmt.Errorf("error building RedelegationQueueRow deletion sql: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + _, err = view.rdb.Exec(sql, sqlArgs...) + if err != nil { + return nil, fmt.Errorf("error deleting RedelegationQueueRow from the table: %v: %w", err, rdb.ErrWrite) + } - return nil, nil + return matureDVVTriplets, nil } -// TODO UNIQUE CompletionTime +// NOTES: +// - UNIQUE CompletionTime type RedelegationQueueRow struct { CompletionTime utctime.UTCTime `json:"completionTime"` DVVTriplets []DVVTriplet `json:"dvvTriplets"` diff --git a/projection/validator_delegation/view/redelegations.go b/projection/validator_delegation/view/redelegations.go index 2ca1d341..8a6896b4 100644 --- a/projection/validator_delegation/view/redelegations.go +++ b/projection/validator_delegation/view/redelegations.go @@ -1,20 +1,30 @@ package view import ( + "errors" + "fmt" "time" + pagination_appinterface "github.com/crypto-com/chain-indexing/appinterface/pagination" "github.com/crypto-com/chain-indexing/appinterface/rdb" "github.com/crypto-com/chain-indexing/external/utctime" "github.com/crypto-com/chain-indexing/usecase/coin" + jsoniter "github.com/json-iterator/go" ) type Redelegations interface { - Clone(previousHeight int64, currentHeight int64) error - - Upsert(row RedelegationRow) error + Insert(row RedelegationRow) error + Update(row RedelegationRow) error Delete(row RedelegationRow) error FindBy(delegatorAddress, validatorSrcAddress, validatorDstAddress string, height int64) (RedelegationRow, bool, error) + // For internal projection logic ListBySrcValidator(validatorSrcAddress string, height int64) ([]RedelegationRow, error) + // For HTTP API + ListBySrcValidatorWithPagination( + validatorSrcAddress string, + height int64, + pagination *pagination_appinterface.Pagination, + ) ([]RedelegationRow, *pagination_appinterface.PaginationResult, error) } type RedelegationsView struct { @@ -27,18 +37,176 @@ func NewRedelegationsView(handle *rdb.Handle) Redelegations { } } -func (view *RedelegationsView) Clone(previousHeight, currentHeight int64) error { +func (view *RedelegationsView) Insert(row RedelegationRow) error { + + entriesJSON, err := jsoniter.MarshalToString(row.Entries) + if err != nil { + return fmt.Errorf("error JSON marshalling RedelegationRow.Entries for upsertion: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + sql, sqlArgs, err := view.rdb.StmtBuilder. + Insert( + "view_validator_delegation_redelegations", + ). + Columns( + "height", + "delegator_address", + "validator_src_address", + "validator_dst_address", + "entries", + ). + Values( + fmt.Sprintf("[%v,)", row.Height), + row.DelegatorAddress, + row.ValidatorSrcAddress, + row.ValidatorDstAddress, + entriesJSON, + ). + ToSql() + + if err != nil { + return fmt.Errorf("error building redelegation insertion sql: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + result, err := view.rdb.Exec(sql, sqlArgs...) + if err != nil { + return fmt.Errorf("error inserting redelegation into the table: %v: %w", err, rdb.ErrWrite) + } + if result.RowsAffected() != 1 { + return fmt.Errorf("error inserting redelegation into the table: no row upserted: %w", rdb.ErrWrite) + } return nil } -func (view *RedelegationsView) Upsert(row RedelegationRow) error { +func (view *RedelegationsView) Update(row RedelegationRow) error { + + // Check if there is an record's lower bound start with this height. + found, err := view.isExistedByLowerBoundHeight( + row.DelegatorAddress, + row.ValidatorSrcAddress, + row.ValidatorDstAddress, + row.Height, + ) + if err != nil { + return fmt.Errorf("error in checking new Redelegation record existence at this height: %v", err) + } + + if found { + // If there is a record that height = `[row.Height,)`, then update the existed one + + entriesJSON, err := jsoniter.MarshalToString(row.Entries) + if err != nil { + return fmt.Errorf("error JSON marshalling RedelegationRow.Entries for upsertion: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + sql, sqlArgs, err := view.rdb.StmtBuilder. + Update( + "view_validator_delegation_redelegations", + ). + SetMap(map[string]interface{}{ + "entries": entriesJSON, + }). + Where( + "delegator_address = ? AND validator_src_address = ? AND validator_dst_address = ? AND height @> ?::int8", + row.DelegatorAddress, + row.ValidatorSrcAddress, + row.ValidatorDstAddress, + row.Height, + ). + ToSql() + + if err != nil { + return fmt.Errorf("error building Redelegation update sql: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + result, err := view.rdb.Exec(sql, sqlArgs...) + if err != nil { + return fmt.Errorf("error updating Redelegation into the table: %v: %w", err, rdb.ErrWrite) + } + if result.RowsAffected() != 1 { + return fmt.Errorf("error updating Redelegation into the table: row updated: %v: %w", result.RowsAffected(), rdb.ErrWrite) + } + + } else { + // If there is not an existed record, then update the previous record's height range and insert a new record + + err := view.updateUpperBoundHeight(row) + if err != nil { + return fmt.Errorf("error updating Redelegation.Height upper bound: %v", err) + } + err = view.Insert(row) + if err != nil { + return fmt.Errorf("error inserting a new record for this Redelegation: %v", err) + } + + } return nil } +func (view *RedelegationsView) isExistedByLowerBoundHeight( + delegatorAddress string, + validatorSrcAddress string, + validatorDstAddress string, + height int64, +) (bool, error) { + + sql, sqlArgs, err := view.rdb.StmtBuilder. + Select( + "COUNT(*)", + ). + From("view_validator_delegation_redelegations"). + Where( + "delegator_address = ? AND validator_src_address = ? AND validator_dst_address = ? AND height = ?", + delegatorAddress, + validatorSrcAddress, + validatorDstAddress, + fmt.Sprintf("[%v,)", height), + ). + ToSql() + if err != nil { + return false, fmt.Errorf("error building sql to check redelegation at specific height: %v: %w", err, rdb.ErrPrepare) + } + + var count int64 + if err = view.rdb.QueryRow(sql, sqlArgs...).Scan( + &count, + ); err != nil { + return false, fmt.Errorf("error scanning count: %v: %w", err, rdb.ErrQuery) + } + + return count == 1, nil +} + func (view *RedelegationsView) Delete(row RedelegationRow) error { + return view.updateUpperBoundHeight(row) +} + +func (view *RedelegationsView) updateUpperBoundHeight(row RedelegationRow) error { + + // Set the upper bound for record height: `[, row.Height)`. + sql, sqlErr := view.rdb.StmtBuilder.ReplacePlaceholders(` + UPDATE view_validator_delegation_redelegations + SET height = int8range(lower(height), ?, '[)') + WHERE delegator_address = ? AND validator_src_address = ? AND validator_dst_address = ? AND height @> ?::int8 + `) + if sqlErr != nil { + return fmt.Errorf("error building Redelegation upper(height) update sql: %v: %w", sqlErr, rdb.ErrBuildSQLStmt) + } + sqlArgs := []interface{}{ + row.Height, + row.DelegatorAddress, + row.ValidatorSrcAddress, + row.ValidatorDstAddress, + row.Height, + } + + if _, execErr := view.rdb.Exec(sql, sqlArgs...); execErr != nil { + return fmt.Errorf("error executing Redelegation upper(height)update sql: %v: %w", execErr, rdb.ErrWrite) + } + return nil } @@ -49,9 +217,45 @@ func (view *RedelegationsView) FindBy( height int64, ) (RedelegationRow, bool, error) { - // TODO Handle the error when row is NOT FOUND + sql, sqlArgs, err := view.rdb.StmtBuilder. + Select( + "entries", + ). + From("view_validator_delegation_redelegations"). + Where( + "delegator_address = ? AND validator_src_address = ? AND validator_dst_address = ? AND height @> ?::int8", + delegatorAddress, + validatorSrcAddress, + validatorDstAddress, + height, + ). + ToSql() + if err != nil { + return RedelegationRow{}, false, fmt.Errorf("error building select redelegation sql: %v: %w", err, rdb.ErrPrepare) + } + + var redelegation RedelegationRow + redelegation.Height = height + redelegation.DelegatorAddress = delegatorAddress + redelegation.ValidatorSrcAddress = validatorSrcAddress + redelegation.ValidatorDstAddress = validatorDstAddress + + var entriesJSON string + if err = view.rdb.QueryRow(sql, sqlArgs...).Scan( + &entriesJSON, + ); err != nil { + if errors.Is(err, rdb.ErrNoRows) { + // When the row is not found, do not return error + return RedelegationRow{}, false, nil + } + return RedelegationRow{}, false, fmt.Errorf("error scanning redelegation: %v: %w", err, rdb.ErrQuery) + } + + if err = jsoniter.UnmarshalFromString(entriesJSON, &redelegation.Entries); err != nil { + return RedelegationRow{}, false, fmt.Errorf("error unmarshalling RedelegationRow.Entries JSON: %v: %w", err, rdb.ErrQuery) + } - return RedelegationRow{}, true, nil + return redelegation, true, nil } func (view *RedelegationsView) ListBySrcValidator( @@ -59,12 +263,122 @@ func (view *RedelegationsView) ListBySrcValidator( height int64, ) ([]RedelegationRow, error) { - return nil, nil + sql, sqlArgs, err := view.rdb.StmtBuilder. + Select( + "delegator_address", + "validator_dst_address", + "entries", + ). + From( + "view_validator_delegation_redelegations", + ). + Where( + "validator_src_address = ? AND height @> ?::int8", + validatorSrcAddress, + height, + ). + ToSql() + if err != nil { + return nil, fmt.Errorf("error building redelegation select by src validator SQL: %v, %w", err, rdb.ErrBuildSQLStmt) + } + + rowsResult, err := view.rdb.Query(sql, sqlArgs...) + if err != nil { + return nil, fmt.Errorf("error executing redelegation select by src validator SQL: %v: %w", err, rdb.ErrQuery) + } + defer rowsResult.Close() + + redelegations := make([]RedelegationRow, 0) + for rowsResult.Next() { + var redelegation RedelegationRow + redelegation.Height = height + redelegation.ValidatorSrcAddress = validatorSrcAddress + + var entriesJSON string + if err = rowsResult.Scan( + &redelegation.DelegatorAddress, + &redelegation.ValidatorDstAddress, + &entriesJSON, + ); err != nil { + return nil, fmt.Errorf("error scanning redelegation row: %v: %w", err, rdb.ErrQuery) + } + + if err = jsoniter.UnmarshalFromString(entriesJSON, &redelegation.Entries); err != nil { + return nil, fmt.Errorf("error unmarshalling RedelegationRow.Entries JSON: %v: %w", err, rdb.ErrQuery) + } + + redelegations = append(redelegations, redelegation) + } + + return redelegations, nil +} + +func (view *RedelegationsView) ListBySrcValidatorWithPagination( + validatorSrcAddress string, + height int64, + pagination *pagination_appinterface.Pagination, +) ([]RedelegationRow, *pagination_appinterface.PaginationResult, error) { + + stmtBuilder := view.rdb.StmtBuilder. + Select( + "delegator_address", + "validator_dst_address", + "entries", + ). + From( + "view_validator_delegation_redelegations", + ). + Where( + "validator_src_address = ? AND height @> ?::int8", + validatorSrcAddress, + height, + ) + + rDbPagination := rdb.NewRDbPaginationBuilder( + pagination, + view.rdb, + ).BuildStmt(stmtBuilder) + sql, sqlArgs, err := rDbPagination.ToStmtBuilder().ToSql() + if err != nil { + return nil, nil, fmt.Errorf("error building redelegation select by src validator SQL: %v, %w", err, rdb.ErrBuildSQLStmt) + } + + rowsResult, err := view.rdb.Query(sql, sqlArgs...) + if err != nil { + return nil, nil, fmt.Errorf("error executing redelegation select by src validator SQL: %v: %w", err, rdb.ErrQuery) + } + defer rowsResult.Close() + + redelegations := make([]RedelegationRow, 0) + for rowsResult.Next() { + var redelegation RedelegationRow + redelegation.Height = height + redelegation.ValidatorSrcAddress = validatorSrcAddress + + var entriesJSON string + if err = rowsResult.Scan( + &redelegation.DelegatorAddress, + &redelegation.ValidatorDstAddress, + &entriesJSON, + ); err != nil { + return nil, nil, fmt.Errorf("error scanning redelegation row: %v: %w", err, rdb.ErrQuery) + } + + if err = jsoniter.UnmarshalFromString(entriesJSON, &redelegation.Entries); err != nil { + return nil, nil, fmt.Errorf("error unmarshalling RedelegationRow.Entries JSON: %v: %w", err, rdb.ErrQuery) + } + + redelegations = append(redelegations, redelegation) + } + + paginationResult, err := rDbPagination.Result() + if err != nil { + return nil, nil, fmt.Errorf("error preparing pagination result: %v", err) + } + + return redelegations, paginationResult, nil } -// TODO: -// - UNIQUE(height, delegatorAddress, validatorSrcAddress, validatorDstAddress) -// - Index(height, validatorSrcAddress) type RedelegationRow struct { Height int64 `json:"height"` DelegatorAddress string `json:"delegatorAddress"` @@ -83,9 +397,10 @@ func NewRedelegationRow( sharesDst coin.Dec, ) RedelegationRow { return RedelegationRow{ + Height: creationHeight, DelegatorAddress: delegatorAddress, ValidatorSrcAddress: validatorSrcAddress, - ValidatorDstAddress: validatorSrcAddress, + ValidatorDstAddress: validatorDstAddress, Entries: []RedelegationEntry{ NewRedelegationEntry(creationHeight, completionTime, balance, sharesDst), }, diff --git a/projection/validator_delegation/view/unbonding_delegation_queue.go b/projection/validator_delegation/view/unbonding_delegation_queue.go index d0587371..afa3c131 100644 --- a/projection/validator_delegation/view/unbonding_delegation_queue.go +++ b/projection/validator_delegation/view/unbonding_delegation_queue.go @@ -1,8 +1,12 @@ package view import ( + "errors" + "fmt" + "github.com/crypto-com/chain-indexing/appinterface/rdb" "github.com/crypto-com/chain-indexing/external/utctime" + jsoniter "github.com/json-iterator/go" ) type UnbondingDelegationQueue interface { @@ -23,29 +27,150 @@ func NewUnbondingDelegationQueueView(handle *rdb.Handle) UnbondingDelegationQueu func (view *UnbondingDelegationQueueView) Upsert(row UnbondingDelegationQueueRow) error { + dvPairsJSON, err := jsoniter.MarshalToString(row.DVPairs) + if err != nil { + return fmt.Errorf("error JSON marshalling DVPairs for upsertion: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + sql, sqlArgs, err := view.rdb.StmtBuilder. + Insert( + "view_validator_delegation_unbonding_delegation_queue", + ). + Columns( + "completion_time", + "dv_pairs", + ). + Values( + view.rdb.Tton(&row.CompletionTime), + dvPairsJSON, + ). + Suffix("ON CONFLICT(completion_time) DO UPDATE SET dv_pairs = EXCLUDED.dv_pairs"). + ToSql() + + if err != nil { + return fmt.Errorf("error building UnbondingDelegationQueueRow upsertion sql: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + result, err := view.rdb.Exec(sql, sqlArgs...) + if err != nil { + return fmt.Errorf("error upserting UnbondingDelegationQueueRow into the table: %v: %w", err, rdb.ErrWrite) + } + if result.RowsAffected() != 1 { + return fmt.Errorf("error upserting UnbondingDelegationQueueRow into the table: no rows inserted: %w", rdb.ErrWrite) + } + return nil } func (view *UnbondingDelegationQueueView) FindBy(completionTime utctime.UTCTime) (UnbondingDelegationQueueRow, bool, error) { + + sql, sqlArgs, err := view.rdb.StmtBuilder. + Select( + "dv_pairs", + ). + From("view_validator_delegation_unbonding_delegation_queue"). + Where( + "completion_time = ?", + view.rdb.Tton(&completionTime), + ). + ToSql() + if err != nil { + return UnbondingDelegationQueueRow{}, false, fmt.Errorf("error building select UnbondingDelegationQueueRow sql: %v: %w", err, rdb.ErrPrepare) + } + var row UnbondingDelegationQueueRow + row.CompletionTime = completionTime + + var dvPairsJSON string + if err = view.rdb.QueryRow(sql, sqlArgs...).Scan( + &dvPairsJSON, + ); err != nil { + if errors.Is(err, rdb.ErrNoRows) { + // When the row is not found, do not return error + return UnbondingDelegationQueueRow{}, false, nil + } + return UnbondingDelegationQueueRow{}, false, fmt.Errorf("error scanning RedelegationQueueRow: %v: %w", err, rdb.ErrQuery) + } - // TODO handle the row not found + if err = jsoniter.UnmarshalFromString(dvPairsJSON, &row.DVPairs); err != nil { + return UnbondingDelegationQueueRow{}, false, fmt.Errorf("error unmarshalling DVPairs JSON: %v: %w", err, rdb.ErrQuery) + } return row, true, nil } func (view *UnbondingDelegationQueueView) DequeueAllMatureUnbondingDelegationQueue(blockTime utctime.UTCTime) ([]DVPair, error) { - // TODO find all mature UnbondingDelegationQueueRow, then concate their DVPairs + // Find all mature UnbondingDelegationQueueRow, then concate their DVPairs + + sql, sqlArgs, err := view.rdb.StmtBuilder. + Select( + "dv_pairs", + ). + From( + "view_validator_delegation_unbonding_delegation_queue", + ). + Where( + "completion_time <= ?", + view.rdb.Tton(&blockTime), + ). + ToSql() + if err != nil { + return nil, fmt.Errorf("error building mature UnbondingDelegationQueueRow select SQL: %v, %w", err, rdb.ErrBuildSQLStmt) + } + + rowsResult, err := view.rdb.Query(sql, sqlArgs...) + if err != nil { + return nil, fmt.Errorf("error executing mature UnbondingDelegationQueueRow select SQL: %v: %w", err, rdb.ErrQuery) + } + defer rowsResult.Close() + + matureDVPairs := make([]DVPair, 0) + for rowsResult.Next() { + + var dvPairsJSON string + if err = rowsResult.Scan( + &dvPairsJSON, + ); err != nil { + return nil, fmt.Errorf("error scanning UnbondingDelegationQueueRow: %v: %w", err, rdb.ErrQuery) + } - // Optional TODO: de-duplicate, a same DVPair could appear multiple times, we should avoid that + var dvPairs []DVPair + if err = jsoniter.UnmarshalFromString(dvPairsJSON, &dvPairs); err != nil { + return nil, fmt.Errorf("error unmarshalling DVPairs JSON: %v: %w", err, rdb.ErrQuery) + } - // TODO Delete the mature rows + matureDVPairs = append(matureDVPairs, dvPairs...) + } + + // Optional TODO: De-duplicate the return slice. A same DVPair could appear multiple times, we could avoid it here. + // At the moment, we are following CosmosSDK implementation, so not doing the de-duplicate. + + // Delete the mature rows + + sql, sqlArgs, err = view.rdb.StmtBuilder. + Delete( + "view_validator_delegation_unbonding_delegation_queue", + ). + Where( + "completion_time <= ?", + view.rdb.Tton(&blockTime), + ). + ToSql() + if err != nil { + return nil, fmt.Errorf("error building UnbondingDelegationQueueRow deletion sql: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + _, err = view.rdb.Exec(sql, sqlArgs...) + if err != nil { + return nil, fmt.Errorf("error deleting UnbondingDelegationQueueRow from the table: %v: %w", err, rdb.ErrWrite) + } - return nil, nil + return matureDVPairs, nil } -// TODO UNIQUE CompletionTime +// NOTES: +// - UNIQUE CompletionTime type UnbondingDelegationQueueRow struct { CompletionTime utctime.UTCTime `json:"completionTime"` DVPairs []DVPair `json:"dvPairs"` diff --git a/projection/validator_delegation/view/unbonding_delegations.go b/projection/validator_delegation/view/unbonding_delegations.go index 5878d0a3..fb8a51b6 100644 --- a/projection/validator_delegation/view/unbonding_delegations.go +++ b/projection/validator_delegation/view/unbonding_delegations.go @@ -1,20 +1,30 @@ package view import ( + "errors" + "fmt" "time" + pagination_appinterface "github.com/crypto-com/chain-indexing/appinterface/pagination" "github.com/crypto-com/chain-indexing/appinterface/rdb" "github.com/crypto-com/chain-indexing/external/utctime" "github.com/crypto-com/chain-indexing/usecase/coin" + jsoniter "github.com/json-iterator/go" ) type UnbondingDelegations interface { - Clone(previousHeight int64, currentHeight int64) error - - Upsert(row UnbondingDelegationRow) error + Insert(row UnbondingDelegationRow) error + Update(row UnbondingDelegationRow) error Delete(row UnbondingDelegationRow) error FindBy(delegatorAddress, validatorAddress string, height int64) (UnbondingDelegationRow, bool, error) + // For internal projection logic ListByValidator(validatorAddress string, height int64) ([]UnbondingDelegationRow, error) + // For HTTP API + ListByValidatorWithPagination( + validatorAddress string, + height int64, + pagination *pagination_appinterface.Pagination, + ) ([]UnbondingDelegationRow, *pagination_appinterface.PaginationResult, error) } type UnbondingDelegationsView struct { @@ -27,18 +37,164 @@ func NewUnbondingDelegationsView(handle *rdb.Handle) UnbondingDelegations { } } -func (view *UnbondingDelegationsView) Clone(previousHeight, currentHeight int64) error { +func (view *UnbondingDelegationsView) Insert(row UnbondingDelegationRow) error { + entriesJSON, err := jsoniter.MarshalToString(row.Entries) + if err != nil { + return fmt.Errorf("error JSON marshalling UnbondingDelegationRow.Entries for insertion: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + sql, sqlArgs, err := view.rdb.StmtBuilder. + Insert( + "view_validator_delegation_unbonding_delegations", + ). + Columns( + "height", + "delegator_address", + "validator_address", + "entries", + ). + Values( + fmt.Sprintf("[%v,)", row.Height), + row.DelegatorAddress, + row.ValidatorAddress, + entriesJSON, + ). + ToSql() + + if err != nil { + return fmt.Errorf("error building UnbondingDelegation insertion sql: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + result, err := view.rdb.Exec(sql, sqlArgs...) + if err != nil { + return fmt.Errorf("error inserting UnbondingDelegation into the table: %v: %w", err, rdb.ErrWrite) + } + if result.RowsAffected() != 1 { + return fmt.Errorf("error inserting UnbondingDelegation into the table: no row upserted: %w", rdb.ErrWrite) + } return nil } -func (view *UnbondingDelegationsView) Upsert(row UnbondingDelegationRow) error { +func (view *UnbondingDelegationsView) Update(row UnbondingDelegationRow) error { + + // Check if there is an record's lower bound start with this height. + found, err := view.isExistedByLowerBoundHeight(row.DelegatorAddress, row.ValidatorAddress, row.Height) + if err != nil { + return fmt.Errorf("error in checking new UnbondingDelegation record existence at this height: %v", err) + } + + if found { + // If there is a record that height = `[row.Height,)`, then update the existed one + + entriesJSON, err := jsoniter.MarshalToString(row.Entries) + if err != nil { + return fmt.Errorf("error JSON marshalling UnbondingDelegationRow.Entries for upsertion: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + sql, sqlArgs, err := view.rdb.StmtBuilder. + Update( + "view_validator_delegation_unbonding_delegations", + ). + SetMap(map[string]interface{}{ + "entries": entriesJSON, + }). + Where( + "delegator_address = ? AND validator_address = ? AND height @> ?::int8", + row.DelegatorAddress, + row.ValidatorAddress, + row.Height, + ). + ToSql() + + if err != nil { + return fmt.Errorf("error building UnbondingDelegation update sql: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + result, err := view.rdb.Exec(sql, sqlArgs...) + if err != nil { + return fmt.Errorf("error updating UnbondingDelegation into the table: %v: %w", err, rdb.ErrWrite) + } + if result.RowsAffected() != 1 { + return fmt.Errorf("error updating UnbondingDelegation into the table: row updated: %v: %w", result.RowsAffected(), rdb.ErrWrite) + } + + } else { + // If there is not an existed record, then update the previous record's height range and insert a new record + + err := view.updateUpperBoundHeight(row) + if err != nil { + return fmt.Errorf("error updating UnbondingDelegation.Height upper bound: %v", err) + } + err = view.Insert(row) + if err != nil { + return fmt.Errorf("error inserting a new record for this UnbondingDelegation: %v", err) + } + + } return nil } +func (view *UnbondingDelegationsView) isExistedByLowerBoundHeight( + delegatorAddress string, + validatorAddress string, + height int64, +) (bool, error) { + + sql, sqlArgs, err := view.rdb.StmtBuilder. + Select( + "COUNT(*)", + ). + From("view_validator_delegation_unbonding_delegations"). + Where( + "delegator_address = ? AND validator_address = ? AND height = ?", + delegatorAddress, + validatorAddress, + fmt.Sprintf("[%v,)", height), + ). + ToSql() + if err != nil { + return false, fmt.Errorf("error building sql to check unbonding delegation at specific height: %v: %w", err, rdb.ErrPrepare) + } + + var count int64 + if err = view.rdb.QueryRow(sql, sqlArgs...).Scan( + &count, + ); err != nil { + return false, fmt.Errorf("error scanning count: %v: %w", err, rdb.ErrQuery) + } + + return count == 1, nil +} + func (view *UnbondingDelegationsView) Delete(row UnbondingDelegationRow) error { + return view.updateUpperBoundHeight(row) +} + +func (view *UnbondingDelegationsView) updateUpperBoundHeight(row UnbondingDelegationRow) error { + + // Set the upper bound for record height: `[, row.Height)`. + sql, sqlErr := view.rdb.StmtBuilder.ReplacePlaceholders(` + UPDATE view_validator_delegation_unbonding_delegations + SET height = int8range(lower(height), ?, '[)') + WHERE delegator_address = ? AND validator_address = ? AND height @> ?::int8 + `) + if sqlErr != nil { + return fmt.Errorf("error building UnbondingDelegation upper(height) update sql: %v: %w", sqlErr, rdb.ErrBuildSQLStmt) + } + sqlArgs := []interface{}{ + row.Height, + row.DelegatorAddress, + row.ValidatorAddress, + row.Height, + } + + if _, execErr := view.rdb.Exec(sql, sqlArgs...); execErr != nil { + return fmt.Errorf("error executing UnbondingDelegation upper(height)update sql: %v: %w", execErr, rdb.ErrWrite) + } + return nil } @@ -48,7 +204,43 @@ func (view *UnbondingDelegationsView) FindBy( height int64, ) (UnbondingDelegationRow, bool, error) { - return UnbondingDelegationRow{}, true, nil + sql, sqlArgs, err := view.rdb.StmtBuilder. + Select( + "entries", + ). + From("view_validator_delegation_unbonding_delegations"). + Where( + "delegator_address = ? AND validator_address = ? AND height @> ?::int8", + delegatorAddress, + validatorAddress, + height, + ). + ToSql() + if err != nil { + return UnbondingDelegationRow{}, false, fmt.Errorf("error building select UnbondingDelegation sql: %v: %w", err, rdb.ErrPrepare) + } + + var unbondingDelegation UnbondingDelegationRow + unbondingDelegation.Height = height + unbondingDelegation.DelegatorAddress = delegatorAddress + unbondingDelegation.ValidatorAddress = validatorAddress + + var entriesJSON string + if err = view.rdb.QueryRow(sql, sqlArgs...).Scan( + &entriesJSON, + ); err != nil { + if errors.Is(err, rdb.ErrNoRows) { + // When the row is not found, do not return error + return UnbondingDelegationRow{}, false, nil + } + return UnbondingDelegationRow{}, false, fmt.Errorf("error scanning UnbondingDelegation: %v: %w", err, rdb.ErrQuery) + } + + if err = jsoniter.UnmarshalFromString(entriesJSON, &unbondingDelegation.Entries); err != nil { + return UnbondingDelegationRow{}, false, fmt.Errorf("error unmarshalling UnbondingDelegation.Entries JSON: %v: %w", err, rdb.ErrQuery) + } + + return unbondingDelegation, true, nil } func (view *UnbondingDelegationsView) ListByValidator( @@ -56,13 +248,118 @@ func (view *UnbondingDelegationsView) ListByValidator( height int64, ) ([]UnbondingDelegationRow, error) { - return nil, nil + sql, sqlArgs, err := view.rdb.StmtBuilder. + Select( + "delegator_address", + "entries", + ). + From( + "view_validator_delegation_unbonding_delegations", + ). + Where( + "validator_address = ? AND height @> ?::int8", + validatorAddress, + height, + ). + ToSql() + if err != nil { + return nil, fmt.Errorf("error building UnbondingDelegation select by validator SQL: %v, %w", err, rdb.ErrBuildSQLStmt) + } + + rowsResult, err := view.rdb.Query(sql, sqlArgs...) + if err != nil { + return nil, fmt.Errorf("error executing UnbondingDelegation select by validator SQL: %v: %w", err, rdb.ErrQuery) + } + defer rowsResult.Close() + + unbondingDelegations := make([]UnbondingDelegationRow, 0) + for rowsResult.Next() { + var unbondingDelegation UnbondingDelegationRow + unbondingDelegation.Height = height + unbondingDelegation.ValidatorAddress = validatorAddress + + var entriesJSON string + if err = rowsResult.Scan( + &unbondingDelegation.DelegatorAddress, + &entriesJSON, + ); err != nil { + return nil, fmt.Errorf("error scanning UnbondingDelegation row: %v: %w", err, rdb.ErrQuery) + } + + if err = jsoniter.UnmarshalFromString(entriesJSON, &unbondingDelegation.Entries); err != nil { + return nil, fmt.Errorf("error unmarshalling UnbondingDelegationRow.Entries JSON: %v: %w", err, rdb.ErrQuery) + } + + unbondingDelegations = append(unbondingDelegations, unbondingDelegation) + } + + return unbondingDelegations, nil +} + +func (view *UnbondingDelegationsView) ListByValidatorWithPagination( + validatorAddress string, + height int64, + pagination *pagination_appinterface.Pagination, +) ([]UnbondingDelegationRow, *pagination_appinterface.PaginationResult, error) { + + stmtBuilder := view.rdb.StmtBuilder. + Select( + "delegator_address", + "entries", + ). + From( + "view_validator_delegation_unbonding_delegations", + ). + Where( + "validator_address = ? AND height @> ?::int8", + validatorAddress, + height, + ) + + rDbPagination := rdb.NewRDbPaginationBuilder( + pagination, + view.rdb, + ).BuildStmt(stmtBuilder) + sql, sqlArgs, err := rDbPagination.ToStmtBuilder().ToSql() + if err != nil { + return nil, nil, fmt.Errorf("error building UnbondingDelegation select by validator SQL: %v, %w", err, rdb.ErrBuildSQLStmt) + } + + rowsResult, err := view.rdb.Query(sql, sqlArgs...) + if err != nil { + return nil, nil, fmt.Errorf("error executing UnbondingDelegation select by validator SQL: %v: %w", err, rdb.ErrQuery) + } + defer rowsResult.Close() + + unbondingDelegations := make([]UnbondingDelegationRow, 0) + for rowsResult.Next() { + var unbondingDelegation UnbondingDelegationRow + unbondingDelegation.Height = height + unbondingDelegation.ValidatorAddress = validatorAddress + + var entriesJSON string + if err = rowsResult.Scan( + &unbondingDelegation.DelegatorAddress, + &entriesJSON, + ); err != nil { + return nil, nil, fmt.Errorf("error scanning UnbondingDelegation row: %v: %w", err, rdb.ErrQuery) + } + + if err = jsoniter.UnmarshalFromString(entriesJSON, &unbondingDelegation.Entries); err != nil { + return nil, nil, fmt.Errorf("error unmarshalling UnbondingDelegationRow.Entries JSON: %v: %w", err, rdb.ErrQuery) + } + + unbondingDelegations = append(unbondingDelegations, unbondingDelegation) + } + + paginationResult, err := rDbPagination.Result() + if err != nil { + return nil, nil, fmt.Errorf("error preparing pagination result: %v", err) + } + + return unbondingDelegations, paginationResult, nil } -// TODO: -// - UNIQUE(height, delegatorAddress, validatorAddress) -// - INDEX(height, validatorAddress) -// - INDEX(height, delegatorAddress) type UnbondingDelegationRow struct { Height int64 `json:"height"` DelegatorAddress string `json:"delegatorAddress"` @@ -78,6 +375,7 @@ func NewUnbondingDelegationRow( balance coin.Int, ) UnbondingDelegationRow { return UnbondingDelegationRow{ + Height: creationHeight, DelegatorAddress: delegatorAddress, ValidatorAddress: validatorAddress, Entries: []UnbondingDelegationEntry{ diff --git a/projection/validator_delegation/view/unbonding_validators.go b/projection/validator_delegation/view/unbonding_validators.go index 29eea908..d3d234f6 100644 --- a/projection/validator_delegation/view/unbonding_validators.go +++ b/projection/validator_delegation/view/unbonding_validators.go @@ -1,12 +1,14 @@ package view import ( + "fmt" + "github.com/crypto-com/chain-indexing/appinterface/rdb" "github.com/crypto-com/chain-indexing/external/utctime" ) type UnbondingValidators interface { - Insert(operatorAddress string, UnbondingTime utctime.UTCTime) error + Insert(operatorAddress string, unbondingTime utctime.UTCTime) error RemoveIfExist(operatorAddress string) error GetMatureEntries(blockTime utctime.UTCTime) ([]UnbondingValidatorRow, error) DeleteMatureEntries(blockTime utctime.UTCTime) error @@ -22,29 +24,138 @@ func NewUnbondingValidatorsView(handle *rdb.Handle) UnbondingValidators { } } -func (view *UnbondingValidatorsView) Insert(operatorAddress string, UnbondingTime utctime.UTCTime) error { +func (view *UnbondingValidatorsView) Insert(operatorAddress string, unbondingTime utctime.UTCTime) error { + + sql, sqlArgs, err := view.rdb.StmtBuilder. + Insert( + "view_validator_delegation_unbonding_validators", + ). + Columns( + "operator_address", + "unbonding_time", + ). + Values( + operatorAddress, + view.rdb.Tton(&unbondingTime), + ). + ToSql() + + if err != nil { + return fmt.Errorf("error building UnbondingValidator insertion sql: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + result, err := view.rdb.Exec(sql, sqlArgs...) + if err != nil { + return fmt.Errorf("error inserting UnbondingValidator into the table: %v: %w", err, rdb.ErrWrite) + } + if result.RowsAffected() != 1 { + return fmt.Errorf("error inserting UnbondingValidator into the table: rows inserted: %v: %w", result.RowsAffected(), rdb.ErrWrite) + } return nil } func (view *UnbondingValidatorsView) RemoveIfExist(operatorAddress string) error { - // First to check if the row exist, then remove it + + sql, sqlArgs, err := view.rdb.StmtBuilder. + Delete( + "view_validator_delegation_unbonding_validators", + ). + Where( + "operator_address = ?", + operatorAddress, + ). + ToSql() + if err != nil { + return fmt.Errorf("error building UnbondingValidator deletion sql: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + // Not checking the number of rows deleted here, as it could be 0 or 1. + _, err = view.rdb.Exec(sql, sqlArgs...) + if err != nil { + return fmt.Errorf("error deleting UnbondingValidator from the table: %v: %w", err, rdb.ErrWrite) + } return nil } func (view *UnbondingValidatorsView) GetMatureEntries(blockTime utctime.UTCTime) ([]UnbondingValidatorRow, error) { - return nil, nil + sql, sqlArgs, err := view.rdb.StmtBuilder. + Select( + "operator_address", + "unbonding_time", + ). + From( + "view_validator_delegation_unbonding_validators", + ). + Where( + "unbonding_time <= ?", + view.rdb.Tton(&blockTime), + ). + ToSql() + if err != nil { + return nil, fmt.Errorf("error building mature UnbondingValidatorRow select SQL: %v, %w", err, rdb.ErrBuildSQLStmt) + } + + rowsResult, err := view.rdb.Query(sql, sqlArgs...) + if err != nil { + return nil, fmt.Errorf("error executing mature UnbondingValidatorRow select SQL: %v: %w", err, rdb.ErrQuery) + } + defer rowsResult.Close() + + matureRows := make([]UnbondingValidatorRow, 0) + for rowsResult.Next() { + + var row UnbondingValidatorRow + unbondingTimeReader := view.rdb.NtotReader() + + if err = rowsResult.Scan( + &row.OperatorAddress, + unbondingTimeReader.ScannableArg(), + ); err != nil { + return nil, fmt.Errorf("error scanning UnbondingValidatorRow: %v: %w", err, rdb.ErrQuery) + } + + unbondingTime, parseErr := unbondingTimeReader.Parse() + if parseErr != nil { + return nil, fmt.Errorf("error parsing unbondingTime: %v: %w", parseErr, rdb.ErrQuery) + } + row.UnbondingTime = *unbondingTime + + matureRows = append(matureRows, row) + } + + return matureRows, nil } func (view *UnbondingValidatorsView) DeleteMatureEntries(blockTime utctime.UTCTime) error { + sql, sqlArgs, err := view.rdb.StmtBuilder. + Delete( + "view_validator_delegation_unbonding_validators", + ). + Where( + "unbonding_time <= ?", + view.rdb.Tton(&blockTime), + ). + ToSql() + if err != nil { + return fmt.Errorf("error building UnbondingValidatorRow deletion sql: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + _, err = view.rdb.Exec(sql, sqlArgs...) + if err != nil { + return fmt.Errorf("error deleting UnbondingValidatorRow from the table: %v: %w", err, rdb.ErrWrite) + } + return nil } -// TODO: UNIQUE OperatorAddress +// NOTES: +// - UNIQUE OperatorAddress type UnbondingValidatorRow struct { - OperatorAddress string `json:"operatorAddress"` - UnbondingTime utctime.UTCTime `json:"UnbondingTime"` + OperatorAddress string `json:"operatorAddress"` + // UnbondingTime is the time when Unbonding is finished + UnbondingTime utctime.UTCTime `json:"UnbondingTime"` } diff --git a/projection/validator_delegation/view/validators.go b/projection/validator_delegation/view/validators.go index 00ffdc84..4649a40c 100644 --- a/projection/validator_delegation/view/validators.go +++ b/projection/validator_delegation/view/validators.go @@ -1,6 +1,9 @@ package view import ( + "errors" + "fmt" + "github.com/crypto-com/chain-indexing/appinterface/pagination" "github.com/crypto-com/chain-indexing/appinterface/rdb" "github.com/crypto-com/chain-indexing/external/utctime" @@ -9,13 +12,11 @@ import ( ) type Validators interface { - Clone(previousHeight int64, currentHeight int64) error Insert(row ValidatorRow) error Update(row ValidatorRow) error Delete(row ValidatorRow) error FindByOperatorAddr(operatorAddress string, height int64) (ValidatorRow, bool, error) FindByConsensusNodeAddr(consensusNodeAddress string, height int64) (ValidatorRow, bool, error) - FindByTendermintAddr(tendermintAddress string, height int64) (ValidatorRow, error) List(height int64, pagination *pagination.Pagination) ([]ValidatorRow, *pagination.PaginationResult, error) } @@ -29,43 +30,288 @@ func NewValidatorsView(handle *rdb.Handle) Validators { } } -func (view *ValidatorsView) Clone(previousHeight, currentHeight int64) error { +func (view *ValidatorsView) Insert(row ValidatorRow) error { - return nil -} + sql, sqlArgs, err := view.rdb.StmtBuilder. + Insert( + "view_validator_delegation_validators", + ). + Columns( + "height", + "operator_address", + "consensus_node_address", + "tendermint_address", + "status", + "jailed", + "power", + "unbonding_height", + "unbonding_time", + "tokens", + "shares", + "min_self_delegation", + ). + Values( + fmt.Sprintf("[%v,)", row.Height), + row.OperatorAddress, + row.ConsensusNodeAddress, + row.TendermintAddress, + row.Status, + row.Jailed, + row.Power, + row.UnbondingHeight, + view.rdb.Tton(&row.UnbondingTime), + row.Tokens.String(), + row.Shares.String(), + row.MinSelfDelegation.String(), + ). + ToSql() -func (view *ValidatorsView) Insert(row ValidatorRow) error { + if err != nil { + return fmt.Errorf("error building validator insertion sql: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + result, err := view.rdb.Exec(sql, sqlArgs...) + if err != nil { + return fmt.Errorf("error inserting validator into the table: %v: %w", err, rdb.ErrWrite) + } + if result.RowsAffected() != 1 { + return fmt.Errorf("error inserting validator into the table: rows inserted: %v: %w", result.RowsAffected(), rdb.ErrWrite) + } return nil } func (view *ValidatorsView) Update(row ValidatorRow) error { + // Check if there is an record's lower bound start with this height. + found, err := view.isExistedByLowerBoundHeight(row.OperatorAddress, row.Height) + if err != nil { + return fmt.Errorf("error in checking new validator record existence at this height: %v", err) + } + + if found { + // If there is a record that height = `[row.Height,)`, then update the existed one + + sql, sqlArgs, err := view.rdb.StmtBuilder. + Update( + "view_validator_delegation_validators", + ). + SetMap(map[string]interface{}{ + "status": row.Status, + "jailed": row.Jailed, + "power": row.Power, + "unbonding_height": row.UnbondingHeight, + "unbonding_time": view.rdb.Tton(&row.UnbondingTime), + "tokens": row.Tokens.String(), + "shares": row.Shares.String(), + "min_self_delegation": row.MinSelfDelegation.String(), + }). + Where( + "operator_address = ? AND height @> ?::int8", + row.OperatorAddress, + row.Height, + ). + ToSql() + + if err != nil { + return fmt.Errorf("error building validator update sql: %v: %w", err, rdb.ErrBuildSQLStmt) + } + + result, err := view.rdb.Exec(sql, sqlArgs...) + if err != nil { + return fmt.Errorf("error updating validator into the table: %v: %w", err, rdb.ErrWrite) + } + if result.RowsAffected() != 1 { + return fmt.Errorf("error updating validator into the table: row updated: %v: %w", result.RowsAffected(), rdb.ErrWrite) + } + + } else { + // If there is not an existed record, then update the previous record's height range and insert a new record + + err := view.updateUpperBoundHeight(row) + if err != nil { + return fmt.Errorf("error updating Validator.Height upper bound: %v", err) + } + + err = view.Insert(row) + if err != nil { + return fmt.Errorf("error inserting a new record for this validator: %v", err) + } + + } + return nil } +func (view *ValidatorsView) isExistedByLowerBoundHeight(operatorAddress string, height int64) (bool, error) { + + sql, sqlArgs, err := view.rdb.StmtBuilder. + Select( + "COUNT(*)", + ). + From("view_validator_delegation_validators"). + Where( + "operator_address = ? AND height = ?", + operatorAddress, + fmt.Sprintf("[%v,)", height), + ). + ToSql() + if err != nil { + return false, fmt.Errorf("error building sql to check validator at specific height: %v: %w", err, rdb.ErrPrepare) + } + + var count int64 + if err = view.rdb.QueryRow(sql, sqlArgs...).Scan( + &count, + ); err != nil { + return false, fmt.Errorf("error scanning count: %v: %w", err, rdb.ErrQuery) + } + + return count == 1, nil +} + func (view *ValidatorsView) Delete(row ValidatorRow) error { + return view.updateUpperBoundHeight(row) +} + +func (view *ValidatorsView) updateUpperBoundHeight(row ValidatorRow) error { + + // Set the upper bound for record height: `[, row.Height)`. + sql, sqlErr := view.rdb.StmtBuilder.ReplacePlaceholders(` + UPDATE view_validator_delegation_validators + SET height = int8range(lower(height), ?, '[)') + WHERE operator_address = ? AND height @> ?::int8 + `) + if sqlErr != nil { + return fmt.Errorf("error building validator upper(height) update sql: %v: %w", sqlErr, rdb.ErrBuildSQLStmt) + } + sqlArgs := []interface{}{ + row.Height, + row.OperatorAddress, + row.Height, + } + + if _, execErr := view.rdb.Exec(sql, sqlArgs...); execErr != nil { + return fmt.Errorf("error executing validator upper(height)update sql: %v: %w", execErr, rdb.ErrWrite) + } + return nil } func (view *ValidatorsView) FindByOperatorAddr(operatorAddress string, height int64) (ValidatorRow, bool, error) { - // TODO handle the error when validator is NOT FOUND + row, found, err := view.findBy(&operatorAddress, nil, height) + if err != nil { + return ValidatorRow{}, false, fmt.Errorf("error finding validator by OperatorAddr: %v", err) + } - return ValidatorRow{}, true, nil + return row, found, nil } func (view *ValidatorsView) FindByConsensusNodeAddr(consensusNodeAddress string, height int64) (ValidatorRow, bool, error) { - // TODO handle the error when validator is NOT FOUND + row, found, err := view.findBy(nil, &consensusNodeAddress, height) + if err != nil { + return ValidatorRow{}, false, fmt.Errorf("error finding validator by OperatorAddr: %v", err) + } - return ValidatorRow{}, true, nil + return row, found, nil } -func (view *ValidatorsView) FindByTendermintAddr(tendermintAddress string, height int64) (ValidatorRow, error) { +func (view *ValidatorsView) findBy( + maybeOperatorAddress *string, + maybeConsensusNodeAddress *string, + height int64, +) (ValidatorRow, bool, error) { + + stmtBuilder := view.rdb.StmtBuilder. + Select( + "operator_address", + "consensus_node_address", + "tendermint_address", + "status", + "jailed", + "power", + "unbonding_height", + "unbonding_time", + "tokens", + "shares", + "min_self_delegation", + ). + From("view_validator_delegation_validators") + + if maybeOperatorAddress != nil { + stmtBuilder = stmtBuilder.Where( + "operator_address = ? AND height @> ?::int8", + *maybeOperatorAddress, + height, + ) + } else if maybeConsensusNodeAddress != nil { + stmtBuilder = stmtBuilder.Where( + "consensus_node_address = ? AND height @> ?::int8", + *maybeConsensusNodeAddress, + height, + ) + } else { + return ValidatorRow{}, false, errors.New("no address provided for finding validator") + } + + sql, sqlArgs, err := stmtBuilder.ToSql() + if err != nil { + return ValidatorRow{}, false, fmt.Errorf("error building select validator sql: %v: %w", err, rdb.ErrPrepare) + } + + var validator ValidatorRow + validator.Height = height + + unbondingTimeReader := view.rdb.NtotReader() + var tokensInString string + var sharesInString string + var minSelfDelegationInString string + if err = view.rdb.QueryRow(sql, sqlArgs...).Scan( + &validator.OperatorAddress, + &validator.ConsensusNodeAddress, + &validator.TendermintAddress, + &validator.Status, + &validator.Jailed, + &validator.Power, + &validator.UnbondingHeight, + unbondingTimeReader.ScannableArg(), + &tokensInString, + &sharesInString, + &minSelfDelegationInString, + ); err != nil { + if errors.Is(err, rdb.ErrNoRows) { + // When the row is not found, do not return error + return ValidatorRow{}, false, nil + } + return ValidatorRow{}, false, fmt.Errorf("error scanning validator: %v: %w", err, rdb.ErrQuery) + } + + unbondingTime, parseErr := unbondingTimeReader.Parse() + if parseErr != nil { + return ValidatorRow{}, false, fmt.Errorf("error parsing unbondingTime: %v: %w", parseErr, rdb.ErrQuery) + } + validator.UnbondingTime = *unbondingTime + + var ok bool + validator.Tokens, ok = coin.NewIntFromString(tokensInString) + if !ok { + return ValidatorRow{}, false, fmt.Errorf("error parsing tokens to coin.Int: %w", rdb.ErrQuery) + } + + validator.Shares, err = coin.NewDecFromStr(sharesInString) + if err != nil { + return ValidatorRow{}, false, fmt.Errorf("error parsing shares to coin.Dec: %v: %w", err, rdb.ErrQuery) + } + + validator.MinSelfDelegation, ok = coin.NewIntFromString(minSelfDelegationInString) + if !ok { + return ValidatorRow{}, false, fmt.Errorf("error parsing min_self_delegation to coin.Int: %w", rdb.ErrQuery) + } - return ValidatorRow{}, nil + return validator, true, nil } func (view *ValidatorsView) List( @@ -77,7 +323,99 @@ func (view *ValidatorsView) List( error, ) { - return nil, nil, nil + stmtBuilder := view.rdb.StmtBuilder. + Select( + "operator_address", + "consensus_node_address", + "tendermint_address", + "status", + "jailed", + "power", + "unbonding_height", + "unbonding_time", + "tokens", + "shares", + "min_self_delegation", + ). + From( + "view_validator_delegation_validators", + ). + Where( + "height @> ?::int8", + height, + ) + + rDbPagination := rdb.NewRDbPaginationBuilder( + pagination, + view.rdb, + ).BuildStmt(stmtBuilder) + sql, sqlArgs, err := rDbPagination.ToStmtBuilder().ToSql() + if err != nil { + return nil, nil, fmt.Errorf("error building validators select SQL: %v, %w", err, rdb.ErrBuildSQLStmt) + } + + rowsResult, err := view.rdb.Query(sql, sqlArgs...) + if err != nil { + return nil, nil, fmt.Errorf("error executing validators select SQL: %v: %w", err, rdb.ErrQuery) + } + defer rowsResult.Close() + + validators := make([]ValidatorRow, 0) + for rowsResult.Next() { + var validator ValidatorRow + validator.Height = height + + unbondingTimeReader := view.rdb.NtotReader() + var tokensInString string + var sharesInString string + var minSelfDelegationInString string + if err = rowsResult.Scan( + &validator.OperatorAddress, + &validator.ConsensusNodeAddress, + &validator.TendermintAddress, + &validator.Status, + &validator.Jailed, + &validator.Power, + &validator.UnbondingHeight, + unbondingTimeReader.ScannableArg(), + &tokensInString, + &sharesInString, + &minSelfDelegationInString, + ); err != nil { + return nil, nil, fmt.Errorf("error scanning delegation row: %v: %w", err, rdb.ErrQuery) + } + + unbondingTime, parseErr := unbondingTimeReader.Parse() + if parseErr != nil { + return nil, nil, fmt.Errorf("error parsing unbondingTime: %v: %w", parseErr, rdb.ErrQuery) + } + validator.UnbondingTime = *unbondingTime + + var ok bool + validator.Tokens, ok = coin.NewIntFromString(tokensInString) + if !ok { + return nil, nil, fmt.Errorf("error parsing tokens to coin.Int: %w", rdb.ErrQuery) + } + + validator.Shares, err = coin.NewDecFromStr(sharesInString) + if err != nil { + return nil, nil, fmt.Errorf("error parsing shares to coin.Dec: %v: %w", err, rdb.ErrQuery) + } + + validator.MinSelfDelegation, ok = coin.NewIntFromString(minSelfDelegationInString) + if !ok { + return nil, nil, fmt.Errorf("error parsing min_self_delegation to coin.Int: %w", rdb.ErrQuery) + } + + validators = append(validators, validator) + } + + paginationResult, err := rDbPagination.Result() + if err != nil { + return nil, nil, fmt.Errorf("error preparing pagination result: %v", err) + } + + return validators, paginationResult, nil } // Notes on `Validator.Status`: @@ -88,9 +426,6 @@ func (view *ValidatorsView) List( // - power = 0: Validator.Status = Unbonding, UnbondingValidator entry created // - UnbondingValidator complete Unbonding period: Validator.Status = Unbonded -// TODO: -// - UNIQUE(height, operatorAddress) -// - UNIQUE(height, consensusNodeAddress) type ValidatorRow struct { Height int64 `json:"height"` @@ -104,9 +439,9 @@ type ValidatorRow struct { // `UnbondingHeight` and `UnbondingTime` only useful when `Status` is `Unbonding` // The height start the Unbonding - UnbondingHeight int64 `json:"UnbondingHeight"` + UnbondingHeight int64 `json:"unbondingHeight"` // The time when Unbonding is finished - UnbondingTime utctime.UTCTime `json:"UnbondingTime"` + UnbondingTime utctime.UTCTime `json:"unbondingTime"` Tokens coin.Int `json:"tokens"` Shares coin.Dec `json:"shares"`