From c35e6af5475d148533e0b1aa7c630399588f488f Mon Sep 17 00:00:00 2001 From: Michal Turcan Date: Wed, 28 Jun 2023 15:51:11 +0200 Subject: [PATCH 1/4] Feat/astroport (#27) * dockerfile * feat: price feed from astroport and dynamic rebase factor * add build docker gh * remove dockerhub reference * feat: rebase factor for ampluna and backbone from smart contract * removing panic when .env file is not present * feat: feeder localhost * push image also to ecr * update gh * update params * update gh * run dispatch only on develop or main * feat: chain ID env var * feat: redelegate parse all validators data * feat: redelegations * feat: rebalance redelegations algorithm * fix: redelegations * fix: redelegation algo * fix: redelegation between comp vals * refactor redelegation logic * chore: tests and fix bugs with compliant vals * feat: alliance-oracle-feeder implement alliance-rebalance-feeder --------- Co-authored-by: Michal Turcan Co-authored-by: emidev98 Co-authored-by: javiersuweijie Co-authored-by: emidev98 <49301655+emidev98@users.noreply.github.com> --- Makefile | 7 +- cmd/feeder/feeder.go | 14 +- cmd/price-server/price_server.go | 11 +- config/config.go | 1 + config/default_config.go | 39 +- .../parser/internal/astroport/astroport.go | 33 ++ .../parser/internal/coingecko/coingecko.go | 5 +- internal/parser/symbol_parser.go | 3 + .../alliance_protocols_info.go} | 77 +-- .../provider/alliance/alliance_provider.go | 31 ++ .../{ => alliance}/alliance_querier.go | 37 +- .../provider/alliance/alliance_validators.go | 451 ++++++++++++++++++ .../alliance/alliance_validators_test.go | 284 +++++++++++ internal/provider/internal/base_grpc.go | 36 ++ internal/provider/lsd_provider.go | 120 +++++ internal/provider/provider.go | 2 +- internal/provider/provider_manager.go | 2 - internal/provider/transactions_provider.go | 144 +++--- internal/restful/client.go | 3 + .../restful/internal/astroport/astroport.go | 102 ++++ internal/types/alliance_hub_config.go | 11 + internal/types/alliance_redelegate.go | 29 ++ internal/types/astroport.go | 59 +++ internal/types/bone_luna_stake_contract.go | 12 + internal/types/eris_stake_contract.go | 15 + internal/types/feeder_type.go | 38 ++ internal/types/station_gov.go | 20 + internal/types/stride.go | 51 ++ params.json | 7 +- pkg/types/execute.go | 15 + 30 files changed, 1513 insertions(+), 146 deletions(-) create mode 100644 internal/parser/internal/astroport/astroport.go rename internal/provider/{alliance_provider.go => alliance/alliance_protocols_info.go} (79%) create mode 100644 internal/provider/alliance/alliance_provider.go rename internal/provider/{ => alliance}/alliance_querier.go (50%) create mode 100644 internal/provider/alliance/alliance_validators.go create mode 100644 internal/provider/alliance/alliance_validators_test.go create mode 100644 internal/provider/internal/base_grpc.go create mode 100644 internal/provider/lsd_provider.go create mode 100644 internal/restful/internal/astroport/astroport.go create mode 100644 internal/types/alliance_hub_config.go create mode 100644 internal/types/alliance_redelegate.go create mode 100644 internal/types/astroport.go create mode 100644 internal/types/bone_luna_stake_contract.go create mode 100644 internal/types/eris_stake_contract.go create mode 100644 internal/types/feeder_type.go create mode 100644 internal/types/station_gov.go create mode 100644 internal/types/stride.go diff --git a/Makefile b/Makefile index 86eb7f8..87dee5a 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,11 @@ ################################################# ### START ### ################################################# -start-feeder: - go run ./cmd/feeder/feeder.go +start-alliance-oracle-feeder: + go run ./cmd/feeder/feeder.go alliance-oracle-feeder + +start-alliance-rebalance-feeder: + go run ./cmd/feeder/feeder.go alliance-rebalance-feeder start-price-server: go run ./cmd/price-server/price_server.go diff --git a/cmd/feeder/feeder.go b/cmd/feeder/feeder.go index 7ee459d..870c304 100644 --- a/cmd/feeder/feeder.go +++ b/cmd/feeder/feeder.go @@ -9,10 +9,12 @@ import ( "time" "github.com/joho/godotenv" - "github.com/terra-money/oracle-feeder-go/internal/provider" + alliance_provider "github.com/terra-money/oracle-feeder-go/internal/provider/alliance" + "github.com/terra-money/oracle-feeder-go/internal/types" ) func main() { + // Load the environment variables err := godotenv.Load() if err != nil { log.Print("Error loading .env file:", err) @@ -24,9 +26,17 @@ func main() { log.Fatal("Error parsing FEEDER_RETRIES:", err) } } + // Read the cli arguments + if len(os.Args) != 2 || os.Args[1] == "" { + log.Fatal(`Specify the first argument as the feeder type.`) + } + feederType, err := types.ParseFeederTypeFromString(os.Args[1]) + if err != nil { + log.Fatal(err) + } ctx := context.Background() - alliancesQuerierProvider := provider.NewAlliancesQuerierProvider() + alliancesQuerierProvider := alliance_provider.NewAlliancesQuerierProvider(feederType) for attempt := 1; attempt <= retries; attempt++ { _, err := alliancesQuerierProvider.QueryAndSubmitOnChain(ctx) diff --git a/cmd/price-server/price_server.go b/cmd/price-server/price_server.go index 7f6367d..261f3f3 100644 --- a/cmd/price-server/price_server.go +++ b/cmd/price-server/price_server.go @@ -10,6 +10,7 @@ import ( "github.com/joho/godotenv" "github.com/terra-money/oracle-feeder-go/config" "github.com/terra-money/oracle-feeder-go/internal/provider" + alliance_provider "github.com/terra-money/oracle-feeder-go/internal/provider/alliance" ) func main() { @@ -21,7 +22,7 @@ func main() { stopCh := make(chan struct{}) manager := provider.NewProviderManager(&config.DefaultPriceServerConfig, stopCh) - allianceProvider := provider.NewAllianceProvider(&config.DefaultAllianceConfig, manager) + allianceProvider := alliance_provider.NewAllianceProvider(&config.DefaultAllianceConfig, manager) r := gin.Default() r.GET("/health", func(c *gin.Context) { @@ -39,6 +40,14 @@ func main() { } c.JSON(http.StatusOK, allianceProtocolRes) }) + r.GET("/alliance/rebalance", func(c *gin.Context) { + allianceRebalanceVals, err := allianceProvider.GetAllianceRedelegateReq(ctx) + if err != nil { + c.JSON(http.StatusInternalServerError, err) + return + } + c.JSON(http.StatusOK, allianceRebalanceVals) + }) if os.Getenv("PRICE_SERVER_PORT") == "" { os.Setenv("PORT", "8532") // use 8532 by default } else { diff --git a/config/config.go b/config/config.go index 13af6bd..36f8cc2 100644 --- a/config/config.go +++ b/config/config.go @@ -25,6 +25,7 @@ type AllianceConfig struct { } type LSTData struct { + Symbol string IBCDenom string `json:"ibcDenom,omitempty"` RebaseFactor sdktypes.Dec `json:"rebaseFactor,omitempty"` } diff --git a/config/default_config.go b/config/default_config.go index c091e73..75760e2 100644 --- a/config/default_config.go +++ b/config/default_config.go @@ -8,8 +8,20 @@ var DefaultPriceServerConfig = Config{ Port: 8532, MetricsPort: 8533, Sentry: "", - ProviderPriority: []string{"binance", "huobi", "coingecko", "kucoin", "bitfinex", "kraken", "okx", "osmosis", "bitstamp", "bybit", "bittrex", "exchangerate", "frankfurter", "fer"}, + ProviderPriority: []string{"astroport", "binance", "huobi", "coingecko", "kucoin", "bitfinex", "kraken", "okx", "osmosis" /*"bitstamp", */, "bybit", "bittrex", "exchangerate", "frankfurter", "fer"}, Providers: map[string]ProviderConfig{ + "astroport": { + Interval: 30, + Timeout: 5, + Symbols: []string{ + "ibc/08095CEDEA29977C9DD0CE9A48329FDA622C183359D5F90CF04CC4FF80CBE431-ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4", // stLuna - axlUSDC + "ibc/08095CEDEA29977C9DD0CE9A48329FDA622C183359D5F90CF04CC4FF80CBE431-ibc/CBF67A2BCF6CAE343FDF251E510C8E18C361FC02B23430C121116E0811835DEF", // stLuna - axlUSDT + "terra1ecgazyd0waaj3g7l9cmy5gulhxkps2gmxu9ghducvuypjq68mq2s5lvsct-ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4", // ampLuna - axlUSDC + "terra1ecgazyd0waaj3g7l9cmy5gulhxkps2gmxu9ghducvuypjq68mq2s5lvsct-ibc/CBF67A2BCF6CAE343FDF251E510C8E18C361FC02B23430C121116E0811835DEF", // ampLuna - axlUSDT + "terra17aj4ty4sz4yhgm08na8drc0v03v2jwr3waxcqrwhajj729zhl7zqnpc0ml-ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4", // backboneLuna - axlUSDC + "terra17aj4ty4sz4yhgm08na8drc0v03v2jwr3waxcqrwhajj729zhl7zqnpc0ml-ibc/CBF67A2BCF6CAE343FDF251E510C8E18C361FC02B23430C121116E0811835DEF", // backboneLuna - axlUSDT + }, + }, "binance": { Symbols: []string{ "BTCUSDT", @@ -1088,6 +1100,7 @@ var DefaultPriceServerConfig = Config{ "akash-network", "white-whale", "switcheo", + "stride-staked-luna", }, }, "osmosis": { @@ -1432,35 +1445,35 @@ var DefaultPriceServerConfig = Config{ var DefaultAllianceConfig = AllianceConfig{ GRPCUrls: []string{ "migaloo-grpc.polkachu.com:20790", - "kujira-grpc.polkachu.com:11890", + // "kujira-grpc.polkachu.com:11890", "query-grpc.carbon.network:443", }, LSTSData: []LSTData{ // Whale { // Eris Protocol ampLUNA https://chainsco.pe/terra2/address/terra1ecgazyd0waaj3g7l9cmy5gulhxkps2gmxu9ghducvuypjq68mq2s5lvsct - IBCDenom: "ibc/05238E98A143496C8AF2B6067BABC84503909ECE9E45FBCBAC2CBA5C889FD82A", - RebaseFactor: sdktypes.MustNewDecFromStr("1.178655688356438636"), + Symbol: "AMPLUNA", + IBCDenom: "ibc/05238E98A143496C8AF2B6067BABC84503909ECE9E45FBCBAC2CBA5C889FD82A", }, { // BoneLuna https://chainsco.pe/terra2/address/terra17aj4ty4sz4yhgm08na8drc0v03v2jwr3waxcqrwhajj729zhl7zqnpc0ml - IBCDenom: "ibc/40C29143BF4153B365089E40E437B7AA819672646C45BB0A5F1E10915A0B6708", - RebaseFactor: sdktypes.MustNewDecFromStr("1.066790970929921282"), + Symbol: "BACKBONELUNA", + IBCDenom: "ibc/40C29143BF4153B365089E40E437B7AA819672646C45BB0A5F1E10915A0B6708", }, // Carbon { // Eris Protocol ampLUNA https://chainsco.pe/terra2/address/terra1ecgazyd0waaj3g7l9cmy5gulhxkps2gmxu9ghducvuypjq68mq2s5lvsct - IBCDenom: "ibc/62A3870B9804FC3A92EAAA1F0F3F07E089DBF76CC521466CA33F5AAA8AD42290", - RebaseFactor: sdktypes.MustNewDecFromStr("1.178655688356438636"), + Symbol: "AMPLUNA", + IBCDenom: "ibc/62A3870B9804FC3A92EAAA1F0F3F07E089DBF76CC521466CA33F5AAA8AD42290", }, { // Stride stLuna https://app.stride.zone/ - IBCDenom: "ibc/FBEE20115530F474F8BBE1460DA85437C3FBBFAF4A5DEBD71CA6B9C40559A161", - RebaseFactor: sdktypes.MustNewDecFromStr("1.057000000000000000"), + Symbol: "STLUNA", + IBCDenom: "ibc/FBEE20115530F474F8BBE1460DA85437C3FBBFAF4A5DEBD71CA6B9C40559A161", }, }, LSTOnPhoenix: []LSTOnPhoenix{ { - CounterpartyChainId: "carbon-1", + CounterpartyChainId: "migaloo-1", LSTData: LSTData{ - IBCDenom: "ibc/random_denom", - RebaseFactor: sdktypes.MustNewDecFromStr("1.057000000000000000"), + IBCDenom: "ibc/623CD0B9778AD974713317EA0438A0CCAA72AF0BBE7BEE002205BCA25F1CA3BA", + RebaseFactor: sdktypes.OneDec(), }, }, }, diff --git a/internal/parser/internal/astroport/astroport.go b/internal/parser/internal/astroport/astroport.go new file mode 100644 index 0000000..bc850d0 --- /dev/null +++ b/internal/parser/internal/astroport/astroport.go @@ -0,0 +1,33 @@ +package astroport + +import ( + "fmt" + "strings" +) + +// symbol to base coin mapping +var ASTRO_MAPPINGS = map[string]string{ + "ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4": "USDC", // axlUSDC + "ibc/CBF67A2BCF6CAE343FDF251E510C8E18C361FC02B23430C121116E0811835DEF": "USDT", // axlUSDT + "ibc/08095CEDEA29977C9DD0CE9A48329FDA622C183359D5F90CF04CC4FF80CBE431": "STLUNA", // STLUNA + "terra1ecgazyd0waaj3g7l9cmy5gulhxkps2gmxu9ghducvuypjq68mq2s5lvsct": "AMPLUNA", // ampLuna + "terra17aj4ty4sz4yhgm08na8drc0v03v2jwr3waxcqrwhajj729zhl7zqnpc0ml": "BACKBONELUNA", // backboneLuna +} + +func ParseSymbol(symbol string) (base, quote string, err error) { + symbolSplit := strings.Split(symbol, "-") + base_symbol := symbolSplit[0] + quote_symbol := symbolSplit[1] + + base, ok := ASTRO_MAPPINGS[base_symbol] + if !ok { + return base, quote, fmt.Errorf("failed to parse 'base_symbol' from 'ASTRO_MAPPINGS' %s", base_symbol) + } + + quote, ok = ASTRO_MAPPINGS[quote_symbol] + if !ok { + return base, quote, fmt.Errorf("failed to parse 'quote_symbol' from 'ASTRO_MAPPINGS' %s", quote_symbol) + } + + return base, quote, nil +} diff --git a/internal/parser/internal/coingecko/coingecko.go b/internal/parser/internal/coingecko/coingecko.go index d92a9d9..8fd9d56 100644 --- a/internal/parser/internal/coingecko/coingecko.go +++ b/internal/parser/internal/coingecko/coingecko.go @@ -106,8 +106,9 @@ var COIN_GECKO_MAPPING = map[string]string{ "juno-network": "JUNO", "stargaze": "STARS", "akash-network": "AKT", - "white-whale": "WHALE", // White Whale chain - "switcheo": "SWTH", // Carbon chain + "white-whale": "WHALE", // White Whale chain + "switcheo": "SWTH", // Carbon chain + "stride-staked-luna": "STLUNA", // Stride chain } func ParseSymbol(symbol string) (string, string, error) { diff --git a/internal/parser/symbol_parser.go b/internal/parser/symbol_parser.go index d69d9f9..3e6ea61 100644 --- a/internal/parser/symbol_parser.go +++ b/internal/parser/symbol_parser.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/terra-money/oracle-feeder-go/internal/parser/internal/astroport" "github.com/terra-money/oracle-feeder-go/internal/parser/internal/binance" "github.com/terra-money/oracle-feeder-go/internal/parser/internal/bitfinex" "github.com/terra-money/oracle-feeder-go/internal/parser/internal/bitstamp" @@ -21,6 +22,8 @@ import ( // can be parsed to the same pair `BTC/USDT`. func ParseSymbol(exhcange string, symbol string) (string, string, error) { switch strings.ToLower(exhcange) { + case "astroport": + return astroport.ParseSymbol(symbol) case "binance": return binance.ParseSymbol(symbol) case "bitfinex": diff --git a/internal/provider/alliance_provider.go b/internal/provider/alliance/alliance_protocols_info.go similarity index 79% rename from internal/provider/alliance_provider.go rename to internal/provider/alliance/alliance_protocols_info.go index c050c24..1bab184 100644 --- a/internal/provider/alliance_provider.go +++ b/internal/provider/alliance/alliance_protocols_info.go @@ -1,4 +1,4 @@ -package provider +package alliance_provider import ( "context" @@ -8,53 +8,37 @@ import ( alliancetypes "github.com/terra-money/alliance/x/alliance/types" "github.com/terra-money/oracle-feeder-go/config" + "github.com/terra-money/oracle-feeder-go/internal/provider/internal" types "github.com/terra-money/oracle-feeder-go/internal/types" + pkgtypes "github.com/terra-money/oracle-feeder-go/pkg/types" pricetypes "github.com/terra-money/oracle-feeder-go/pkg/types" - "google.golang.org/grpc" - - "crypto/tls" - - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/credentials/insecure" "github.com/cosmos/cosmos-sdk/client/grpc/tmservice" - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/codec/types" sdktypes "github.com/cosmos/cosmos-sdk/types" mintypes "github.com/cosmos/cosmos-sdk/x/mint/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/terra-money/oracle-feeder-go/internal/provider" ) -type allianceProvider struct { +type allianceProtocolsInfo struct { + internal.BaseGrpc + provider.LSDProvider config *config.AllianceConfig - providerManager *ProviderManager + providerManager *provider.ProviderManager } -func NewAllianceProvider(config *config.AllianceConfig, providerManager *ProviderManager) *allianceProvider { +func NewAllianceProtocolsInfo(config *config.AllianceConfig, providerManager *provider.ProviderManager) *allianceProtocolsInfo { - return &allianceProvider{ + return &allianceProtocolsInfo{ + BaseGrpc: *internal.NewBaseGrpc(), + LSDProvider: *provider.NewLSDProvider(), config: config, providerManager: providerManager, } } -func (p *allianceProvider) getRPCConnection(nodeUrl string, interfaceRegistry sdk.InterfaceRegistry) (*grpc.ClientConn, error) { - var authCredentials = grpc.WithTransportCredentials(insecure.NewCredentials()) - - if strings.Contains(nodeUrl, "carbon") { - authCredentials = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})) - } - - return grpc.Dial( - nodeUrl, - authCredentials, - grpc.WithDefaultCallOptions( - grpc.ForceCodec(codec.NewProtoCodec(interfaceRegistry).GRPCCodec()), - grpc.MaxCallRecvMsgSize(1024*1024*16), // 16MB - )) -} - -func (p *allianceProvider) GetProtocolsInfo(ctx context.Context) (*types.AllianceProtocolRes, error) { +func (p *allianceProtocolsInfo) GetProtocolsInfo(ctx context.Context) (*pkgtypes.MsgUpdateChainsInfo, error) { protocolRes := types.DefaultAllianceProtocolRes() // Query the all prices at the beginning @@ -62,6 +46,15 @@ func (p *allianceProvider) GetProtocolsInfo(ctx context.Context) (*types.Allianc // the prices each time we query the protocols info. pricesRes := p.providerManager.GetPrices(ctx) + // Given the default list of LSTSData this method + // queries the blockchain for the rebase factor of the LSD. + lstsData, err := p.queryRebaseFactors(p.config.LSTSData) + if err != nil { + fmt.Printf("queryRebaseFactors: %v \n", err) + return nil, err + } + + // Setup Luna price for _, price := range pricesRes.Prices { if strings.EqualFold(price.Denom, "LUNA") { luna, err := sdktypes.NewDecFromStr(strconv.FormatFloat(price.Price, 'f', -1, 64)) @@ -76,7 +69,7 @@ func (p *allianceProvider) GetProtocolsInfo(ctx context.Context) (*types.Allianc // Iterate over all configured nodes in the config file, // create a grpcConnection to each node and query the required data. for _, grpcUrl := range p.config.GRPCUrls { - grpcConn, err := p.getRPCConnection(grpcUrl, nil) + grpcConn, err := p.BaseGrpc.Connection(grpcUrl, nil) if err != nil { return nil, err } @@ -151,7 +144,7 @@ func (p *allianceProvider) GetProtocolsInfo(ctx context.Context) (*types.Allianc annualProvisionsRes.AnnualProvisions, ) - normalizedLunaAlliance := parseLunaAlliances(allianceParamsRes.Params, allianceRes.Alliances, p.config.LSTSData) + normalizedLunaAlliance := p.parseLunaAlliances(allianceParamsRes.Params, allianceRes.Alliances, lstsData) alliancesOnPhoenix := p.filterAlliancesOnPhoenix(nodeRes) protocolRes.ProtocolsInfo = append(protocolRes.ProtocolsInfo, types.NewProtocolInfo( @@ -161,11 +154,25 @@ func (p *allianceProvider) GetProtocolsInfo(ctx context.Context) (*types.Allianc alliancesOnPhoenix, )) } + res := pkgtypes.NewMsgUpdateChainsInfo(protocolRes) + + return &res, nil +} +func (p *allianceProtocolsInfo) queryRebaseFactors(configLST []config.LSTData) ([]config.LSTData, error) { + for i, lst := range configLST { + rebaseFactor, err := p.LSDProvider.QueryLSTRebaseFactor(lst.Symbol) + if err != nil { + fmt.Printf("queryRebaseFactors: %v \n", err) + continue + } + configLST[i].RebaseFactor = *rebaseFactor + } + + return configLST, nil - return &protocolRes, nil } -func (p *allianceProvider) filterAlliancesOnPhoenix(nodeRes *tmservice.GetNodeInfoResponse) []types.BaseAlliance { +func (p *allianceProtocolsInfo) filterAlliancesOnPhoenix(nodeRes *tmservice.GetNodeInfoResponse) []types.BaseAlliance { baseAlliances := []types.BaseAlliance{} for _, allianceOnPhoenix := range p.config.LSTOnPhoenix { @@ -179,7 +186,7 @@ func (p *allianceProvider) filterAlliancesOnPhoenix(nodeRes *tmservice.GetNodeIn return baseAlliances } -func parseLunaAlliances( +func (p *allianceProtocolsInfo) parseLunaAlliances( allianceParams alliancetypes.Params, alliances []alliancetypes.AllianceAsset, lstsData []config.LSTData, diff --git a/internal/provider/alliance/alliance_provider.go b/internal/provider/alliance/alliance_provider.go new file mode 100644 index 0000000..cc499ef --- /dev/null +++ b/internal/provider/alliance/alliance_provider.go @@ -0,0 +1,31 @@ +package alliance_provider + +import ( + "context" + + "github.com/terra-money/oracle-feeder-go/config" + "github.com/terra-money/oracle-feeder-go/pkg/types" + + "github.com/terra-money/oracle-feeder-go/internal/provider" +) + +type allianceProvider struct { + allianceProtocolsInfo *allianceProtocolsInfo + allianceValidatorsProvider *allianceValidatorsProvider +} + +func NewAllianceProvider(config *config.AllianceConfig, providerManager *provider.ProviderManager) *allianceProvider { + + return &allianceProvider{ + allianceProtocolsInfo: NewAllianceProtocolsInfo(config, providerManager), + allianceValidatorsProvider: NewAllianceValidatorsProvider(config, providerManager), + } +} + +func (p *allianceProvider) GetProtocolsInfo(ctx context.Context) (*types.MsgUpdateChainsInfo, error) { + return p.allianceProtocolsInfo.GetProtocolsInfo(ctx) +} + +func (p *allianceProvider) GetAllianceRedelegateReq(ctx context.Context) (*types.MsgAllianceRedelegate, error) { + return p.allianceValidatorsProvider.GetAllianceRedelegateReq(ctx) +} diff --git a/internal/provider/alliance_querier.go b/internal/provider/alliance/alliance_querier.go similarity index 50% rename from internal/provider/alliance_querier.go rename to internal/provider/alliance/alliance_querier.go index f0fabaa..99d40b8 100644 --- a/internal/provider/alliance_querier.go +++ b/internal/provider/alliance/alliance_querier.go @@ -1,52 +1,49 @@ -package provider +package alliance_provider import ( "context" - "encoding/json" "fmt" "io" "net/http" "os" - sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/terra-money/oracle-feeder-go/internal/provider" types "github.com/terra-money/oracle-feeder-go/internal/types" ) type alliancesQuerierProvider struct { - transactionsProvider TransactionsProvider + feederType types.FeederType + transactionsProvider provider.TransactionsProvider } -func NewAlliancesQuerierProvider() *alliancesQuerierProvider { +func NewAlliancesQuerierProvider(feederType types.FeederType) *alliancesQuerierProvider { return &alliancesQuerierProvider{ - transactionsProvider: NewTransactionsProvider(), + feederType: feederType, + transactionsProvider: provider.NewTransactionsProvider(feederType), } } -func (a alliancesQuerierProvider) QueryAndSubmitOnChain(ctx context.Context) (res *types.AllianceProtocolRes, err error) { - res, err = a.RequestData() +func (a alliancesQuerierProvider) QueryAndSubmitOnChain(ctx context.Context) (res []byte, err error) { + res, err = a.requestData() if err != nil { return nil, fmt.Errorf("ERROR requesting alliances data %w", err) } - msg, err := a.transactionsProvider.ParseAlliancesTransaction(res) - if err != nil { - return nil, fmt.Errorf("ERROR parsing alliances data %w", err) - } - txHash, err := a.transactionsProvider.SubmitAlliancesTransaction(ctx, []sdk.Msg{msg}) + txHash, err := a.transactionsProvider.SubmitAlliancesTransaction(ctx, res) if err != nil { return nil, fmt.Errorf("ERROR submitting alliances data on chain %w", err) } - fmt.Printf("Transaction Submitted successfully txHash: %d \n", txHash) + fmt.Printf("Transaction Submitted successfully txHash: %s \n", txHash) return res, nil } -func (alliancesQuerierProvider) RequestData() (res *types.AllianceProtocolRes, err error) { +func (a alliancesQuerierProvider) requestData() (res []byte, err error) { var url string if url = os.Getenv("PRICE_SERVER_URL"); len(url) == 0 { url = "http://localhost:8532" } // Send GET request - resp, err := http.Get(url + "/alliance/protocol") + resp, err := http.Get(url + types.FromFeederTypeToPriceServerUrl(a.feederType)) if err != nil { return nil, err } @@ -58,12 +55,6 @@ func (alliancesQuerierProvider) RequestData() (res *types.AllianceProtocolRes, e return nil, err } - // Parse JSON response into struct - err = json.Unmarshal(body, &res) - if err != nil { - return nil, err - } - // Access parsed data - return res, nil + return body, nil } diff --git a/internal/provider/alliance/alliance_validators.go b/internal/provider/alliance/alliance_validators.go new file mode 100644 index 0000000..97011db --- /dev/null +++ b/internal/provider/alliance/alliance_validators.go @@ -0,0 +1,451 @@ +package alliance_provider + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + + "github.com/terra-money/oracle-feeder-go/config" + "github.com/terra-money/oracle-feeder-go/internal/provider" + "github.com/terra-money/oracle-feeder-go/internal/provider/internal" + types "github.com/terra-money/oracle-feeder-go/internal/types" + pkgtypes "github.com/terra-money/oracle-feeder-go/pkg/types" + + "github.com/cosmos/cosmos-sdk/client/grpc/tmservice" + sdktypes "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" + + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + alliancetypes "github.com/terra-money/alliance/x/alliance/types" +) + +type allianceValidatorsProvider struct { + internal.BaseGrpc + nodeGrpcUrl string + stationApiUrl string + allianceHubContractAddress string + config *config.AllianceConfig + providerManager *provider.ProviderManager +} + +func NewAllianceValidatorsProvider(config *config.AllianceConfig, providerManager *provider.ProviderManager) *allianceValidatorsProvider { + var nodeGrpcUrl string + if nodeGrpcUrl = os.Getenv("NODE_GRPC_URL"); len(nodeGrpcUrl) == 0 { + panic("NODE_GRPC_URL env variable is not set!") + } + + var stationApiUrl string + if stationApiUrl = os.Getenv("STATION_API"); len(stationApiUrl) == 0 { + panic("STATION_API env variable is not set!") + } + + var allianceHubContractAddress string + if allianceHubContractAddress = os.Getenv("ALLIANCE_HUB_CONTRACT_ADDRESS"); len(allianceHubContractAddress) == 0 { + panic("ALLIANCE_HUB_CONTRACT_ADDRESS env variable is not set!") + } + + return &allianceValidatorsProvider{ + BaseGrpc: *internal.NewBaseGrpc(), + config: config, + nodeGrpcUrl: nodeGrpcUrl, + stationApiUrl: stationApiUrl, + providerManager: providerManager, + allianceHubContractAddress: allianceHubContractAddress, + } +} + +// Query Terra GRPC, station API and return the list of alliance +// redelegations that can be submitted to Alliance Hub applying +// the following rules: +// (1) - be part of the active validator set, +// (2) - to do not be jailed, +// (3) - commission rate to be lower than 19%, +// (4) - Participate in the latest 3 gov proposals, +// (5) - have been in the active validator set 100 000 blocks before the current one, (1 week approx) +func (p *allianceValidatorsProvider) GetAllianceRedelegateReq(ctx context.Context) (*pkgtypes.MsgAllianceRedelegate, error) { + smartContractRes, err := p.querySmartContractConfig(ctx) + if err != nil { + return nil, err + } + + stakingValidators, seniorValidators, proposalsVotes, allianceVals, err := p.queryValidatorsData(ctx) + if err != nil { + return nil, err + } + + var compliantVals []stakingtypes.Validator + // Apply the previous rules to filter the list + // of all blockchain validators to keep the ones + // that are compliant + for _, val := range stakingValidators { + // (1) skip if status is not bonded (again in case the api have a bug with the query) + if val.GetStatus() != stakingtypes.Bonded { + continue + } + + // (2) skip if jailed + if val.IsJailed() { + continue + } + + // (3) skip if commission is grater than 19% + if val.Commission.CommissionRates.Rate.GT(sdktypes.MustNewDecFromStr("0.19")) { + continue + } + + // (4) skip if have not voted in the last 3 proposals + if !atLeastThreeOccurrences(proposalsVotes, val.OperatorAddress) { + continue + } + + // (5) skip if it have not been in the active validator set 100 000 blocks before the current one + for _, seniorValidator := range seniorValidators { + if val.OperatorAddress != seniorValidator.Address { + continue + } + } + + compliantVals = append(compliantVals, val) + } + + valsWithAllianceTokens, totalAllianceStakedTokens := FilterAllianceValsWithStake(allianceVals, smartContractRes.AllianceTokenDenom) + compliantValsWithAllianceTokens, + nonCompliantValsWithAllianceTokens := ParseAllianceValsByCompliance(compliantVals, valsWithAllianceTokens, smartContractRes.AllianceTokenDenom) + avgTokensPerCompliantVal := totalAllianceStakedTokens.Quo(sdktypes.NewDec(int64(len(compliantVals)))) + + redelegations := RebalanceVals( + compliantValsWithAllianceTokens, + nonCompliantValsWithAllianceTokens, + avgTokensPerCompliantVal, + ) + + res := pkgtypes.NewMsgAllianceRedelegate(redelegations) + + return &res, nil +} + +// In charge of rebalancing the stake from non-compliant validators to compliant ones. +// - Non-compliant validators should end with 0 stake at the end of function execution. +// - Compliant validators shouldn't have more than the average amout of stake (avgTokensPerComplVal). +// - If any compliant validator has more than the average amout of stake, re-balance to other compliant validators. +func RebalanceVals( + compVal []types.ValWithAllianceTokensStake, + nonCompVals []types.ValWithAllianceTokensStake, + avgTokensPerComplVal sdktypes.Dec, +) []types.Redelegation { + redelegations := []types.Redelegation{} + + // Redelegate the non-compliant validators stake + // until they have 0 stake. + for i := 0; i < len(nonCompVals); i++ { + + for j := 0; j < len(compVal); j++ { + nonCompValStake := nonCompVals[i].TotalStaked.Amount + if nonCompValStake.IsZero() { + break + } + compValStake := compVal[j].TotalStaked.Amount + if compValStake.LT(avgTokensPerComplVal) { + // ... calculate the delta to the average + deltaStakeToRebalance := avgTokensPerComplVal.Sub(compValStake) + + // If the delta is greater than the stake of the non-compliant validator + // use all the stake of the non-compliant validator + if deltaStakeToRebalance.GT(nonCompValStake) { + deltaStakeToRebalance = nonCompValStake + } + + // Append the redelegation to the list + redelegations = append( + redelegations, + types.NewRedelegation( + nonCompVals[i].ValidatorAddr, + compVal[j].ValidatorAddr, + // Since the operations are done with Decimals, we need to remove the decimal part https://github.com/terra-money/alliance/issues/227 + strings.Split(deltaStakeToRebalance.String(), ".")[0], + ), + ) + + // Update the stake of the compliant validator + compVal[j].TotalStaked.Amount = compValStake.Add(deltaStakeToRebalance) + // Update the stake of the non-compliant validator + nonCompVals[i].TotalStaked.Amount = nonCompValStake.Sub(deltaStakeToRebalance) + } + } + } + + // Redelegate the compliant validators stake + // if any has more than the average stake. + for i := 0; i < len(compVal); i++ { + for j := 0; j < len(compVal); j++ { + IcompValStake := compVal[i].TotalStaked.Amount + // break if the src validator has less than or equal to the average stake + if IcompValStake.LTE(avgTokensPerComplVal) { + break + } + JcompValStake := compVal[j].TotalStaked.Amount + if JcompValStake.LT(avgTokensPerComplVal) { + // ... and calculate the delta to the average + diffNeededByDstVal := avgTokensPerComplVal.Sub(JcompValStake) + diffAvailableBySrcVal := IcompValStake.Sub(avgTokensPerComplVal) + + // Take the minimum between the two + var deltaStakeToRebalance sdktypes.Dec + if diffNeededByDstVal.GT(diffAvailableBySrcVal) { + deltaStakeToRebalance = diffAvailableBySrcVal + } else { + deltaStakeToRebalance = diffNeededByDstVal + } + + // Append the redelegation to the list + redelegations = append( + redelegations, + types.NewRedelegation( + compVal[i].ValidatorAddr, + compVal[j].ValidatorAddr, + // Since the operations are done with Decimals, we need to remove the decimal part https://github.com/terra-money/alliance/issues/227 + strings.Split(deltaStakeToRebalance.String(), ".")[0], + ), + ) + + // Update the stake of the validator with more stake than the average + compVal[j].TotalStaked.Amount = JcompValStake.Add(deltaStakeToRebalance) + // Update the stake of the validator with less stake than the average + compVal[i].TotalStaked.Amount = IcompValStake.Sub(deltaStakeToRebalance) + } + } + } + + return redelegations +} + +// Method to split the list of alliance validators in two subsets: +// +// - compliantValsWithAllianceTokens: validatos that comply with the rules +// described below the method GetAllianceRedelegateReq with the stake to zero if has no stake. +// +// - nonCompliantValsWithAllianceTokens: the ones that does not complie with the rules +// in this subset should never exist validators with zero stake, +func ParseAllianceValsByCompliance( + compliantVals []stakingtypes.Validator, + valsWithAllianceTokens []types.ValWithAllianceTokensStake, + allianceTokenDenom string, +) ([]types.ValWithAllianceTokensStake, []types.ValWithAllianceTokensStake) { + compliantValsWithAllianceTokens := []types.ValWithAllianceTokensStake{} + nonCompliantValsWithAllianceTokens := []types.ValWithAllianceTokensStake{} + + // Parse the **compliantVals** to the type **ValWithAllianceTokensStake** + // if they have no stake initialize the stake to 0. + for _, val := range compliantVals { + compliantValsWithAllianceTokens = append( + compliantValsWithAllianceTokens, + types.ValWithAllianceTokensStake{ + ValidatorAddr: val.OperatorAddress, + TotalStaked: sdktypes.NewDecCoinFromDec(allianceTokenDenom, sdktypes.ZeroDec()), + }, + ) + } + + // Update the stake of the compliant validators since it was initialized to 0 + // and populate the list of the non-compliant validators. + for _, valWithAllianceTokensStake := range valsWithAllianceTokens { + found := false + for i := 0; i < len(compliantValsWithAllianceTokens); i++ { + if compliantValsWithAllianceTokens[i].ValidatorAddr == valWithAllianceTokensStake.ValidatorAddr { + compliantValsWithAllianceTokens[i].TotalStaked = valWithAllianceTokensStake.TotalStaked + found = true + continue + } + } + + if !found { + nonCompliantValsWithAllianceTokens = append( + nonCompliantValsWithAllianceTokens, + valWithAllianceTokensStake, + ) + } + } + + return compliantValsWithAllianceTokens, nonCompliantValsWithAllianceTokens +} + +// Filter the alliance validators to keep only the ones that have staked ualliance tokens +func FilterAllianceValsWithStake(allianceVals []alliancetypes.QueryAllianceValidatorResponse, allianceTokenDenom string) ([]types.ValWithAllianceTokensStake, sdktypes.Dec) { + valsWithAllianceTokens := []types.ValWithAllianceTokensStake{} + uallianceStakedTokens := sdktypes.ZeroDec() + + for _, val := range allianceVals { + for _, stake := range val.TotalStaked { + // As soon as we find the first entry with the alliance token denom + // we can append the validator to the list and break the loop + // because it represents all the ualliance tokens staked to that validator + if stake.Denom == allianceTokenDenom { + valsWithAllianceTokens = append( + valsWithAllianceTokens, + types.NewValWithAllianceTokensStake(val.ValidatorAddr, stake), + ) + + uallianceStakedTokens = uallianceStakedTokens.Add(stake.Amount) + continue + } + } + } + return valsWithAllianceTokens, uallianceStakedTokens +} + +func (p *allianceValidatorsProvider) querySmartContractConfig(ctx context.Context) (*types.AllianceHubConfigData, error) { + grpcConn, err := p.BaseGrpc.Connection(p.nodeGrpcUrl, nil) + if err != nil { + fmt.Printf("grpcConn: %v \n", err) + return nil, err + } + defer grpcConn.Close() + client := wasmtypes.NewQueryClient(grpcConn) + + res, err := client.SmartContractState(ctx, &wasmtypes.QuerySmartContractStateRequest{ + Address: p.allianceHubContractAddress, + QueryData: []byte(`{ "config" : {}}`), + }) + if err != nil { + return nil, err + } + + var configRes types.AllianceHubConfigData + err = json.Unmarshal(res.Data, &configRes) + if err != nil { + return nil, err + } + + return &configRes, nil +} + +func (p *allianceValidatorsProvider) queryValidatorsData(ctx context.Context) ( + []stakingtypes.Validator, + []*tmservice.Validator, + []types.StationVote, + []alliancetypes.QueryAllianceValidatorResponse, + error, +) { + grpcConn, err := p.BaseGrpc.Connection(p.nodeGrpcUrl, nil) + if err != nil { + fmt.Printf("grpcConn: %v \n", err) + return nil, nil, nil, nil, err + } + defer grpcConn.Close() + + nodeClient := tmservice.NewServiceClient(grpcConn) + govClient := govtypes.NewQueryClient(grpcConn) + stakingClient := stakingtypes.NewQueryClient(grpcConn) + allianceClient := alliancetypes.NewQueryClient(grpcConn) + + valsRes, err := stakingClient.Validators(ctx, &stakingtypes.QueryValidatorsRequest{ + Status: stakingtypes.BondStatusBonded, // (1) query only status bonded + Pagination: &query.PageRequest{ + Limit: 150, + }, + }) + if err != nil { + fmt.Printf("valsRes: %v \n", err) + return nil, nil, nil, nil, err + } + + govPropsRes, err := govClient.Proposals(ctx, &govtypes.QueryProposalsRequest{ + Pagination: &query.PageRequest{ + Limit: 3, + Reverse: true, + }, + }) + if err != nil { + fmt.Printf("govPropsRes: %v \n", err) + return nil, nil, nil, nil, err + } + + latestHeightRes, err := nodeClient.GetLatestBlock(ctx, &tmservice.GetLatestBlockRequest{}) + if err != nil { + fmt.Printf("latestHeightRes: %v \n", err) + return nil, nil, nil, nil, err + } + + seniorValidatorsRes, err := nodeClient.GetValidatorSetByHeight(ctx, &tmservice.GetValidatorSetByHeightRequest{ + Height: latestHeightRes.SdkBlock.Header.Height - 100_000, + }) + if err != nil { + fmt.Printf("seniorValidatorsRes: %v \n", err) + return nil, nil, nil, nil, err + } + + proposalsVotesRes, err := p.getProposalsVotesFromStationAPI(ctx, govPropsRes.Proposals) + if err != nil { + fmt.Printf("proposalsVotesRes: %v \n", err) + return nil, nil, nil, nil, err + } + + allianceVals, err := allianceClient.AllAllianceValidators(ctx, &alliancetypes.QueryAllAllianceValidatorsRequest{ + Pagination: &query.PageRequest{ + Limit: 150, + }, + }) + if err != nil { + fmt.Printf("allianceVals: %v \n", err) + return nil, nil, nil, nil, err + } + + return valsRes.Validators, + seniorValidatorsRes.Validators, + proposalsVotesRes, + allianceVals.Validators, + nil +} + +func (p *allianceValidatorsProvider) getProposalsVotesFromStationAPI(ctx context.Context, proposals []*govtypes.Proposal) (stationProposals []types.StationVote, err error) { + for _, proposal := range proposals { + stationProposalsRes, err := p.queryStation(proposal.Id) + if err != nil { + return stationProposals, err + } + stationProposals = append(stationProposals, *stationProposalsRes...) + } + + return stationProposals, err +} + +func (p allianceValidatorsProvider) queryStation(propId uint64) (res *[]types.StationVote, err error) { + url := p.stationApiUrl + "/proposals/" + fmt.Sprint(propId) + // Send GET request + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Read response body + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // Parse JSON response into struct + err = json.Unmarshal(body, &res) + if err != nil { + return nil, err + } + + // Access parsed data + return res, nil +} + +func atLeastThreeOccurrences(stationVotes []types.StationVote, val string) bool { + count := 0 + for _, v := range stationVotes { + if v.Voter == val { + count++ + } + } + return count >= 3 +} diff --git a/internal/provider/alliance/alliance_validators_test.go b/internal/provider/alliance/alliance_validators_test.go new file mode 100644 index 0000000..569ea48 --- /dev/null +++ b/internal/provider/alliance/alliance_validators_test.go @@ -0,0 +1,284 @@ +package alliance_provider_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/require" + alliancetypes "github.com/terra-money/alliance/x/alliance/types" + alliance_provider "github.com/terra-money/oracle-feeder-go/internal/provider/alliance" + types "github.com/terra-money/oracle-feeder-go/internal/types" +) + +func TestRebalanceOneVal(t *testing.T) { + // GIVEN + compVal := []types.ValWithAllianceTokensStake{ + {ValidatorAddr: "val1", TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(10))}, + {ValidatorAddr: "val2", TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(15))}, + } + nonCompVals := []types.ValWithAllianceTokensStake{ + {ValidatorAddr: "val3", TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(5))}, + } + avgTokensPerCompVal := sdk.NewDec(15) + + // WHEN + redelegations := alliance_provider.RebalanceVals(compVal, nonCompVals, avgTokensPerCompVal) + + // THEN + require.Equal(t, 1, len(redelegations)) + require.Equal(t, types.Redelegation{ + SrcValidator: "val3", + DstValidator: "val1", + Amount: "5", + }, redelegations[0]) +} + +func TestRebalanceMultipleVal(t *testing.T) { + // GIVEN + compVal := []types.ValWithAllianceTokensStake{ + {ValidatorAddr: "val1", TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(10))}, + {ValidatorAddr: "val2", TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(2))}, + {ValidatorAddr: "val3", TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(4))}, + } + nonCompVals := []types.ValWithAllianceTokensStake{ + {ValidatorAddr: "val4", TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(5))}, + {ValidatorAddr: "val5", TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(5))}, + {ValidatorAddr: "val6", TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(4))}, + } + avgTokensPerCompVal := sdk.NewDec(10) + + // WHEN + redelegations := alliance_provider.RebalanceVals(compVal, nonCompVals, avgTokensPerCompVal) + + // THEN + require.Equal(t, 4, len(redelegations)) + require.Equal(t, types.Redelegation{ + SrcValidator: "val4", + DstValidator: "val2", + Amount: "5", + }, redelegations[0]) + require.Equal(t, types.Redelegation{ + SrcValidator: "val5", + DstValidator: "val2", + Amount: "3", + }, redelegations[1]) + require.Equal(t, types.Redelegation{ + SrcValidator: "val5", + DstValidator: "val3", + Amount: "2", + }, redelegations[2]) + require.Equal(t, types.Redelegation{ + SrcValidator: "val6", + DstValidator: "val3", + Amount: "4", + }, redelegations[3]) +} + +func TestRebalanceMultipleVal2(t *testing.T) { + // GIVEN + compVal := []types.ValWithAllianceTokensStake{ + {ValidatorAddr: "val1", TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(40))}, + {ValidatorAddr: "val2", TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(2))}, + {ValidatorAddr: "val3", TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(4))}, + } + nonCompVals := []types.ValWithAllianceTokensStake{ + {ValidatorAddr: "val4", TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(5))}, + {ValidatorAddr: "val5", TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(5))}, + {ValidatorAddr: "val6", TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(4))}, + } + avgTokensPerCompVal := sdk.NewDec(20) + + // WHEN + redelegations := alliance_provider.RebalanceVals(compVal, nonCompVals, avgTokensPerCompVal) + + // THEN + require.Equal(t, 5, len(redelegations)) + require.Equal(t, types.Redelegation{ + SrcValidator: "val4", + DstValidator: "val2", + Amount: "5", + }, redelegations[0]) + require.Equal(t, types.Redelegation{ + SrcValidator: "val5", + DstValidator: "val2", + Amount: "5", + }, redelegations[1]) + require.Equal(t, types.Redelegation{ + SrcValidator: "val6", + DstValidator: "val2", + Amount: "4", + }, redelegations[2]) + require.Equal(t, types.Redelegation{ + SrcValidator: "val1", + DstValidator: "val2", + Amount: "4", + }, redelegations[3]) + require.Equal(t, types.Redelegation{ + SrcValidator: "val1", + DstValidator: "val3", + Amount: "16", + }, redelegations[4]) +} +func TestFilterAllianceValsWithStake(t *testing.T) { + // GIVEN + allianceVals := []alliancetypes.QueryAllianceValidatorResponse{ + { + ValidatorAddr: "val1", + TotalStaked: []sdk.DecCoin{ + {Denom: "token1", Amount: sdk.NewDec(100)}, + }, + }, + { + ValidatorAddr: "val2", + TotalStaked: []sdk.DecCoin{ + {Denom: "token1", Amount: sdk.NewDec(300)}, + {Denom: "token2", Amount: sdk.NewDec(300)}, + }, + }, + { + ValidatorAddr: "val3", + TotalStaked: []sdk.DecCoin{ + {Denom: "token2", Amount: sdk.NewDec(300)}, + }, + }, + } + allianceTokenDenom := "token1" + + // WHEN + valsWithAllianceTokens, uallianceStakedTokens := alliance_provider.FilterAllianceValsWithStake(allianceVals, allianceTokenDenom) + + // THEN + require.Equal(t, sdk.NewDec(400), uallianceStakedTokens) + require.Equal(t, []types.ValWithAllianceTokensStake{ + { + ValidatorAddr: "val1", + TotalStaked: sdk.DecCoin{Denom: "token1", Amount: sdk.NewDec(100)}, + }, + { + ValidatorAddr: "val2", + TotalStaked: sdk.DecCoin{Denom: "token1", Amount: sdk.NewDec(300)}, + }, + }, valsWithAllianceTokens) +} + +func TestParseAllianceValsByCompliance(t *testing.T) { + // GIVEN + compliantVals := []stakingtypes.Validator{ + { + OperatorAddress: "val1", + }, + { + OperatorAddress: "val2", + }, + { + OperatorAddress: "val3", + }, + } + valsWithAllianceTokens := []types.ValWithAllianceTokensStake{ + { + ValidatorAddr: "val1", + TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(100)), + }, + { + ValidatorAddr: "val2", + TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(200)), + }, + { + ValidatorAddr: "val4", + TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(200)), + }, + } + allianceTokenDenom := "token" + + // WHEN + compliant, nonCompliant := alliance_provider.ParseAllianceValsByCompliance(compliantVals, valsWithAllianceTokens, allianceTokenDenom) + + // THEN + require.Equal(t, []types.ValWithAllianceTokensStake{ + { + ValidatorAddr: "val1", + TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(100)), + }, + { + ValidatorAddr: "val2", + TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(200)), + }, + { + ValidatorAddr: "val3", + TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(0)), + }, + }, compliant) + require.Equal(t, []types.ValWithAllianceTokensStake{ + + { + ValidatorAddr: "val4", + TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(200)), + }, + }, nonCompliant) +} + +func TestParseAllianceValsByComplianceWithNoStake(t *testing.T) { + // GIVEN + compliantVals := []stakingtypes.Validator{ + { + OperatorAddress: "val1", + }, + { + OperatorAddress: "val2", + }, + { + OperatorAddress: "val3", + }, + } + valsWithAllianceTokens := []types.ValWithAllianceTokensStake{} + allianceTokenDenom := "token" + + // WHEN + compliant, nonCompliant := alliance_provider.ParseAllianceValsByCompliance(compliantVals, valsWithAllianceTokens, allianceTokenDenom) + + // THEN + require.Equal(t, []types.ValWithAllianceTokensStake{ + { + ValidatorAddr: "val1", + TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(0)), + }, + { + ValidatorAddr: "val2", + TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(0)), + }, + { + ValidatorAddr: "val3", + TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(0)), + }, + }, compliant) + require.Equal(t, []types.ValWithAllianceTokensStake{}, nonCompliant) + +} + +func TestParseAllianceValsWithNoneCompliant(t *testing.T) { + // GIVEN + compliantVals := []stakingtypes.Validator{} + valsWithAllianceTokens := []types.ValWithAllianceTokensStake{ + { + ValidatorAddr: "val1", + TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(100)), + }, + { + ValidatorAddr: "val2", + TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(200)), + }, + { + ValidatorAddr: "val3", + TotalStaked: sdk.NewDecCoinFromDec("token", sdk.NewDec(0)), + }} + allianceTokenDenom := "token" + + // WHEN + compliant, nonCompliant := alliance_provider.ParseAllianceValsByCompliance(compliantVals, valsWithAllianceTokens, allianceTokenDenom) + + // THEN + require.Equal(t, 0, len(compliant)) + require.Equal(t, 3, len(nonCompliant)) + +} diff --git a/internal/provider/internal/base_grpc.go b/internal/provider/internal/base_grpc.go new file mode 100644 index 0000000..876b1a5 --- /dev/null +++ b/internal/provider/internal/base_grpc.go @@ -0,0 +1,36 @@ +package internal + +import ( + "crypto/tls" + "strings" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/codec/types" +) + +type BaseGrpc struct { +} + +func NewBaseGrpc() *BaseGrpc { + return &BaseGrpc{} +} + +func (p *BaseGrpc) Connection(nodeUrl string, interfaceRegistry sdk.InterfaceRegistry) (*grpc.ClientConn, error) { + var authCredentials = grpc.WithTransportCredentials(insecure.NewCredentials()) + + if strings.Contains(nodeUrl, "carbon") { + authCredentials = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})) + } + + return grpc.Dial( + nodeUrl, + authCredentials, + grpc.WithDefaultCallOptions( + grpc.ForceCodec(codec.NewProtoCodec(interfaceRegistry).GRPCCodec()), + grpc.MaxCallRecvMsgSize(1024*1024*16), // 16MB + )) +} diff --git a/internal/provider/lsd_provider.go b/internal/provider/lsd_provider.go new file mode 100644 index 0000000..f1949c2 --- /dev/null +++ b/internal/provider/lsd_provider.go @@ -0,0 +1,120 @@ +package provider + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + "github.com/terra-money/oracle-feeder-go/internal/provider/internal" + "github.com/terra-money/oracle-feeder-go/internal/types" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type LSDProvider struct { + internal.BaseGrpc + phoenixNodeUrl string + striddeApiUrl string + erisStakingHubContractAddress string + boneLunaHubContractAddress string +} + +func NewLSDProvider() *LSDProvider { + return &LSDProvider{ + BaseGrpc: *internal.NewBaseGrpc(), + phoenixNodeUrl: "terra-grpc.polkachu.com:11790", + striddeApiUrl: "https://stride-fleet.main.stridenet.co/api/Stride-Labs/stride/stakeibc/host_zone/phoenix-1", + erisStakingHubContractAddress: "terra10788fkzah89xrdm27zkj5yvhj9x3494lxawzm5qq3vvxcqz2yzaqyd3enk", + boneLunaHubContractAddress: "terra1l2nd99yze5fszmhl5svyh5fky9wm4nz4etlgnztfu4e8809gd52q04n3ea", + } +} + +func (p *LSDProvider) QueryLSTRebaseFactor(symbol string) (*sdk.Dec, error) { + switch symbol { + case "AMPLUNA": + return p.queryAmpLunaRebaseFactor() + case "BACKBONELUNA": + return p.queryBoneLunaRebaseFactor() + case "STLUNA": + return p.queryStLunaRebaseFactor() + default: + return nil, fmt.Errorf("LSDProvider no querier implemented for symbol '%s'", symbol) + } +} + +func (p *LSDProvider) queryAmpLunaRebaseFactor() (*sdk.Dec, error) { + ctx := context.Background() + connection, err := p.BaseGrpc.Connection(p.phoenixNodeUrl, nil) + if err != nil { + return nil, err + } + client := wasmtypes.NewQueryClient(connection) + + res, err := client.SmartContractState(ctx, &wasmtypes.QuerySmartContractStateRequest{ + Address: p.erisStakingHubContractAddress, + QueryData: []byte(`{ "state" : {}}`), + }) + if err != nil { + return nil, err + } + + var erisParsedRes types.ErisData + err = json.Unmarshal(res.Data, &erisParsedRes) + if err != nil { + return nil, err + } + + return &erisParsedRes.ExchangeRate, nil +} + +func (p *LSDProvider) queryBoneLunaRebaseFactor() (*sdk.Dec, error) { + ctx := context.Background() + connection, err := p.BaseGrpc.Connection(p.phoenixNodeUrl, nil) + if err != nil { + return nil, err + } + client := wasmtypes.NewQueryClient(connection) + + res, err := client.SmartContractState(ctx, &wasmtypes.QuerySmartContractStateRequest{ + Address: p.boneLunaHubContractAddress, + QueryData: []byte(`{ "state" : {}}`), + }) + if err != nil { + return nil, err + } + + var boneLunaParsedRes types.BoneLunaData + err = json.Unmarshal(res.Data, &boneLunaParsedRes) + if err != nil { + return nil, err + } + + return &boneLunaParsedRes.ExchangeRate, nil + +} + +func (p *LSDProvider) queryStLunaRebaseFactor() (*sdk.Dec, error) { + resp, err := http.Get(p.striddeApiUrl) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Read response body + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var res types.StrideData + // Parse JSON response into struct + err = json.Unmarshal(body, &res) + if err != nil { + return nil, err + } + + return &res.HostZone.RedemptionRate, nil +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 59c4121..7251eab 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -21,7 +21,7 @@ func NewProvider(exchange string, config *config.ProviderConfig, stopCh <-chan s switch strings.ToLower(exchange) { case "binance", "bitfinex", "bybit", "coinbase", "huobi", "kraken", "kucoin", "okx": return internal.NewWebsocketProvider(exchange, config.Symbols, stopCh) - case "bitstamp", "bittrex", "coingecko", "exchangerate", "fer", "frankfurter": + case "astroport", "bitstamp", "bittrex", "coingecko", "exchangerate", "fer", "frankfurter": return internal.NewRESTfulProvider(exchange, config.Symbols, config.Interval, config.Timeout, stopCh) case "osmosis": return osmosis.NewOsmosisProvider(config, stopCh) diff --git a/internal/provider/provider_manager.go b/internal/provider/provider_manager.go index e9fb530..83623c1 100644 --- a/internal/provider/provider_manager.go +++ b/internal/provider/provider_manager.go @@ -112,8 +112,6 @@ func averagePriceByCoin(prices map[string]types.PriceByPair) map[string]float64 if quotePrice, ok := prices[quoteUSD]; ok && quotePrice.Price > 0.0 { coinSum[priceByPair.Base] += priceByPair.Price * quotePrice.Price coinCount[priceByPair.Base] += 1.0 - } else { - println("line 106 ", quoteUSD) } } } diff --git a/internal/provider/transactions_provider.go b/internal/provider/transactions_provider.go index b71795f..cd0756f 100644 --- a/internal/provider/transactions_provider.go +++ b/internal/provider/transactions_provider.go @@ -2,20 +2,17 @@ package provider import ( "context" - "encoding/json" "fmt" "os" wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" "github.com/tendermint/tmlibs/bech32" "github.com/terra-money/oracle-feeder-go/internal/types" - pkgtypes "github.com/terra-money/oracle-feeder-go/pkg/types" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" - codecTypes "github.com/cosmos/cosmos-sdk/codec/types" sdktypes "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/crypto/hd" cryptoTypes "github.com/cosmos/cosmos-sdk/crypto/types" @@ -29,16 +26,20 @@ import ( ) type TransactionsProvider struct { - privKey cryptoTypes.PrivKey - nodeGrpcUrl string - oracleAddress string - address sdk.AccAddress - prefix string - denom string - chainId string + privKey cryptoTypes.PrivKey + nodeGrpcUrl string + oracleAddress string + allianceHubContractAddress string + address sdk.AccAddress + prefix string + denom string + chainId string + feederType types.FeederType } -func NewTransactionsProvider() TransactionsProvider { +func NewTransactionsProvider( + feederType types.FeederType, +) TransactionsProvider { var mnemonic string if mnemonic = os.Getenv("MNEMONIC"); len(mnemonic) == 0 { panic("MNEMONIC env variable is not set!") @@ -54,6 +55,16 @@ func NewTransactionsProvider() TransactionsProvider { panic("ORACLE_ADDRESS env variable is not set!") } + var allianceHubContractAddress string + if allianceHubContractAddress = os.Getenv("ALLIANCE_HUB_CONTRACT_ADDRESS"); len(allianceHubContractAddress) == 0 { + panic("ALLIANCE_HUB_CONTRACT_ADDRESS env variable is not set!") + } + + var chainId string + if chainId = os.Getenv("CHAIN_ID"); len(chainId) == 0 { + panic("ORACLE_ADDRESS env variable is not set!") + } + privKeyBytes, err := hd.Secp256k1.Derive()(mnemonic, "", "m/44'/330'/0'/0/0") if err != nil { panic(err) @@ -61,57 +72,50 @@ func NewTransactionsProvider() TransactionsProvider { privKey := hd.Secp256k1.Generate()(privKeyBytes) address := sdk.AccAddress(privKey.PubKey().Address()) - return TransactionsProvider{ - privKey: privKey, - nodeGrpcUrl: nodeGrpcUrl, - address: address, - oracleAddress: oracleAddress, - chainId: "pisco-1", - prefix: "terra", - denom: "uluna", + privKey: privKey, + nodeGrpcUrl: nodeGrpcUrl, + address: address, + oracleAddress: oracleAddress, + allianceHubContractAddress: allianceHubContractAddress, + feederType: feederType, + chainId: chainId, + prefix: "terra", + denom: "uluna", } } -func (p *TransactionsProvider) ParseAlliancesTransaction(protocolRes *types.AllianceProtocolRes) (msg sdk.Msg, err error) { - bech32Addr, err := bech32.ConvertAndEncode(p.prefix, p.address) - if err != nil { - return msg, err - } - executeMsg := pkgtypes.NewMsgUpdateChainsInfo(*protocolRes) - executeB, err := json.Marshal(executeMsg) +func (p *TransactionsProvider) SubmitAlliancesTransaction( + ctx context.Context, + msg []byte, +) (string, error) { + // Get the bech address and... + bech32Addr, err := bech32.ConvertAndEncode(p.prefix, p.address) if err != nil { - return msg, err + return "", err } - - fmt.Printf("%s", executeB) - // This needs to be a smart contract send execution - msg = &wasmtypes.MsgExecuteContract{ - Sender: bech32Addr, - Contract: p.oracleAddress, - Msg: executeB, - Funds: nil, + // ... build the messages to be signed + msgs := []sdk.Msg{ + &wasmtypes.MsgExecuteContract{ + Sender: bech32Addr, + Contract: p.getContractAddress(), + Msg: msg, + Funds: nil, + }, } - return msg, nil -} - -func (p *TransactionsProvider) SubmitAlliancesTransaction( - ctx context.Context, - msgs []sdk.Msg, -) (*string, error) { - var faucetAccount authTypes.AccountI + var account authTypes.AccountI txBuilder, txConfig, interfaceRegistry := p.getTxClients() // create gRPC connection grpcConn, err := p.getRPCConnection(p.nodeGrpcUrl, interfaceRegistry) if err != nil { - return nil, err + return "", err } defer grpcConn.Close() fromAddress, err := bech32.ConvertAndEncode(p.prefix, p.address) if err != nil { - return nil, err + return "", err } authClient := authTypes.NewQueryClient(grpcConn) @@ -119,21 +123,21 @@ func (p *TransactionsProvider) SubmitAlliancesTransaction( Address: fromAddress, }) if err != nil { - return nil, err + return "", err } - err = interfaceRegistry.UnpackAny(accRes.Account, &faucetAccount) + err = interfaceRegistry.UnpackAny(accRes.Account, &account) if err != nil { - return nil, err + return "", err } - accSeq := faucetAccount.GetSequence() + accSeq := account.GetSequence() pubKey := p.privKey.PubKey() signMode := txConfig.SignModeHandler().DefaultMode() signerData := signing.SignerData{ ChainID: p.chainId, - AccountNumber: faucetAccount.GetAccountNumber(), + AccountNumber: account.GetAccountNumber(), Sequence: accSeq, } sigData := txsigning.SingleSignatureData{ @@ -149,25 +153,25 @@ func (p *TransactionsProvider) SubmitAlliancesTransaction( // build txn err = txBuilder.SetMsgs(msgs...) if err != nil { - return nil, err + return "", err } err = txBuilder.SetSignatures(sigv2) if err != nil { - return nil, err + return "", err } // simulate transaction to get gas cost and see if it will fail simulateBytes, err := txConfig.TxEncoder()(txBuilder.GetTx()) if err != nil { - return nil, err + return "", err } queryClient := txTypes.NewServiceClient(grpcConn) simRes, err := queryClient.Simulate(ctx, &txTypes.SimulateRequest{ TxBytes: simulateBytes, }) if err != nil { - return nil, err + return "", err } // set the gas needed with some allowance @@ -178,17 +182,17 @@ func (p *TransactionsProvider) SubmitAlliancesTransaction( txBuilder.SetGasLimit(uint64(float64(gasUsed) * 1.5)) if err != nil { - return nil, err + return "", err } // sign the final message with the private key bytesToSign, err := txConfig.SignModeHandler().GetSignBytes(signMode, signerData, txBuilder.GetTx()) if err != nil { - return nil, err + return "", err } sig, err := p.privKey.Sign(bytesToSign) if err != nil { - return nil, err + return "", err } sigData = txsigning.SingleSignatureData{ SignMode: signMode, @@ -201,13 +205,13 @@ func (p *TransactionsProvider) SubmitAlliancesTransaction( } err = txBuilder.SetSignatures(sigv2) if err != nil { - return nil, err + return "", err } // encode and broadcast transaction txBytes, err := txConfig.TxEncoder()(txBuilder.GetTx()) if err != nil { - return nil, err + return "", err } // Increase cached account sequence before broadcasting tx bRes, err := queryClient.BroadcastTx(ctx, @@ -217,14 +221,17 @@ func (p *TransactionsProvider) SubmitAlliancesTransaction( }, ) if err != nil { - return nil, err + return "", err } - return &bRes.TxResponse.TxHash, err + if bRes.TxResponse.Code != 0 { + return "", fmt.Errorf("tx failed with code %d, %s", bRes.TxResponse.Code, bRes.TxResponse.RawLog) + } + return bRes.TxResponse.TxHash, err } -func (p *TransactionsProvider) getTxClients() (client.TxBuilder, client.TxConfig, codecTypes.InterfaceRegistry) { +func (p *TransactionsProvider) getTxClients() (client.TxBuilder, client.TxConfig, sdktypes.InterfaceRegistry) { amino := codec.NewLegacyAmino() - interfaceRegistry := codecTypes.NewInterfaceRegistry() + interfaceRegistry := sdktypes.NewInterfaceRegistry() protoCodec := codec.NewProtoCodec(interfaceRegistry) txConfig := tx.NewTxConfig(protoCodec, tx.DefaultSignModes) @@ -248,3 +255,14 @@ func (p *TransactionsProvider) getRPCConnection(nodeUrl string, interfaceRegistr grpc.WithDefaultCallOptions(grpc.ForceCodec(codec.NewProtoCodec(interfaceRegistry).GRPCCodec())), ) } + +func (p *TransactionsProvider) getContractAddress() string { + switch p.feederType { + case types.AllianceOracleFeeder: + return p.oracleAddress + case types.AllianceRebalanceFeeder: + return p.allianceHubContractAddress + } + + panic("Unknown feeder type") +} diff --git a/internal/restful/client.go b/internal/restful/client.go index d724543..5087b48 100644 --- a/internal/restful/client.go +++ b/internal/restful/client.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/terra-money/oracle-feeder-go/internal/restful/internal/astroport" "github.com/terra-money/oracle-feeder-go/internal/restful/internal/bitstamp" "github.com/terra-money/oracle-feeder-go/internal/restful/internal/bittrex" "github.com/terra-money/oracle-feeder-go/internal/restful/internal/coingecko" @@ -20,6 +21,8 @@ type restfulClient interface { func NewRESTfulClient(exchange string, symbols []string) (restfulClient, error) { var client restfulClient switch strings.ToLower(exchange) { + case "astroport": + client = astroport.NewAstroportClient() case "bitstamp": client = bitstamp.NewBitstampClient() case "bittrex": diff --git a/internal/restful/internal/astroport/astroport.go b/internal/restful/internal/astroport/astroport.go new file mode 100644 index 0000000..d5dde7f --- /dev/null +++ b/internal/restful/internal/astroport/astroport.go @@ -0,0 +1,102 @@ +package astroport + +import ( + "encoding/json" + "fmt" + "io" + "log" + "math" + "net/http" + "strings" + "time" + + "github.com/terra-money/oracle-feeder-go/internal/parser" + internal_types "github.com/terra-money/oracle-feeder-go/internal/types" +) + +type AstroportClient struct { + url string + amount int64 + chainId string +} + +func NewAstroportClient() *AstroportClient { + return &AstroportClient{ + url: "https://develop-multichain-api.astroport.fi/router/v2/routes", + amount: 1000000, + chainId: "phoenix-1", + } +} + +func (p *AstroportClient) FetchAndParse(symbols []string, timeout int) (map[string]internal_types.PriceBySymbol, error) { + astroData, err := p.fetchPrices(symbols, timeout) + if err != nil { + return nil, err + } + res := p.parseAmounts(astroData) + return res, nil +} + +func (p *AstroportClient) fetchPrices(symbols []string, timeout int) (map[string]internal_types.AstroportData, error) { + astroData := make(map[string]internal_types.AstroportData) + for _, symbol := range symbols { + symbolSplit := strings.Split(symbol, "-") + data, err := p.queryData(symbolSplit[0], symbolSplit[1]) + if err != nil { + return nil, err + } + + astroData[symbol] = data[0] + if err != nil { + return nil, err + } + + } + return astroData, nil +} + +func (p *AstroportClient) queryData(start, end string) (res []internal_types.AstroportData, err error) { + urlParams := fmt.Sprintf("?start=%s&end=%s&amount=%d&chainId=%s", start, end, p.amount, p.chainId) + + // Send GET request + resp, err := http.Get(p.url + urlParams) + if err != nil { + return nil, err + } + defer resp.Body.Close() + // Read response body + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // Parse JSON response into struct + err = json.Unmarshal(body, &res) + if err != nil { + return nil, err + } + + // Access parsed data + return res, nil +} + +func (p *AstroportClient) parseAmounts(res map[string]internal_types.AstroportData) map[string]internal_types.PriceBySymbol { + prices := make(map[string]internal_types.PriceBySymbol) + now := uint64(time.Now().UnixMilli()) + for symbol, value := range res { + base, quote, err := parser.ParseSymbol("astroport", symbol) + if err == nil { + prices[symbol] = internal_types.PriceBySymbol{ + Exchange: "astroport", + Symbol: symbol, + Base: base, + Quote: quote, + Price: float64(value.AmountOut) / math.Pow10(6), // 6 decimals + Timestamp: now, + } + } else { + log.Printf("%v", err) + } + } + return prices +} diff --git a/internal/types/alliance_hub_config.go b/internal/types/alliance_hub_config.go new file mode 100644 index 0000000..caa9f89 --- /dev/null +++ b/internal/types/alliance_hub_config.go @@ -0,0 +1,11 @@ +package types + +type AllianceHubConfigData struct { + Governance string `json:"governance"` + Controller string `json:"controller"` + Oracle string `json:"oracle"` + LastRewardUpdateTimestamp string `json:"last_reward_update_timestamp"` + AllianceTokenDenom string `json:"alliance_token_denom"` + AllianceTokenSupply string `json:"alliance_token_supply"` + RewardDenom string `json:"reward_denom"` +} diff --git a/internal/types/alliance_redelegate.go b/internal/types/alliance_redelegate.go new file mode 100644 index 0000000..20dda5a --- /dev/null +++ b/internal/types/alliance_redelegate.go @@ -0,0 +1,29 @@ +package types + +import "github.com/cosmos/cosmos-sdk/types" + +type Redelegation struct { + SrcValidator string `json:"src_validator"` + DstValidator string `json:"dst_validator"` + Amount string `json:"amount"` +} + +func NewRedelegation(srcValidator string, dstValidator string, amount string) Redelegation { + return Redelegation{ + SrcValidator: srcValidator, + DstValidator: dstValidator, + Amount: amount, + } +} + +type ValWithAllianceTokensStake struct { + ValidatorAddr string + TotalStaked types.DecCoin +} + +func NewValWithAllianceTokensStake(validatorAddr string, totalStaked types.DecCoin) ValWithAllianceTokensStake { + return ValWithAllianceTokensStake{ + ValidatorAddr: validatorAddr, + TotalStaked: totalStaked, + } +} diff --git a/internal/types/astroport.go b/internal/types/astroport.go new file mode 100644 index 0000000..8e79ced --- /dev/null +++ b/internal/types/astroport.go @@ -0,0 +1,59 @@ +package types + +type AstroportData struct { + Path Path `json:"path"` + Simulate Simulate `json:"simulate"` + AmountOut int `json:"amount_out"` + TotalPriceImpact float64 `json:"total_price_impact"` +} + +type Path struct { + Route []Route `json:"route"` + Tokens []string `json:"tokens"` + Illiquid bool `json:"illiquid"` +} + +type Route struct { + ContractAddr string `json:"contract_addr"` + From string `json:"from"` + To string `json:"to"` + Type string `json:"type"` + PriceImpact float64 `json:"price_impact"` + Illiquid bool `json:"illiquid"` +} + +type Simulate struct { + SimulateSwapOperations SimulateSwapOperations `json:"simulate_swap_operations"` +} + +type SimulateSwapOperations struct { + OfferAmount string `json:"offer_amount"` + Operations []Operation `json:"operations"` +} + +type Operation struct { + AstroSwap AstroSwap `json:"astro_swap"` +} + +type AstroSwap struct { + OfferAssetInfo OfferAssetInfo `json:"offer_asset_info"` + AskAssetInfo AskAssetInfo `json:"ask_asset_info"` +} + +type OfferAssetInfo struct { + Token *Token `json:"token,omitempty"` + NativeToken *AstroNativeToken `json:"native_token,omitempty"` +} + +type AskAssetInfo struct { + Token *Token `json:"token,omitempty"` + NativeToken *AstroNativeToken `json:"native_token,omitempty"` +} + +type Token struct { + ContractAddr string `json:"contract_addr"` +} + +type AstroNativeToken struct { + Denom string `json:"denom"` +} diff --git a/internal/types/bone_luna_stake_contract.go b/internal/types/bone_luna_stake_contract.go new file mode 100644 index 0000000..7928ee1 --- /dev/null +++ b/internal/types/bone_luna_stake_contract.go @@ -0,0 +1,12 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type BoneLunaData struct { + TotalUSTeak string `json:"total_usteak"` + TotalNative string `json:"total_native"` + ExchangeRate sdk.Dec `json:"exchange_rate"` + UnlockedCoins []sdk.Coin `json:"unlocked_coins"` +} diff --git a/internal/types/eris_stake_contract.go b/internal/types/eris_stake_contract.go new file mode 100644 index 0000000..ac48569 --- /dev/null +++ b/internal/types/eris_stake_contract.go @@ -0,0 +1,15 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type ErisData struct { + TotalUSTake string `json:"total_ustake"` + TotalULuna string `json:"total_uluna"` + ExchangeRate sdk.Dec `json:"exchange_rate"` + UnlockedCoins []sdk.Coin `json:"unlocked_coins"` + Unbonding string `json:"unbonding"` + Available string `json:"available"` + TVLULuna string `json:"tvl_uluna"` +} diff --git a/internal/types/feeder_type.go b/internal/types/feeder_type.go new file mode 100644 index 0000000..7a0f2e8 --- /dev/null +++ b/internal/types/feeder_type.go @@ -0,0 +1,38 @@ +package types + +import "fmt" + +type FeederType string + +const ( + AllianceOracleFeeder FeederType = "alliance-oracle-feeder" + AllianceRebalanceFeeder FeederType = "alliance-rebalance-feeder" +) + +// parse from string to FeederType +func ParseFeederTypeFromString(s string) (FeederType, error) { + switch s { + case string(AllianceOracleFeeder): + return AllianceOracleFeeder, nil + case string(AllianceRebalanceFeeder): + return AllianceRebalanceFeeder, nil + default: + return "", fmt.Errorf( + `invalid feeder type: "%s", expected types are "%s" | "%s"`, + s, + AllianceOracleFeeder, + AllianceRebalanceFeeder, + ) + } +} + +func FromFeederTypeToPriceServerUrl(feederType FeederType) string { + switch feederType { + case AllianceOracleFeeder: + return "/alliance/protocol" + case AllianceRebalanceFeeder: + return "/alliance/rebalance" + default: + return "" + } +} diff --git a/internal/types/station_gov.go b/internal/types/station_gov.go new file mode 100644 index 0000000..889c120 --- /dev/null +++ b/internal/types/station_gov.go @@ -0,0 +1,20 @@ +package types + +type VoteOption string + +const ( + VoteOptionYes VoteOption = "VOTE_OPTION_YES" + VoteOptionNo VoteOption = "VOTE_OPTION_NO" +) + +type VoteOptionWeight struct { + Option VoteOption `json:"option"` + Weight string `json:"weight"` +} + +type StationVote struct { + Voter string `json:"voter"` + Options []VoteOptionWeight `json:"options"` +} + +type StationVotes = []StationVote diff --git a/internal/types/stride.go b/internal/types/stride.go new file mode 100644 index 0000000..a69f1a2 --- /dev/null +++ b/internal/types/stride.go @@ -0,0 +1,51 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type StrideData struct { + HostZone struct { + ChainID string `json:"chain_id"` + ConnectionID string `json:"connection_id"` + Bech32Prefix string `json:"bech32prefix"` + TransferChannelID string `json:"transfer_channel_id"` + Validators []struct { + Name string `json:"name"` + Address string `json:"address"` + DelegationAmt string `json:"delegation_amt"` + Weight string `json:"weight"` + InternalExchangeRate *struct { + InternalTokensToSharesRate string `json:"internal_tokens_to_shares_rate"` + EpochNumber string `json:"epoch_number"` + } `json:"internal_exchange_rate"` + } `json:"validators"` + BlacklistedValidators []string `json:"blacklisted_validators"` + WithdrawalAccount struct { + Address string `json:"address"` + Target string `json:"target"` + } `json:"withdrawal_account"` + FeeAccount struct { + Address string `json:"address"` + Target string `json:"target"` + } `json:"fee_account"` + DelegationAccount struct { + Address string `json:"address"` + Target string `json:"target"` + } `json:"delegation_account"` + RedemptionAccount struct { + Address string `json:"address"` + Target string `json:"target"` + } `json:"redemption_account"` + IBCDenom string `json:"ibc_denom"` + HostDenom string `json:"host_denom"` + LastRedemptionRate sdk.Dec `json:"last_redemption_rate"` + RedemptionRate sdk.Dec `json:"redemption_rate"` + UnbondingFrequency string `json:"unbonding_frequency"` + StakedBal string `json:"staked_bal"` + Address string `json:"address"` + Halted bool `json:"halted"` + MinRedemptionRate sdk.Dec `json:"min_redemption_rate"` + MaxRedemptionRate sdk.Dec `json:"max_redemption_rate"` + } `json:"host_zone"` +} diff --git a/params.json b/params.json index 6df29d4..4a351f4 100644 --- a/params.json +++ b/params.json @@ -1,5 +1,8 @@ { "price_server_port": "8532", - "oracle_address": "terra1vhndln95yd7rngslzvf6sax6axcshkxqpmpr886ntelh28p9ghuqspfswp", - "feeder_retries": 4 + "oracle_address": "terra19y3kv8r4ef4wjfvw5fzqnkff8fut0f4y3lt46k4ce0a0cwpscuqqg39mvq", + "alliance_hub_contract_address" : "terra1majrm6e6n0eg760n9fs4g5jvwzh4ytp8e2d99mfgzv2e7mjmdwxse0ty73", + "feeder_retries": 4, + "chain_id": "pisco-1", + "station_api": "https://pisco-api.terra.dev" } \ No newline at end of file diff --git a/pkg/types/execute.go b/pkg/types/execute.go index 0d24fe4..95c7004 100644 --- a/pkg/types/execute.go +++ b/pkg/types/execute.go @@ -19,3 +19,18 @@ func NewMsgUpdateChainsInfo(data types.AllianceProtocolRes) MsgUpdateChainsInfo }, } } + +type AllianceRedelegate struct { + Redelegations []types.Redelegation `json:"redelegations"` +} +type MsgAllianceRedelegate struct { + AllianceRedelegate AllianceRedelegate `json:"alliance_redelegate"` +} + +func NewMsgAllianceRedelegate(redelegations []types.Redelegation) MsgAllianceRedelegate { + return MsgAllianceRedelegate{ + AllianceRedelegate: AllianceRedelegate{ + Redelegations: redelegations, + }, + } +} From 74bc5cb86ccc759475d0e44d1ad6bf0e117f2518 Mon Sep 17 00:00:00 2001 From: Michal Turcan Date: Wed, 28 Jun 2023 16:17:21 +0200 Subject: [PATCH 2/4] update params and gh action (#28) Co-authored-by: tuky191 --- .github/workflows/docker.yml | 12 ++++++++---- params.json | 17 +++++++++++------ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 5efdb17..39f43d4 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -30,15 +30,19 @@ jobs: - name: Parse refs.json id: parse-refs + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' run: | # Install jq to parse JSON sudo apt-get update sudo apt-get install -y jq - # Parse refs.json and extract the required information - ORACLE_ADDRESS=$(jq -r '.oracle_address' params.json) - PRICE_SERVER_PORT=$(jq -r '.price_server_port' params.json) - FEEDER_RETRIES=$(jq -r '.feeder_retries' params.json) + # Determine the branch name + BRANCH_NAME=${GITHUB_REF#refs/heads/} + + # Parse params.json and extract the required information + ORACLE_ADDRESS=$(jq -r --arg branch "$BRANCH_NAME" '.[$branch].oracle_address' params.json) + PRICE_SERVER_PORT=$(jq -r --arg branch "$BRANCH_NAME" '.[$branch].price_server_port' params.json) + FEEDER_RETRIES=$(jq -r --arg branch "$BRANCH_NAME" '.[$branch].feeder_retries' params.json) # Set output parameters for subsequent jobs echo "::set-output name=oracle_address::$ORACLE_ADDRESS" diff --git a/params.json b/params.json index 4a351f4..6358a33 100644 --- a/params.json +++ b/params.json @@ -1,8 +1,13 @@ { - "price_server_port": "8532", - "oracle_address": "terra19y3kv8r4ef4wjfvw5fzqnkff8fut0f4y3lt46k4ce0a0cwpscuqqg39mvq", - "alliance_hub_contract_address" : "terra1majrm6e6n0eg760n9fs4g5jvwzh4ytp8e2d99mfgzv2e7mjmdwxse0ty73", - "feeder_retries": 4, - "chain_id": "pisco-1", - "station_api": "https://pisco-api.terra.dev" + "main": { + "price_server_port": "8532", + "oracle_address": "terra1vhndln95yd7rngslzvf6sax6axcshkxqpmpr886ntelh28p9ghuqspfswp", + "feeder_retries": 4 + }, + "develop": { + "price_server_port": "8532", + "oracle_address": "terra19y3kv8r4ef4wjfvw5fzqnkff8fut0f4y3lt46k4ce0a0cwpscuqqg39mvq", + "alliance_hub_contract_address" : "terra1majrm6e6n0eg760n9fs4g5jvwzh4ytp8e2d99mfgzv2e7mjmdwxse0ty73", + "feeder_retries": 4 + } } \ No newline at end of file From ae5870f555bd1c99c4f5dba60900edc4752b808a Mon Sep 17 00:00:00 2001 From: Michal Turcan Date: Wed, 28 Jun 2023 16:51:35 +0200 Subject: [PATCH 3/4] Feat/update env variables (#29) * update params and gh action * update github actions --------- Co-authored-by: tuky191 --- .github/workflows/docker.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 39f43d4..3fbe1f5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -41,13 +41,16 @@ jobs: # Parse params.json and extract the required information ORACLE_ADDRESS=$(jq -r --arg branch "$BRANCH_NAME" '.[$branch].oracle_address' params.json) + ALLIANCE_HUB_CONTRACT_ADDRESS=$(jq -r --arg branch "$BRANCH_NAME" '.[$branch].alliance_hub_contract_address' params.json) PRICE_SERVER_PORT=$(jq -r --arg branch "$BRANCH_NAME" '.[$branch].price_server_port' params.json) FEEDER_RETRIES=$(jq -r --arg branch "$BRANCH_NAME" '.[$branch].feeder_retries' params.json) # Set output parameters for subsequent jobs echo "::set-output name=oracle_address::$ORACLE_ADDRESS" + echo "::set-output name=alliance_hub_contract_address::$ALLIANCE_HUB_CONTRACT_ADDRESS" echo "::set-output name=price_server_port::$PRICE_SERVER_PORT" echo "::set-output name=feeder_retries::$FEEDER_RETRIES" + build: runs-on: ubuntu-latest permissions: @@ -98,6 +101,7 @@ jobs: type=semver,pattern={{tag}} type=semver,pattern={{version}} type=raw,value=latest,event=branch,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + type=raw,value=main,event=branch,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} type=raw,value=develop,event=branch,enable=${{ github.ref == format('refs/heads/{0}', 'develop') }} - name: Build docker image @@ -140,4 +144,4 @@ jobs: ref: ${{ env.TF_GITHUB_REF }} workflow: Terraform token: ${{ secrets.TFL_GITHUB_TOKEN }} - inputs: '{ "ORACLE_ADDRESS": "${{ needs.refs-parsing.outputs.oracle_address }}", "PRICE_SERVER_PORT": "${{needs.refs-parsing.outputs.price_server_port}}", "FEEDER_RETRIES": "${{needs.refs-parsing.outputs.feeder_retries}}", "TAG": "${{env.TAG}}" }' + inputs: '{ "ALLIANCE_HUB_CONTRACT_ADDRESS": "${{ needs.refs-parsing.outputs.alliance_hub_contract_address }}", "ORACLE_ADDRESS": "${{ needs.refs-parsing.outputs.oracle_address }}", "PRICE_SERVER_PORT": "${{needs.refs-parsing.outputs.price_server_port}}", "FEEDER_RETRIES": "${{needs.refs-parsing.outputs.feeder_retries}}", "TAG": "${{env.TAG}}" }' From 253851f07c85d15fa95c443ec59ee1b1d8405eac Mon Sep 17 00:00:00 2001 From: emidev98 Date: Tue, 4 Jul 2023 09:33:00 +0300 Subject: [PATCH 4/4] feat: update env variables --- Makefile | 2 +- cmd/price-server/price_server.go | 1 + params.json | 5 +++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 87dee5a..5235836 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ start-alliance-rebalance-feeder: start-price-server: go run ./cmd/price-server/price_server.go -.PHONY: start-feeder start-price-server +.PHONY: start-alliance-oracle-feeder start-alliance-rebalance-feeder start-price-server ################################################# diff --git a/cmd/price-server/price_server.go b/cmd/price-server/price_server.go index 261f3f3..63a9cc8 100644 --- a/cmd/price-server/price_server.go +++ b/cmd/price-server/price_server.go @@ -34,6 +34,7 @@ func main() { }) r.GET("/alliance/protocol", func(c *gin.Context) { allianceProtocolRes, err := allianceProvider.GetProtocolsInfo(ctx) + allianceProtocolRes.UpdateChainsInfo.ChainsInfo.ProtocolsInfo[0].ChainId = "narwhal-1" if err != nil { c.JSON(http.StatusInternalServerError, err) return diff --git a/params.json b/params.json index 6358a33..4fe96b2 100644 --- a/params.json +++ b/params.json @@ -1,12 +1,13 @@ { "main": { "price_server_port": "8532", - "oracle_address": "terra1vhndln95yd7rngslzvf6sax6axcshkxqpmpr886ntelh28p9ghuqspfswp", + "oracle_address": "", + "alliance_hub_contract_address": "", "feeder_retries": 4 }, "develop": { "price_server_port": "8532", - "oracle_address": "terra19y3kv8r4ef4wjfvw5fzqnkff8fut0f4y3lt46k4ce0a0cwpscuqqg39mvq", + "oracle_address": "terra1jf3nndysevley5p3wnajkjvjxcql9d00gpj4en3xwp7yrkrdqess48rr27", "alliance_hub_contract_address" : "terra1majrm6e6n0eg760n9fs4g5jvwzh4ytp8e2d99mfgzv2e7mjmdwxse0ty73", "feeder_retries": 4 }