diff --git a/api/account/api.go b/api/account/api.go index d5f778ec..a6fad3cd 100644 --- a/api/account/api.go +++ b/api/account/api.go @@ -28,139 +28,131 @@ const ( FixSlashedDelegations = "fixslasheddelegations" ) -func Register(router *gin.Engine, db *database.Database, s *store.Store, sdkServiceClients sdkservice.SDKServiceClients) { +type AccountAPI struct { + app App + // FIXME remove when #801 is done + db *database.Database +} + +func New(app App) *AccountAPI { + return &AccountAPI{ + app: app, + } +} + +func Register(router *gin.Engine, db *database.Database, s *store.Store, sdkServiceClients sdkservice.SDKServiceClients, app App) { + + accountAPI := New(app) + // FIXME remove when #801 is done + accountAPI.db = db + router.GET("/accounts/:rawaddress", accountAPI.GetAccounts) + group := router.Group("/account/:address") - group.GET("/balance", GetBalancesByAddress(db)) - group.GET("/stakingbalances", GetDelegationsByAddress(db)) - group.GET("/unbondingdelegations", GetUnbondingDelegationsByAddress(db)) + group.GET("/balance", accountAPI.GetBalancesByAddress) + group.GET("/stakingbalances", accountAPI.GetDelegationsByAddress) + group.GET("/unbondingdelegations", accountAPI.GetUnbondingDelegationsByAddress) group.GET("/numbers", GetNumbersByAddress(db, sdkServiceClients)) group.GET("/tickets", GetUserTickets(db, s)) group.GET("/delegatorrewards/:chain", GetDelegatorRewards(db, sdkServiceClients)) } -// GetBalancesByAddress returns account of an address. -// @Summary Gets address balance +// GetAccounts returns accounts from a raw address +// @Summary Gets accounts' balance, delegation and rewards // @Tags Account -// @ID get-account -// @Description gets address balance +// @ID get-accounts +// @Description gets accounts' balance, delegation and rewards // @Produce json -// @Param address path string true "address to query balance for" -// @Success 200 {object} BalancesResponse +// @Param raw address path string true "raw address to query balance for" +// @Success 200 {object} AccountsResponse // @Failure 500,403 {object} apierrors.UserFacingError -// @Router /account/{address}/balance [get] -func GetBalancesByAddress(db *database.Database) gin.HandlerFunc { - return func(c *gin.Context) { - ctx := c.Request.Context() - var res BalancesResponse - - address := c.Param("address") - - balances, err := db.Balances(ctx, address) +// @Router /accounts/{rawaddress} [get] +func (a *AccountAPI) GetAccounts(c *gin.Context) { + var ( + ctx = c.Request.Context() + rawAddress = c.Param("rawaddress") + resp AccountsResponse + ) + // Derive addresses + addrs, err := a.app.DeriveRawAddress(ctx, rawAddress) + if err != nil { + err := apierrors.Wrap(err, "account", + fmt.Sprintf("cannot derive addresses from raw address %v", rawAddress), + http.StatusBadRequest, + ) + _ = c.Error(err) + return + } + fmt.Println("ADRS", addrs) + g, ctx := errgroup.WithContext(ctx) + // Fetch balances + g.Go(func() error { + balances, err := a.app.Balances(ctx, addrs) if err != nil { - e := apierrors.New( - "account", - fmt.Sprintf("cannot retrieve account for address %v", address), + return apierrors.Wrap(err, "account", + fmt.Sprintf("cannot retrieve balances for raw address %v", rawAddress), http.StatusBadRequest, - ).WithLogContext( - fmt.Errorf("cannot query database balance for address: %w", err), - "address", - address, ) - _ = c.Error(e) - return } - - vd, err := verifiedDenomsMap(ctx, db) + resp.Balances = balances + return nil + }) + // Fetch staking balances + g.Go(func() error { + stakingBalances, err := a.app.StakingBalances(ctx, addrs) if err != nil { - e := apierrors.New( - "account", - fmt.Sprintf("cannot retrieve account for address %v", address), + return apierrors.Wrap(err, "account", + fmt.Sprintf("cannot retrieve staking balances for raw address %v", rawAddress), http.StatusBadRequest, - ).WithLogContext( - fmt.Errorf("cannot query database verified denoms: %w", err), - "address", - address, ) - _ = c.Error(e) - return } - - // TODO: get unique chains - // perhaps we can remove this since there will be another endpoint specifically for fee tokens - - for _, b := range balances { - res.Balances = append(res.Balances, balanceRespForBalance( - ctx, - b, - vd, - db.DenomTrace, - )) + resp.StakingBalances = stakingBalances + return nil + }) + // Fetch unbonding delegations + g.Go(func() error { + unbondingDelegations, err := a.app.UnbondingDelegations(ctx, addrs) + if err != nil { + return apierrors.Wrap(err, "account", + fmt.Sprintf("cannot retrieve unbonding delegations for raw address %v", rawAddress), + http.StatusBadRequest, + ) } - - c.JSON(http.StatusOK, res) + resp.UnbondingDelegations = unbondingDelegations + return nil + }) + if err := g.Wait(); err != nil { + _ = c.Error(err) + return } + c.JSON(http.StatusOK, resp) } -// What lies ahead is a refactoring operation to ease testing of the algorithm implemented -// to determine whether a given IBC balance is verified or not. -// Since at the time of this commit there isn't a well-formed testing framework for -// api-server, we refactored the algo out, and provided a database querying function type. -// This way we can easily implement table testing for this sensible component, and provide -// fixes to it in a time-sensitive manner. -// This will most probably go away as soon as we have proper testing in place. -type denomTraceFunc func(context.Context, string, string) (tracelistener.IBCDenomTraceRow, error) - -func balanceRespForBalance(ctx context.Context, rawBalance tracelistener.BalanceRow, vd map[string]bool, dt denomTraceFunc) Balance { - balance := Balance{ - Address: rawBalance.Address, - Amount: rawBalance.Amount, - OnChain: rawBalance.ChainName, - } - - verified := vd[rawBalance.Denom] - baseDenom := rawBalance.Denom - - if rawBalance.Denom[:4] == "ibc/" { - // is ibc token - balance.Ibc = IbcInfo{ - Hash: rawBalance.Denom[4:], - } - - // if err is nil, the ibc denom has a denom trace associated with it - // so we return it, along with its verified status as well as the complete ibc - // path - - // otherwise, since we don't touch `verified` and `baseDenom` variables, we stick to the - // original `ibc/...` denom, which will be unverified by default - denomTrace, err := dt(ctx, rawBalance.ChainName, rawBalance.Denom[4:]) - if err == nil { - balance.Ibc.Path = denomTrace.Path - baseDenom = denomTrace.BaseDenom - verified = vd[denomTrace.BaseDenom] - } - } - - balance.Verified = verified - balance.BaseDenom = baseDenom +// GetBalancesByAddress returns account of an address. +// @Summary Gets address balance +// @Tags Account +// @ID get-account +// @Description gets address balance +// @Produce json +// @Param address path string true "address to query balance for" +// @Success 200 {object} BalancesResponse +// @Failure 500,403 {object} apierrors.UserFacingError +// @Router /account/{address}/balance [get] +func (a *AccountAPI) GetBalancesByAddress(c *gin.Context) { + ctx := c.Request.Context() - return balance -} + address := c.Param("address") -func verifiedDenomsMap(ctx context.Context, d *database.Database) (map[string]bool, error) { - chains, err := d.VerifiedDenoms(ctx) + balances, err := a.app.Balances(ctx, []string{address}) if err != nil { - return nil, err - } - - ret := make(map[string]bool) - for _, cc := range chains { - for _, vd := range cc { - ret[vd.Name] = vd.Verified - } + err := apierrors.Wrap(err, "account", + fmt.Sprintf("cannot retrieve balances for address %v", address), + http.StatusBadRequest, + ) + _ = c.Error(err) + return } - - return ret, err + c.JSON(http.StatusOK, BalancesResponse{Balances: balances}) } // GetDelegationsByAddress returns staking account of an address. @@ -173,147 +165,34 @@ func verifiedDenomsMap(ctx context.Context, d *database.Database) (map[string]bo // @Success 200 {object} StakingBalancesResponse // @Failure 500,400 {object} apierrors.UserFacingError // @Router /account/{address}/stakingbalances [get] -func GetDelegationsByAddress(db *database.Database) gin.HandlerFunc { - return func(c *gin.Context) { - ctx := c.Request.Context() - var res StakingBalancesResponse - - address := c.Param("address") +func (a *AccountAPI) GetDelegationsByAddress(c *gin.Context) { + ctx := c.Request.Context() + var res StakingBalancesResponse - if fflag.Enabled(c, FixSlashedDelegations) { - dl, err := db.Delegations(ctx, address) - - if err != nil { - e := apierrors.New( - "delegations", - fmt.Sprintf("cannot retrieve delegations for address %v", address), - http.StatusBadRequest, - ).WithLogContext( - fmt.Errorf("cannot query database delegations for addresses: %w", err), - "address", - address, - ) - _ = c.Error(e) - - return - } - - for _, del := range dl { - delegationAmount, err := sdktypes.NewDecFromStr(del.Amount) - if err != nil { - e := apierrors.New( - "delegations", - fmt.Sprintf("cannot convert delegation amount to Dec"), - http.StatusInternalServerError, - ).WithLogContext( - fmt.Errorf("cannot convert delegation amount to Dec: %w", err), - "address", - address, - ) - _ = c.Error(e) - - return - } - - validatorShares, err := sdktypes.NewDecFromStr(del.ValidatorShares) - if err != nil { - e := apierrors.New( - "delegations", - fmt.Sprintf("cannot convert validator total shares to Dec"), - http.StatusInternalServerError, - ).WithLogContext( - fmt.Errorf("cannot convert validator total shares to Dec: %w", err), - "address", - address, - ) - _ = c.Error(e) - - return - } - - validatorTokens, err := sdktypes.NewDecFromStr(del.ValidatorTokens) - if err != nil { - e := apierrors.New( - "delegations", - fmt.Sprintf("cannot convert validator total tokens to Dec"), - http.StatusInternalServerError, - ).WithLogContext( - fmt.Errorf("cannot convert validator total tokens to Dec: %w", err), - "address", - address, - ) - _ = c.Error(e) - - return - } - - // apply shares * total_validator_balance / total_validator_shares - balance := delegationAmount.Mul(validatorTokens).Quo(validatorShares) - res.StakingBalances = append(res.StakingBalances, StakingBalance{ - ValidatorAddress: del.Validator, - Amount: balance.String(), - ChainName: del.ChainName, - }) - } - - c.JSON(http.StatusOK, res) - } else { - dl, err := db.DelegationsOldResponse(ctx, address) - - if err != nil { - e := apierrors.New( - "delegations", - fmt.Sprintf("cannot retrieve delegations for address %v", address), - http.StatusBadRequest, - ).WithLogContext( - fmt.Errorf("cannot query database delegations for addresses: %w", err), - "address", - address, - ) - _ = c.Error(e) - - return - } - - for _, del := range dl { - res.StakingBalances = append(res.StakingBalances, StakingBalance{ - ValidatorAddress: del.Validator, - Amount: del.Amount, - ChainName: del.ChainName, - }) - } - - c.JSON(http.StatusOK, res) + address := c.Param("address") + if fflag.Enabled(c, FixSlashedDelegations) { + balances, err := a.app.StakingBalances(ctx, []string{address}) + if err != nil { + err := apierrors.Wrap(err, "delegations", + fmt.Sprintf("cannot retrieve delegations for address %v", address), + http.StatusBadRequest, + ) + _ = c.Error(err) + return } - } -} + c.JSON(http.StatusOK, StakingBalancesResponse{StakingBalances: balances}) -// GetUnbondingDelegationsByAddress returns the unbonding delegations of an address -// @Summary Gets unbonding delegations -// @Description gets unbonding delegations -// @Tags Account -// @ID get-unbonding-delegations-account -// @Produce json -// @Param address path string true "address to query unbonding delegations for" -// @Success 200 {object} UnbondingDelegationsResponse -// @Failure 500,403 {object} apierrors.UserFacingError -// @Router /account/{address}/unbondingdelegations [get] -func GetUnbondingDelegationsByAddress(db *database.Database) gin.HandlerFunc { - return func(c *gin.Context) { - ctx := c.Request.Context() - var res UnbondingDelegationsResponse + } else { - address := c.Param("address") - - unbondings, err := db.UnbondingDelegations(ctx, address) + dl, err := a.db.DelegationsOldResponse(ctx, address) if err != nil { e := apierrors.New( - "unbonding delegations", - fmt.Sprintf("cannot retrieve unbonding delegations for address %v", address), + "delegations", + fmt.Sprintf("cannot retrieve delegations for address %v", address), http.StatusBadRequest, ).WithLogContext( - fmt.Errorf("cannot query database unbonding delegations for addresses: %w", err), + fmt.Errorf("cannot query database delegations for addresses: %w", err), "address", address, ) @@ -322,11 +201,11 @@ func GetUnbondingDelegationsByAddress(db *database.Database) gin.HandlerFunc { return } - for _, unbonding := range unbondings { - res.UnbondingDelegations = append(res.UnbondingDelegations, UnbondingDelegation{ - ValidatorAddress: unbonding.Validator, - Entries: unbonding.Entries, - ChainName: unbonding.ChainName, + for _, del := range dl { + res.StakingBalances = append(res.StakingBalances, StakingBalance{ + ValidatorAddress: del.Validator, + Amount: del.Amount, + ChainName: del.ChainName, }) } @@ -334,6 +213,35 @@ func GetUnbondingDelegationsByAddress(db *database.Database) gin.HandlerFunc { } } +// GetUnbondingDelegationsByAddress returns the unbonding delegations of an address +// @Summary Gets unbonding delegations +// @Description gets unbonding delegations +// @Tags Account +// @ID get-unbonding-delegations-account +// @Produce json +// @Param address path string true "address to query unbonding delegations for" +// @Success 200 {object} UnbondingDelegationsResponse +// @Failure 500,403 {object} apierrors.UserFacingError +// @Router /account/{address}/unbondingdelegations [get] +func (a *AccountAPI) GetUnbondingDelegationsByAddress(c *gin.Context) { + ctx := c.Request.Context() + + address := c.Param("address") + unbondings, err := a.app.UnbondingDelegations(ctx, []string{address}) + if err != nil { + err := apierrors.Wrap(err, "unbonding delegations", + fmt.Sprintf("cannot retrieve unbonding delegations for address %v", address), + http.StatusBadRequest, + ) + _ = c.Error(err) + return + } + + c.JSON(http.StatusOK, UnbondingDelegationsResponse{ + UnbondingDelegations: unbondings, + }) +} + // GetDelegatorRewards returns the delegations rewards of an address on a chain // @Summary Gets delegation rewards // @Description gets delegation rewards diff --git a/api/account/api_test.go b/api/account/api_test.go index 6368bc23..b0720fba 100644 --- a/api/account/api_test.go +++ b/api/account/api_test.go @@ -1,151 +1,278 @@ -package account +package account_test import ( "context" - "fmt" + "encoding/json" + "net/http" + "net/http/httptest" "testing" + "github.com/emerishq/demeris-api-server/api/account" + "github.com/emerishq/demeris-api-server/lib/fflag" "github.com/emerishq/demeris-backend-models/tracelistener" + "github.com/emerishq/emeris-utils/logging" + "github.com/gin-gonic/gin" + gomock "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func Test_balanceRespForBalance(t *testing.T) { - tests := []struct { - name string - rawBalance tracelistener.BalanceRow - vd map[string]bool - dt denomTraceFunc - want Balance - }{ - { - "verified IBC balance returns verified balance", - tracelistener.BalanceRow{ - Address: "address", - Amount: "42", - Denom: "ibc/hash", - }, - map[string]bool{ - "uatom": true, +type mocks struct { + app *MockApp +} + +func newAccountAPI(t *testing.T, setup func(mocks)) *account.AccountAPI { + ctrl := gomock.NewController(t) + m := mocks{ + app: NewMockApp(ctrl), + } + if setup != nil { + setup(m) + } + return account.New(m.app) +} + +func TestGetAccounts(t *testing.T) { + var ( + ctx = context.Background() + resp = account.AccountsResponse{ + Balances: []account.Balance{ + {Address: "adr1", BaseDenom: "denom1", Amount: "42"}, + {Address: "adr2", BaseDenom: "denom2", Amount: "42"}, }, - func(_ context.Context, _, hash string) (tracelistener.IBCDenomTraceRow, error) { - return tracelistener.IBCDenomTraceRow{ - Path: "path", - BaseDenom: "uatom", - Hash: "hash", - }, nil + StakingBalances: []account.StakingBalance{ + {ValidatorAddress: "adr1", ChainName: "chain1", Amount: "42"}, + {ValidatorAddress: "adr2", ChainName: "chain1", Amount: "42"}, }, - Balance{ - Address: "address", - BaseDenom: "uatom", - Verified: true, - Amount: "42", - OnChain: "", - Ibc: IbcInfo{ - Path: "path", - Hash: "hash", + UnbondingDelegations: []account.UnbondingDelegation{ + { + ChainName: "chain1", + ValidatorAddress: "vadr1", + Entries: []tracelistener.UnbondingDelegationEntry{ + { + Balance: "42", + InitialBalance: "1", + CreationHeight: 1024, + CompletionTime: "time", + }, + }, }, }, - }, + } + respJSON, _ = json.Marshal(resp) + ) + tests := []struct { + name string + expectedStatusCode int + expectedBody string + expectedError string + setup func(mocks) + }{ { - "non-verified IBC balance returns non-verified balance", - tracelistener.BalanceRow{ - Address: "address", - Amount: "42", - Denom: "ibc/hash", - }, - map[string]bool{ - "uatom": false, - }, - func(_ context.Context, _, hash string) (tracelistener.IBCDenomTraceRow, error) { - return tracelistener.IBCDenomTraceRow{ - Path: "path", - BaseDenom: "uatom", - Hash: "hash", - }, nil - }, - Balance{ - Address: "address", - BaseDenom: "uatom", - Verified: false, - Amount: "42", - OnChain: "", - Ibc: IbcInfo{ - Path: "path", - Hash: "hash", - }, + name: "ok", + expectedStatusCode: http.StatusOK, + expectedBody: string(respJSON), + + setup: func(m mocks) { + adrs := []string{"adr1", "adr2"} + m.app.EXPECT().DeriveRawAddress(gomock.Any(), "xxx").Return(adrs, nil) + m.app.EXPECT().Balances(gomock.Any(), adrs).Return(resp.Balances, nil) + m.app.EXPECT().StakingBalances(gomock.Any(), adrs).Return(resp.StakingBalances, nil) + m.app.EXPECT().UnbondingDelegations(gomock.Any(), adrs).Return(resp.UnbondingDelegations, nil) }, }, - { - "error on denomtrace function returns unverified balance", - tracelistener.BalanceRow{ - Address: "address", - Amount: "42", - Denom: "ibc/hash", - }, - map[string]bool{ - "uatom": true, - }, - func(_ context.Context, _, hash string) (tracelistener.IBCDenomTraceRow, error) { - return tracelistener.IBCDenomTraceRow{}, fmt.Errorf("error") - }, - Balance{ - Address: "address", - BaseDenom: "ibc/hash", - Verified: false, - Amount: "42", - OnChain: "", - Ibc: IbcInfo{ - Hash: "hash", - }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "rawaddress", Value: "xxx"}} + c.Request, _ = http.NewRequestWithContext(ctx, http.MethodGet, "", nil) + // add logger or else it fails + logger := logging.New(logging.LoggingConfig{}) + c.Set(logging.LoggerKey, logger) + ac := newAccountAPI(t, tt.setup) + + ac.GetAccounts(c) + + assert.Equal(tt.expectedStatusCode, w.Code) + if tt.expectedError != "" { + require.Len(c.Errors, 1, "expected one error but got %d", len(c.Errors)) + require.EqualError(c.Errors[0], tt.expectedError) + return + } + require.Empty(c.Errors) + assert.JSONEq(tt.expectedBody, w.Body.String()) + }) + } +} + +func TestGetBalancesPerAddress(t *testing.T) { + var ( + ctx = context.Background() + resp = account.BalancesResponse{ + Balances: []account.Balance{ + {Address: "adr1", BaseDenom: "denom1", Amount: "42"}, + {Address: "adr2", BaseDenom: "denom2", Amount: "42"}, }, - }, + } + respJSON, _ = json.Marshal(resp) + ) + tests := []struct { + name string + expectedStatusCode int + expectedBody string + expectedError string + setup func(mocks) + }{ { - "verified non-ibc token returns verified balance", - tracelistener.BalanceRow{ - Address: "address", - Amount: "42", - Denom: "denom", - }, - map[string]bool{ - "denom": true, - }, - func(_ context.Context, _, hash string) (tracelistener.IBCDenomTraceRow, error) { - return tracelistener.IBCDenomTraceRow{}, nil - }, - Balance{ - Address: "address", - BaseDenom: "denom", - Verified: true, - Amount: "42", + name: "ok", + expectedStatusCode: http.StatusOK, + expectedBody: string(respJSON), + + setup: func(m mocks) { + m.app.EXPECT().Balances(ctx, []string{"adr1"}).Return(resp.Balances, nil) }, }, - { - "non-verified non-ibc token returns non-verified balance", - tracelistener.BalanceRow{ - Address: "address", - Amount: "42", - Denom: "denom", + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "address", Value: "adr1"}} + c.Request, _ = http.NewRequestWithContext(ctx, http.MethodGet, "", nil) + // add logger or else it fails + logger := logging.New(logging.LoggingConfig{}) + c.Set(logging.LoggerKey, logger) + ac := newAccountAPI(t, tt.setup) + + ac.GetBalancesByAddress(c) + + assert.Equal(tt.expectedStatusCode, w.Code) + if tt.expectedError != "" { + require.Len(c.Errors, 1, "expected one error but got %d", len(c.Errors)) + require.EqualError(c.Errors[0], tt.expectedError) + return + } + require.Empty(c.Errors) + assert.JSONEq(tt.expectedBody, w.Body.String()) + }) + } + +} + +func TestGetDelegationsPerAddress(t *testing.T) { + var ( + ctx = context.Background() + resp = account.StakingBalancesResponse{ + StakingBalances: []account.StakingBalance{ + {ValidatorAddress: "adr1", ChainName: "chain1", Amount: "42"}, + {ValidatorAddress: "adr2", ChainName: "chain1", Amount: "42"}, }, - map[string]bool{ - "denom": false, + } + respJSON, _ = json.Marshal(resp) + ) + tests := []struct { + name string + expectedStatusCode int + expectedBody string + expectedError string + setup func(mocks) + }{ + { + name: "ok", + expectedStatusCode: http.StatusOK, + expectedBody: string(respJSON), + + setup: func(m mocks) { + m.app.EXPECT().StakingBalances(ctx, []string{"adr1"}).Return(resp.StakingBalances, nil) }, - func(_ context.Context, _, hash string) (tracelistener.IBCDenomTraceRow, error) { - return tracelistener.IBCDenomTraceRow{}, nil + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "address", Value: "adr1"}} + c.Request, _ = http.NewRequestWithContext(ctx, http.MethodGet, "", nil) + // FIXME remove when #801 is done + fflag.EnableGlobal(account.FixSlashedDelegations) + // add logger or else it fails + logger := logging.New(logging.LoggingConfig{}) + c.Set(logging.LoggerKey, logger) + ac := newAccountAPI(t, tt.setup) + + ac.GetDelegationsByAddress(c) + + assert.Equal(tt.expectedStatusCode, w.Code) + if tt.expectedError != "" { + require.Len(c.Errors, 1, "expected one error but got %d", len(c.Errors)) + require.EqualError(c.Errors[0], tt.expectedError) + return + } + require.Empty(c.Errors) + assert.JSONEq(tt.expectedBody, w.Body.String()) + }) + } +} + +func TestGetUnbondingDelegationsPerAddress(t *testing.T) { + var ( + ctx = context.Background() + resp = account.UnbondingDelegationsResponse{ + UnbondingDelegations: []account.UnbondingDelegation{ + {ValidatorAddress: "adr1", ChainName: "chain1"}, + {ValidatorAddress: "adr2", ChainName: "chain1"}, }, - Balance{ - Address: "address", - BaseDenom: "denom", - Verified: false, - Amount: "42", + } + respJSON, _ = json.Marshal(resp) + ) + tests := []struct { + name string + expectedStatusCode int + expectedBody string + expectedError string + setup func(mocks) + }{ + { + name: "ok", + expectedStatusCode: http.StatusOK, + expectedBody: string(respJSON), + + setup: func(m mocks) { + m.app.EXPECT().UnbondingDelegations(ctx, []string{"adr1"}).Return(resp.UnbondingDelegations, nil) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - require.Equal(t, - tt.want, - balanceRespForBalance(context.Background(), tt.rawBalance, tt.vd, tt.dt), - ) + require := require.New(t) + assert := assert.New(t) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "address", Value: "adr1"}} + c.Request, _ = http.NewRequestWithContext(ctx, http.MethodGet, "", nil) + // add logger or else it fails + logger := logging.New(logging.LoggingConfig{}) + c.Set(logging.LoggerKey, logger) + ac := newAccountAPI(t, tt.setup) + + ac.GetUnbondingDelegationsByAddress(c) + + assert.Equal(tt.expectedStatusCode, w.Code) + if tt.expectedError != "" { + require.Len(c.Errors, 1, "expected one error but got %d", len(c.Errors)) + require.EqualError(c.Errors[0], tt.expectedError) + return + } + require.Empty(c.Errors) + assert.JSONEq(tt.expectedBody, w.Body.String()) }) } } diff --git a/api/account/models.go b/api/account/models.go index 68f557fd..3332509f 100644 --- a/api/account/models.go +++ b/api/account/models.go @@ -4,6 +4,12 @@ import ( "github.com/emerishq/demeris-backend-models/tracelistener" ) +type AccountsResponse struct { + Balances []Balance `json:"balances"` + StakingBalances []StakingBalance `json:"staking_balances"` + UnbondingDelegations []UnbondingDelegation `json:"unbonding_delegations"` +} + type BalancesResponse struct { Balances []Balance `json:"balances"` } diff --git a/api/account/ports.go b/api/account/ports.go new file mode 100644 index 00000000..5f166fa7 --- /dev/null +++ b/api/account/ports.go @@ -0,0 +1,14 @@ +package account + +import ( + "context" +) + +//go:generate mockgen -package account_test -source ports.go -destination ports_mocks_test.go + +type App interface { + DeriveRawAddress(ctx context.Context, rawAddress string) ([]string, error) + Balances(ctx context.Context, addresses []string) ([]Balance, error) + UnbondingDelegations(ctx context.Context, addresses []string) ([]UnbondingDelegation, error) + StakingBalances(ctx context.Context, addresses []string) ([]StakingBalance, error) +} diff --git a/api/account/ports_mocks_test.go b/api/account/ports_mocks_test.go new file mode 100644 index 00000000..a10fc3cb --- /dev/null +++ b/api/account/ports_mocks_test.go @@ -0,0 +1,96 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ports.go + +// Package account_test is a generated GoMock package. +package account_test + +import ( + context "context" + reflect "reflect" + + account "github.com/emerishq/demeris-api-server/api/account" + gomock "github.com/golang/mock/gomock" +) + +// MockApp is a mock of App interface. +type MockApp struct { + ctrl *gomock.Controller + recorder *MockAppMockRecorder +} + +// MockAppMockRecorder is the mock recorder for MockApp. +type MockAppMockRecorder struct { + mock *MockApp +} + +// NewMockApp creates a new mock instance. +func NewMockApp(ctrl *gomock.Controller) *MockApp { + mock := &MockApp{ctrl: ctrl} + mock.recorder = &MockAppMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockApp) EXPECT() *MockAppMockRecorder { + return m.recorder +} + +// Balances mocks base method. +func (m *MockApp) Balances(ctx context.Context, addresses []string) ([]account.Balance, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Balances", ctx, addresses) + ret0, _ := ret[0].([]account.Balance) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Balances indicates an expected call of Balances. +func (mr *MockAppMockRecorder) Balances(ctx, addresses interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Balances", reflect.TypeOf((*MockApp)(nil).Balances), ctx, addresses) +} + +// DeriveRawAddress mocks base method. +func (m *MockApp) DeriveRawAddress(ctx context.Context, rawAddress string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeriveRawAddress", ctx, rawAddress) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeriveRawAddress indicates an expected call of DeriveRawAddress. +func (mr *MockAppMockRecorder) DeriveRawAddress(ctx, rawAddress interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeriveRawAddress", reflect.TypeOf((*MockApp)(nil).DeriveRawAddress), ctx, rawAddress) +} + +// StakingBalances mocks base method. +func (m *MockApp) StakingBalances(ctx context.Context, addresses []string) ([]account.StakingBalance, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StakingBalances", ctx, addresses) + ret0, _ := ret[0].([]account.StakingBalance) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StakingBalances indicates an expected call of StakingBalances. +func (mr *MockAppMockRecorder) StakingBalances(ctx, addresses interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StakingBalances", reflect.TypeOf((*MockApp)(nil).StakingBalances), ctx, addresses) +} + +// UnbondingDelegations mocks base method. +func (m *MockApp) UnbondingDelegations(ctx context.Context, addresses []string) ([]account.UnbondingDelegation, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UnbondingDelegations", ctx, addresses) + ret0, _ := ret[0].([]account.UnbondingDelegation) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UnbondingDelegations indicates an expected call of UnbondingDelegations. +func (mr *MockAppMockRecorder) UnbondingDelegations(ctx, addresses interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnbondingDelegations", reflect.TypeOf((*MockApp)(nil).UnbondingDelegations), ctx, addresses) +} diff --git a/api/chains/chains_test.go b/api/chains/chains_test.go index 878f492d..85789758 100644 --- a/api/chains/chains_test.go +++ b/api/chains/chains_test.go @@ -111,8 +111,8 @@ func TestGetChain(t *testing.T) { } func TestGetChains(t *testing.T) { - utils.RunTraceListnerMigrations(testingCtx, t) - utils.InsertTraceListnerData(testingCtx, t, utils.VerifyTraceData) + utils.RunTraceListenerMigrations(testingCtx, t) + utils.InsertTraceListenerData(testingCtx, t, utils.VerifyTraceData) for _, tt := range getChainsTestCases { t.Run(tt.name, func(t *testing.T) { @@ -152,11 +152,11 @@ func TestGetChains(t *testing.T) { } func TestVerifyTrace(t *testing.T) { - utils.RunTraceListnerMigrations(testingCtx, t) + utils.RunTraceListenerMigrations(testingCtx, t) for i, tt := range verifyTraceTestCases { t.Run(fmt.Sprintf("%d %s", i, tt.name), func(t *testing.T) { - utils.InsertTraceListnerData(testingCtx, t, tt.dataStruct) + utils.InsertTraceListenerData(testingCtx, t, tt.dataStruct) for _, chain := range tt.chains { require.NoError(t, testingCtx.CnsDB.AddChain(chain)) } @@ -194,8 +194,8 @@ func TestVerifyTrace(t *testing.T) { } func TestGetChainStatus(t *testing.T) { - utils.RunTraceListnerMigrations(testingCtx, t) - utils.InsertTraceListnerData(testingCtx, t, utils.VerifyTraceData) + utils.RunTraceListenerMigrations(testingCtx, t) + utils.InsertTraceListenerData(testingCtx, t, utils.VerifyTraceData) tests := []struct { name string @@ -321,8 +321,8 @@ func TestGetChainSupply(t *testing.T) { } func TestGetChainsStatuses(t *testing.T) { - utils.RunTraceListnerMigrations(testingCtx, t) - utils.InsertTraceListnerData(testingCtx, t, utils.VerifyTraceData) + utils.RunTraceListenerMigrations(testingCtx, t) + utils.InsertTraceListenerData(testingCtx, t, utils.VerifyTraceData) // arrange testChains := []cns.Chain{ diff --git a/api/database/balances.go b/api/database/balances.go index 826e5b1c..25e6c89f 100644 --- a/api/database/balances.go +++ b/api/database/balances.go @@ -5,9 +5,10 @@ import ( "github.com/emerishq/demeris-backend-models/tracelistener" "github.com/getsentry/sentry-go" + "github.com/jmoiron/sqlx" ) -func (d *Database) Balances(ctx context.Context, address string) ([]tracelistener.BalanceRow, error) { +func (d *Database) Balances(ctx context.Context, addresses []string) ([]tracelistener.BalanceRow, error) { defer sentry.StartSpan(ctx, "db.Balances").Finish() var balances []tracelistener.BalanceRow @@ -22,14 +23,15 @@ func (d *Database) Balances(ctx context.Context, address string) ([]tracelistene amount, denom FROM tracelistener.balances - WHERE address=? + WHERE address IN (?) AND chain_name IN ( SELECT chain_name FROM cns.chains WHERE enabled=true ) - AND delete_height IS NULL - ` - + AND delete_height IS NULL` + q, args, err := sqlx.In(q, addresses) + if err != nil { + return nil, err + } q = d.dbi.DB.Rebind(q) - - return balances, d.dbi.DB.SelectContext(ctx, &balances, q, address) + return balances, d.dbi.DB.SelectContext(ctx, &balances, q, args...) } diff --git a/api/database/balances_test.go b/api/database/balances_test.go new file mode 100644 index 00000000..2cd374bb --- /dev/null +++ b/api/database/balances_test.go @@ -0,0 +1,66 @@ +package database_test + +import ( + "context" + + utils "github.com/emerishq/demeris-api-server/api/test_utils" + "github.com/emerishq/demeris-backend-models/tracelistener" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func (s *TestSuite) TestBalances() { + t := s.T() + ctx := context.Background() + require := require.New(t) + assert := assert.New(t) + err := s.ctx.CnsDB.AddChain(utils.ChainWithoutPublicEndpoints) + require.NoError(err) + utils.RunTraceListenerMigrations(s.ctx, t) + utils.InsertTraceListenerData(s.ctx, t, utils.TracelistenerData{ + Balances: []tracelistener.BalanceRow{ + { + TracelistenerDatabaseRow: tracelistener.TracelistenerDatabaseRow{ + ChainName: "chain1", Height: 1024, + }, + Address: "adr1", Amount: "42", Denom: "denom1", + }, + { + TracelistenerDatabaseRow: tracelistener.TracelistenerDatabaseRow{ + ChainName: "chain1", Height: 1024, + }, + Address: "adr2", Amount: "42", Denom: "denom2", + }, + }, + }) + + // case 1: one address + bs, err := s.ctx.Router.DB.Balances(ctx, []string{"adr1"}) + + require.NoError(err) + if assert.Len(bs, 1) { + assert.Equal("chain1", bs[0].ChainName) + assert.Equal("denom1", bs[0].Denom) + assert.Equal("adr1", bs[0].Address) + assert.Equal("42", bs[0].Amount) + assert.EqualValues(1024, bs[0].Height) + } + + // case 2: multiple addresses + bs, err = s.ctx.Router.DB.Balances(ctx, []string{"adr1", "adr2"}) + + require.NoError(err) + if assert.Len(bs, 2) { + assert.Equal("chain1", bs[0].ChainName) + assert.Equal("denom1", bs[0].Denom) + assert.Equal("adr1", bs[0].Address) + assert.Equal("42", bs[0].Amount) + assert.EqualValues(1024, bs[0].Height) + + assert.Equal("chain1", bs[1].ChainName) + assert.Equal("denom2", bs[1].Denom) + assert.Equal("adr2", bs[1].Address) + assert.Equal("42", bs[1].Amount) + assert.EqualValues(1024, bs[1].Height) + } +} diff --git a/api/database/delegations.go b/api/database/delegations.go index 56117610..27556e2c 100644 --- a/api/database/delegations.go +++ b/api/database/delegations.go @@ -16,7 +16,7 @@ type DelegationResponse struct { ValidatorShares string `db:"delegator_shares" json:"delegator_shares"` } -func (d *Database) Delegations(ctx context.Context, address string) ([]DelegationResponse, error) { +func (d *Database) Delegations(ctx context.Context, addresses []string) ([]DelegationResponse, error) { defer sentry.StartSpan(ctx, "db.Delegations").Finish() var delegations []DelegationResponse @@ -26,19 +26,17 @@ func (d *Database) Delegations(ctx context.Context, address string) ([]Delegatio FROM tracelistener.delegations as d INNER JOIN tracelistener.validators as v ON d.chain_name=v.chain_name AND d.validator_address=v.validator_address - WHERE d.delegator_address=(?) + WHERE d.delegator_address IN (?) AND d.chain_name IN ( SELECT chain_name FROM cns.chains WHERE enabled=true ) AND v.delete_height IS NULL AND d.delete_height IS NULL - `, []string{address}) + `, addresses) if err != nil { return nil, err } - q = d.dbi.DB.Rebind(q) - return delegations, d.dbi.DB.SelectContext(ctx, &delegations, q, args...) } diff --git a/api/database/delegations_test.go b/api/database/delegations_test.go new file mode 100644 index 00000000..c6647192 --- /dev/null +++ b/api/database/delegations_test.go @@ -0,0 +1,78 @@ +package database_test + +import ( + "context" + + utils "github.com/emerishq/demeris-api-server/api/test_utils" + "github.com/emerishq/demeris-backend-models/tracelistener" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func (s *TestSuite) TestDelegations() { + t := s.T() + ctx := context.Background() + require := require.New(t) + assert := assert.New(t) + err := s.ctx.CnsDB.AddChain(utils.ChainWithoutPublicEndpoints) + require.NoError(err) + utils.RunTraceListenerMigrations(s.ctx, t) + utils.InsertTraceListenerData(s.ctx, t, utils.TracelistenerData{ + Validators: []tracelistener.ValidatorRow{ + { + TracelistenerDatabaseRow: tracelistener.TracelistenerDatabaseRow{ + ChainName: "chain1", Height: 1024, + }, + OperatorAddress: "opadr", ValidatorAddress: "vadr", + DelegatorShares: "1000", Tokens: "10000", + }, + }, + Delegations: []tracelistener.DelegationRow{ + { + TracelistenerDatabaseRow: tracelistener.TracelistenerDatabaseRow{ + ChainName: "chain1", Height: 1024, + }, + Delegator: "adr1", Validator: "vadr", Amount: "42", + }, + { + TracelistenerDatabaseRow: tracelistener.TracelistenerDatabaseRow{ + ChainName: "chain1", Height: 1024, + }, + Delegator: "adr2", Validator: "vadr", Amount: "42", + }, + }, + }) + + // case 1: one address + bs, err := s.ctx.Router.DB.Delegations(ctx, []string{"adr1"}) + + require.NoError(err) + if assert.Len(bs, 1) { + assert.Equal("chain1", bs[0].ChainName) + assert.Equal("adr1", bs[0].Delegator) + assert.Equal("vadr", bs[0].Validator) + assert.Equal("42", bs[0].Amount) + assert.Equal("1000", bs[0].ValidatorShares) + assert.Equal("10000", bs[0].ValidatorTokens) + } + + // case 2: multiple addresses + bs, err = s.ctx.Router.DB.Delegations(ctx, []string{"adr1", "adr2"}) + + require.NoError(err) + if assert.Len(bs, 2) { + assert.Equal("chain1", bs[0].ChainName) + assert.Equal("adr1", bs[0].Delegator) + assert.Equal("vadr", bs[0].Validator) + assert.Equal("42", bs[0].Amount) + assert.Equal("1000", bs[0].ValidatorShares) + assert.Equal("10000", bs[0].ValidatorTokens) + + assert.Equal("chain1", bs[1].ChainName) + assert.Equal("adr2", bs[1].Delegator) + assert.Equal("vadr", bs[1].Validator) + assert.Equal("42", bs[1].Amount) + assert.Equal("1000", bs[1].ValidatorShares) + assert.Equal("10000", bs[1].ValidatorTokens) + } +} diff --git a/api/database/init_test.go b/api/database/init_test.go index 1b9fee6a..38df84c2 100644 --- a/api/database/init_test.go +++ b/api/database/init_test.go @@ -21,8 +21,8 @@ func (s *TestSuite) SetupSuite() { s.ctx.CnsDB.AddChain(utils.ChainWithoutPublicEndpoints) s.ctx.CnsDB.AddChain(utils.DisabledChain) - utils.RunTraceListnerMigrations(s.ctx, s.T()) - utils.InsertTraceListnerData(s.ctx, s.T(), utils.VerifyTraceData) + utils.RunTraceListenerMigrations(s.ctx, s.T()) + utils.InsertTraceListenerData(s.ctx, s.T(), utils.VerifyTraceData) } func (s *TestSuite) TearDownSuite() { diff --git a/api/database/unbonding_delegations.go b/api/database/unbonding_delegations.go index 153cd87b..811db594 100644 --- a/api/database/unbonding_delegations.go +++ b/api/database/unbonding_delegations.go @@ -8,7 +8,7 @@ import ( "github.com/jmoiron/sqlx" ) -func (d *Database) UnbondingDelegations(ctx context.Context, address string) ([]tracelistener.UnbondingDelegationRow, error) { +func (d *Database) UnbondingDelegations(ctx context.Context, addresses []string) ([]tracelistener.UnbondingDelegationRow, error) { defer sentry.StartSpan(ctx, "db.UnbondingDelegations").Finish() var unbondingDelegations []tracelistener.UnbondingDelegationRow @@ -28,7 +28,7 @@ func (d *Database) UnbondingDelegations(ctx context.Context, address string) ([] SELECT chain_name FROM cns.chains WHERE enabled=true ) AND delete_height IS NULL - `, []string{address}) + `, addresses) if err != nil { return nil, err } diff --git a/api/database/unbonding_delegations_test.go b/api/database/unbonding_delegations_test.go new file mode 100644 index 00000000..ea8582a2 --- /dev/null +++ b/api/database/unbonding_delegations_test.go @@ -0,0 +1,63 @@ +package database_test + +import ( + "context" + + utils "github.com/emerishq/demeris-api-server/api/test_utils" + "github.com/emerishq/demeris-backend-models/tracelistener" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func (s *TestSuite) TestUnbondingDelegations() { + t := s.T() + ctx := context.Background() + require := require.New(t) + assert := assert.New(t) + err := s.ctx.CnsDB.AddChain(utils.ChainWithoutPublicEndpoints) + require.NoError(err) + utils.RunTraceListenerMigrations(s.ctx, t) + utils.InsertTraceListenerData(s.ctx, t, utils.TracelistenerData{ + UnbondingDelegations: []tracelistener.UnbondingDelegationRow{ + { + TracelistenerDatabaseRow: tracelistener.TracelistenerDatabaseRow{ + ChainName: "chain1", Height: 1024, + }, + Delegator: "dadr1", Validator: "vadr1", + }, + { + TracelistenerDatabaseRow: tracelistener.TracelistenerDatabaseRow{ + ChainName: "chain1", Height: 1024, + }, + Delegator: "dadr2", Validator: "vadr2", + }, + }, + }) + + // case 1: one address + bs, err := s.ctx.Router.DB.UnbondingDelegations(ctx, []string{"dadr1"}) + + require.NoError(err) + if assert.Len(bs, 1) { + assert.Equal("chain1", bs[0].ChainName) + assert.Equal("dadr1", bs[0].Delegator) + assert.Equal("vadr1", bs[0].Validator) + assert.EqualValues(1024, bs[0].Height) + } + + // case 2: multiple addresses + bs, err = s.ctx.Router.DB.UnbondingDelegations(ctx, []string{"dadr1", "dadr2"}) + + require.NoError(err) + if assert.Len(bs, 2) { + assert.Equal("chain1", bs[0].ChainName) + assert.Equal("dadr1", bs[0].Delegator) + assert.Equal("vadr1", bs[0].Validator) + assert.EqualValues(1024, bs[0].Height) + + assert.Equal("chain1", bs[1].ChainName) + assert.Equal("dadr2", bs[1].Delegator) + assert.Equal("vadr2", bs[1].Validator) + assert.EqualValues(1024, bs[1].Height) + } +} diff --git a/api/relayer/api.go b/api/relayer/api.go index 772c949f..45561f5d 100644 --- a/api/relayer/api.go +++ b/api/relayer/api.go @@ -212,7 +212,7 @@ func enoughBalance(ctx context.Context, address string, denom cnsmodels.Denom, d addrHex := hex.EncodeToString(hb) - balance, err := db.Balances(ctx, addrHex) + balance, err := db.Balances(ctx, []string{addrHex}) if err != nil { return false, err } diff --git a/api/router/router.go b/api/router/router.go index 839a51aa..33113be8 100644 --- a/api/router/router.go +++ b/api/router/router.go @@ -136,7 +136,7 @@ func registerRoutes(engine *gin.Engine, db *database.Database, s *store.Store, app *usecase.App) { // @tag.name Account // @tag.description Account-querying endpoints - account.Register(engine, db, s, sdkServiceClients) + account.Register(engine, db, s, sdkServiceClients, app) // @tag.name Denoms // @tag.description Denoms-related endpoints diff --git a/api/test_utils/utils.go b/api/test_utils/utils.go index d879fb2e..5a5bc5f5 100644 --- a/api/test_utils/utils.go +++ b/api/test_utils/utils.go @@ -8,6 +8,7 @@ import ( "time" "github.com/emerishq/demeris-backend-models/cns" + "github.com/emerishq/demeris-backend-models/tracelistener" "github.com/emerishq/emeris-utils/store" "github.com/stretchr/testify/require" @@ -94,11 +95,80 @@ const ( unique(chain_name) )` + createBalancesTable = `CREATE TABLE IF NOT EXISTS tracelistener.balances ( + id serial PRIMARY KEY NOT NULL, + height integer NOT NULL, + delete_height integer, + chain_name text NOT NULL, + address text NOT NULL, + amount text NOT NULL, + denom text NOT NULL, + UNIQUE (chain_name, address, denom) + )` + createDelegationsTable = `CREATE TABLE IF NOT EXISTS tracelistener.delegations ( + id serial PRIMARY KEY NOT NULL, + height integer NOT NULL, + delete_height integer, + chain_name text NOT NULL, + delegator_address text NOT NULL, + validator_address text NOT NULL, + amount text NOT NULL, + UNIQUE (chain_name, delegator_address, validator_address) + )` + createUnbondingDelegationsTable = `CREATE TABLE IF NOT EXISTS tracelistener.unbonding_delegations ( +id serial PRIMARY KEY NOT NULL, +height integer NOT NULL, +delete_height integer, +chain_name text NOT NULL, +delegator_address text NOT NULL, +validator_address text NOT NULL, +entries jsonb NOT NULL, +UNIQUE (chain_name, delegator_address, validator_address) +)` + createValidatorsTable = `CREATE TABLE IF NOT EXISTS tracelistener.validators( + id serial PRIMARY KEY NOT NULL, + height integer NOT NULL, + delete_height integer, + chain_name text NOT NULL, + validator_address text NOT NULL, + operator_address text NOT NULL, + consensus_pubkey_type text, + consensus_pubkey_value bytes, + jailed bool NOT NULL, + status integer NOT NULL, + tokens text NOT NULL, + delegator_shares text NOT NULL, + moniker text, + identity text, + website text, + security_contact text, + details text, + unbonding_height bigint, + unbonding_time text, + commission_rate text NOT NULL, + max_rate text NOT NULL, + max_change_rate text NOT NULL, + update_time text NOT NULL, + min_self_delegation text NOT NULL, + UNIQUE (chain_name, operator_address) + )` + insertDenomTrace = "INSERT INTO tracelistener.denom_traces (path, base_denom, hash, chain_name) VALUES (($1), ($2), ($3), ($4)) ON CONFLICT (chain_name, hash) DO UPDATE SET base_denom=($2), hash=($3), path=($1)" insertChannel = "INSERT INTO tracelistener.channels (channel_id, counter_channel_id, port, state, hops, chain_name) VALUES (($1), ($2), ($3), ($4), ($5), ($6)) ON CONFLICT (chain_name, channel_id, port) DO UPDATE SET state=($4),counter_channel_id=($2),hops=($5),port=($3),channel_id=($1)" insertConnection = "INSERT INTO tracelistener.connections (chain_name, connection_id, client_id, state, counter_connection_id, counter_client_id) VALUES (($1), ($2), ($3), ($4), ($5), ($6)) ON CONFLICT (chain_name, connection_id, client_id) DO UPDATE SET chain_name=($1),state=($4),counter_connection_id=($5),counter_client_id=($6)" insertClient = "INSERT INTO tracelistener.clients (chain_name, chain_id, client_id, latest_height, trusting_period) VALUES (($1), ($2), ($3), ($4), ($5)) ON CONFLICT (chain_name, chain_id, client_id) DO UPDATE SET chain_id=($2),client_id=($3),latest_height=($4),trusting_period=($5)" insertBlocktime = "INSERT INTO tracelistener.blocktime (chain_name, block_time) VALUES (($1), ($2)) ON CONFLICT (chain_name) DO UPDATE SET chain_name=($1),block_time=($2);" + insertBalance = `INSERT INTO tracelistener.balances +(height, chain_name, address, amount, denom) +VALUES (:height, :chain_name, :address, :amount, :denom)` + insertDelegation = `INSERT INTO tracelistener.delegations +(height, chain_name, delegator_address, validator_address, amount) +VALUES (:height, :chain_name, :delegator_address, :validator_address, :amount)` + insertUnbondingDelegation = `INSERT INTO tracelistener.unbonding_delegations +(height, chain_name, delegator_address, validator_address, entries) +VALUES (:height, :chain_name, :delegator_address, :validator_address, :entries)` + insertValidator = `INSERT INTO tracelistener.validators (height, chain_name, validator_address, operator_address, consensus_pubkey_type, consensus_pubkey_value, jailed, status, tokens, delegator_shares, moniker, identity, website, security_contact, details, unbonding_height, unbonding_time, commission_rate, max_rate, max_change_rate, update_time, min_self_delegation) + VALUES (:height, :chain_name, :validator_address, :operator_address, :consensus_pubkey_type, :consensus_pubkey_value, :jailed, :status, :tokens, :delegator_shares, :moniker, :identity, :website, :security_contact, :details, :unbonding_height, :unbonding_time, :commission_rate, :max_rate, :max_change_rate, :update_time, :min_self_delegation)` truncateDenomTraces = `TRUNCATE tracelistener.denom_traces` truncateChannels = `TRUNCATE tracelistener.channels` @@ -114,6 +184,10 @@ var migrations = []string{ createConnectionsTable, createClientsTable, createBlockTimeTable, + createBalancesTable, + createDelegationsTable, + createUnbondingDelegationsTable, + createValidatorsTable, } var truncating = []string{ @@ -125,11 +199,15 @@ var truncating = []string{ } type TracelistenerData struct { - Denoms []DenomTrace - Channels []Channel - Connections []Connection - Clients []Client - BlockTimes []BlockTime + Denoms []DenomTrace + Channels []Channel + Connections []Connection + Clients []Client + BlockTimes []BlockTime + Balances []tracelistener.BalanceRow + Delegations []tracelistener.DelegationRow + UnbondingDelegations []tracelistener.UnbondingDelegationRow + Validators []tracelistener.ValidatorRow } type DenomTrace struct { @@ -267,7 +345,7 @@ func Setup(runServer bool) *TestingCtx { } // Creates tracelistner database and required tables only if they dont exist -func RunTraceListnerMigrations(ctx *TestingCtx, t *testing.T) { +func RunTraceListenerMigrations(ctx *TestingCtx, t *testing.T) { for _, m := range migrations { _, err := ctx.CnsDB.Instance.DB.Exec(m) require.NoError(t, err) @@ -294,7 +372,7 @@ func insertRow(ctx *TestingCtx, t *testing.T, query string, args ...interface{}) } // inserts data from given struct into respective tracelistener tables -func InsertTraceListnerData(ctx *TestingCtx, t *testing.T, data TracelistenerData) { +func InsertTraceListenerData(ctx *TestingCtx, t *testing.T, data TracelistenerData) { for _, d := range data.Denoms { insertRow(ctx, t, insertDenomTrace, d.Path, d.BaseDenom, d.Hash, d.ChainName) } @@ -314,6 +392,22 @@ func InsertTraceListnerData(ctx *TestingCtx, t *testing.T, data TracelistenerDat for _, d := range data.BlockTimes { insertRow(ctx, t, insertBlocktime, d.ChainName, d.Time) } + for _, d := range data.Balances { + _, err := ctx.CnsDB.Instance.DB.NamedExec(insertBalance, d) + require.NoError(t, err) + } + for _, d := range data.Delegations { + _, err := ctx.CnsDB.Instance.DB.NamedExec(insertDelegation, d) + require.NoError(t, err) + } + for _, d := range data.UnbondingDelegations { + _, err := ctx.CnsDB.Instance.DB.NamedExec(insertUnbondingDelegation, d) + require.NoError(t, err) + } + for _, d := range data.Validators { + _, err := ctx.CnsDB.Instance.DB.NamedExec(insertValidator, d) + require.NoError(t, err) + } } // TruncateDB Empties the CNS DB of data. diff --git a/cmd/api-server/main.go b/cmd/api-server/main.go index e1fe4790..5f2a49d4 100644 --- a/cmd/api-server/main.go +++ b/cmd/api-server/main.go @@ -108,7 +108,7 @@ func main() { l.Panicw("cannot initialize sdk-service clients", "error", err) } - app := usecase.NewApp(sdkServiceClients) + app := usecase.NewApp(dbi, sdkServiceClients) r := router.New( dbi, diff --git a/go.mod b/go.mod index 50fc8257..d798b9d7 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/allinbits/starport-operator v0.0.1-alpha.45 github.com/cockroachdb/cockroach-go/v2 v2.2.8 github.com/cosmos/cosmos-sdk v0.45.3 - github.com/emerishq/demeris-backend-models v1.5.0 + github.com/emerishq/demeris-backend-models v1.7.2 github.com/emerishq/emeris-cns-server v0.0.0-20220422070001-a18e063b6374 github.com/emerishq/emeris-price-oracle v1.0.0 github.com/emerishq/emeris-utils v1.9.0 @@ -22,14 +22,14 @@ require ( github.com/getsentry/sentry-go v0.13.0 github.com/gin-contrib/zap v0.0.2 github.com/gin-gonic/gin v1.7.7 - github.com/go-playground/validator/v10 v10.10.1 + github.com/go-playground/validator/v10 v10.11.0 github.com/go-redis/redis/v8 v8.11.4 github.com/gofrs/uuid v4.2.0+incompatible github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.7 github.com/gravity-devs/liquidity v1.5.0 github.com/jmoiron/sqlx v1.3.3 - github.com/lib/pq v1.10.4 + github.com/lib/pq v1.10.6 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/stretchr/testify v1.7.1 github.com/swaggo/swag v1.8.0 @@ -58,7 +58,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect github.com/btcsuite/btcd v0.22.0-beta // indirect - github.com/btcsuite/btcd/btcec/v2 v2.1.2 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/confio/ics23/go v0.7.0 // indirect @@ -78,7 +78,7 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac // indirect github.com/dvsekhvalnov/jose2go v0.0.0-20200901110807-248326c1351b // indirect - github.com/ethereum/go-ethereum v1.10.17 // indirect + github.com/ethereum/go-ethereum v1.10.18 // indirect github.com/evanphx/json-patch v4.11.0+incompatible // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/ghodss/yaml v1.0.0 // indirect diff --git a/go.sum b/go.sum index 56c8ac58..80716ac7 100644 --- a/go.sum +++ b/go.sum @@ -253,9 +253,9 @@ github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13P github.com/btcsuite/btcd v0.21.0-beta/go.mod h1:ZSWyehm27aAuS9bvkATT+Xte3hjHZ+MRgMY/8NJ7K94= github.com/btcsuite/btcd v0.22.0-beta h1:LTDpDKUM5EeOFBPM8IXpinEcmZ6FWfNZbE3lfrfdnWo= github.com/btcsuite/btcd v0.22.0-beta/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA= -github.com/btcsuite/btcd/btcec/v2 v2.1.2 h1:YoYoC9J0jwfukodSBMzZYUVQ8PTiYg4BnOWiJVzTmLs= -github.com/btcsuite/btcd/btcec/v2 v2.1.2/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= +github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/btcutil v0.0.0-20190207003914-4c204d697803/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= @@ -431,6 +431,7 @@ github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4Kfc github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v1.6.2/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= @@ -439,6 +440,7 @@ github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dop251/goja v0.0.0-20200721192441-a695b0cdd498/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA= github.com/dop251/goja v0.0.0-20211011172007-d99e4b8cbf48/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20220405120441-9037c2b61cbf/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -455,8 +457,8 @@ github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c/go.mod h1:YO35OhQPt github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emerishq/demeris-backend-models v1.5.0 h1:PBldMVGK+jswfmKrtHZB7K6HI0SvjwDlEfPxPoH3azo= -github.com/emerishq/demeris-backend-models v1.5.0/go.mod h1:1+h5uREsxHEWUbfdowE10imcluyW/Z0IgCpnihDJTyQ= +github.com/emerishq/demeris-backend-models v1.7.2 h1:q0rGow3+2nk7qa+ZKOjOvooopQbrAa9ZKoSNJig4NJw= +github.com/emerishq/demeris-backend-models v1.7.2/go.mod h1:bpeyV7DYugEJA1LNwTva7dLvsF4v4HADkHi4lxCiEvo= github.com/emerishq/emeris-cns-server v0.0.0-20220422070001-a18e063b6374 h1:e+pWkWBIi3NJLFBYWsMPq1yr1sbUMseSIO+Qr4fWRiE= github.com/emerishq/emeris-cns-server v0.0.0-20220422070001-a18e063b6374/go.mod h1:BYciyChqW2Odw8LHvACNznByJtI3D9xOuKwQyyFz1rA= github.com/emerishq/emeris-price-oracle v1.0.0 h1:e8l1fyvcZfW0uqoafl8vEwVt8ogxvGfgMu8ebdnE1mU= @@ -477,8 +479,8 @@ github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= github.com/ethereum/go-ethereum v1.9.25/go.mod h1:vMkFiYLHI4tgPw4k2j4MHKoovchFE8plZ0M9VMk4/oM= github.com/ethereum/go-ethereum v1.10.16/go.mod h1:Anj6cxczl+AHy63o4X9O8yWNHuN5wMpfb8MAnHkWn7Y= -github.com/ethereum/go-ethereum v1.10.17 h1:XEcumY+qSr1cZQaWsQs5Kck3FHB0V2RiMHPdTBJ+oT8= -github.com/ethereum/go-ethereum v1.10.17/go.mod h1:Lt5WzjM07XlXc95YzrhosmR4J9Ahd6X2wyEV2SvGhk0= +github.com/ethereum/go-ethereum v1.10.18 h1:hLEd5M+UD0GJWPaROiYMRgZXl6bi5YwoTJSthsx5CZw= +github.com/ethereum/go-ethereum v1.10.18/go.mod h1:RD3NhcSBjZpj3k+SnQq24wBrmnmie78P5R/P62iNBD8= github.com/evanphx/json-patch v0.0.0-20200808040245-162e5629780b/go.mod h1:NAJj0yf/KaRKURN6nyi7A9IZydMivZEm9oQLWNjfKDc= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -502,6 +504,7 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fjl/gencodec v0.0.0-20220412091415-8bb9e558978c/go.mod h1:AzA8Lj6YtixmJWL+wkKoBGsLWy9gFrAzi4g+5bCKwpY= github.com/fjl/memsize v0.0.0-20180418122429-ca190fb6ffbc/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= @@ -517,6 +520,7 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61/go.mod h1:Q0X6pkwTILDlzrGEckF6HKjXe48EgsY/l7K7vhY4MW8= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/gertd/go-pluralize v0.1.7/go.mod h1:O4eNeeIf91MHh1GJ2I47DNtaesm66NYvjYgAahcqSDQ= @@ -639,8 +643,8 @@ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= -github.com/go-playground/validator/v10 v10.10.1 h1:uA0+amWMiglNZKZ9FJRKUAe9U3RX91eVn1JYXMWt7ig= -github.com/go-playground/validator/v10 v10.10.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw= +github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= github.com/go-sourcemap/sourcemap v2.1.2+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= @@ -927,7 +931,7 @@ github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmK github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo= github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc= github.com/huin/goupnp v1.0.2/go.mod h1:0dxJBVBHqTMjIUMkESDTNgOOx/Mw5wYIfyFmdzSamkM= -github.com/huin/goupnp v1.0.3-0.20220313090229-ca81a64b4204/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= +github.com/huin/goupnp v1.0.3/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= github.com/iamolegga/enviper v1.4.0 h1:EmJiySDhv20KjCtkCADcsC3BUKwta+E983qcGF2DuK0= github.com/iamolegga/enviper v1.4.0/go.mod h1:zfAP/NiI+JhN+sy3r6edrNSyppFGTNQxaeYJ8kjQmsk= @@ -1120,6 +1124,7 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v0.0.0-20170224010052-a616ab194758/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= @@ -1136,8 +1141,9 @@ github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= +github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOSqcmlfs= github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= @@ -1879,6 +1885,7 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= +golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -1908,6 +1915,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -2222,6 +2231,7 @@ golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191126055441-b0650ceb63d9/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191224055732-dd894d0a8a40/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -2269,6 +2279,7 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/usecase/address.go b/usecase/address.go new file mode 100644 index 00000000..a06f35ae --- /dev/null +++ b/usecase/address.go @@ -0,0 +1,41 @@ +package usecase + +import ( + "context" + "encoding/hex" + "fmt" + + "github.com/cosmos/cosmos-sdk/types/bech32" + "github.com/getsentry/sentry-go" +) + +// DeriveRawAddress returns the chain addresses from rawAddress for all enabled +// chains. +func (a *App) DeriveRawAddress(ctx context.Context, rawAddress string) ([]string, error) { + defer sentry.StartSpan(ctx, "usecase.DeriveRawAddress").Finish() + + if rawAddress == "" { + return nil, fmt.Errorf("raw address is empty") + } + bz, err := hex.DecodeString(rawAddress) + if err != nil { + return nil, fmt.Errorf("raw address is not in hex format: %w", err) + } + + chains, err := a.db.Chains(ctx) + if err != nil { + return nil, err + } + addrs := make([]string, len(chains)) + for i, ch := range chains { + // Get chain address bech 32 human readable part (aka prefix or tag) + // FIXME(tb): MainPrefix or PrefixAccount or ? + hrp := ch.NodeInfo.Bech32Config.MainPrefix + addr, err := bech32.ConvertAndEncode(hrp, bz) + if err != nil { + return nil, err + } + addrs[i] = addr + } + return addrs, nil +} diff --git a/usecase/address_test.go b/usecase/address_test.go new file mode 100644 index 00000000..ff9430b6 --- /dev/null +++ b/usecase/address_test.go @@ -0,0 +1,83 @@ +package usecase_test + +import ( + "context" + "testing" + + "github.com/emerishq/demeris-backend-models/cns" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDeriveRawAddress(t *testing.T) { + ctx := context.Background() + tests := []struct { + name string + rawAddress string + expectedError string + expectedAddresses []string + + setup func(mocks) + }{ + { + name: "fail: empty raw address", + rawAddress: "", + expectedError: "raw address is empty", + }, + { + name: "fail: raw address is not in hex format", + rawAddress: "-", + expectedError: "raw address is not in hex format: encoding/hex: invalid byte: U+002D '-'", + }, + { + name: "ok: no chain enabled", + rawAddress: "abc123", + expectedAddresses: []string{}, + + setup: func(m mocks) { + m.db.EXPECT().Chains(ctx).Return([]cns.Chain{}, nil) + }, + }, + { + name: "ok", + rawAddress: "abc123", + expectedAddresses: []string{ + "pre1140qjx2fqawy", + "pre2140qjxcavcur", + }, + + setup: func(m mocks) { + m.db.EXPECT().Chains(ctx).Return([]cns.Chain{ + { + ChainName: "chain1", + NodeInfo: cns.NodeInfo{ + Bech32Config: cns.Bech32Config{MainPrefix: "pre1"}, + }, + }, + { + ChainName: "chain2", + NodeInfo: cns.NodeInfo{ + Bech32Config: cns.Bech32Config{MainPrefix: "pre2"}, + }, + }, + }, + nil, + ) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + app := newApp(t, tt.setup) + + adrs, err := app.DeriveRawAddress(ctx, tt.rawAddress) + + if tt.expectedError != "" { + require.EqualError(t, err, tt.expectedError) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expectedAddresses, adrs) + }) + } +} diff --git a/usecase/balance.go b/usecase/balance.go new file mode 100644 index 00000000..eb207443 --- /dev/null +++ b/usecase/balance.go @@ -0,0 +1,143 @@ +package usecase + +import ( + "context" + "fmt" + "strings" + + "github.com/emerishq/demeris-api-server/api/account" + "github.com/emerishq/emeris-utils/exported/sdktypes" + "github.com/getsentry/sentry-go" +) + +func (a *App) Balances(ctx context.Context, addresses []string) ([]account.Balance, error) { + defer sentry.StartSpan(ctx, "usecase.Balances").Finish() + + if len(addresses) == 0 { + return []account.Balance{}, nil + } + balances, err := a.db.Balances(ctx, addresses) + if err != nil { + return nil, err + } + if len(balances) == 0 { + return []account.Balance{}, nil + } + verifiedDenoms, err := a.verifiedDenomsMap(ctx) + if err != nil { + return nil, err + } + // TODO: get unique chains + // perhaps we can remove this since there will be another endpoint specifically for fee tokens + res := make([]account.Balance, 0, len(balances)) + for _, b := range balances { + balance := account.Balance{ + Address: b.Address, + Amount: b.Amount, + OnChain: b.ChainName, + Verified: verifiedDenoms[b.Denom], + BaseDenom: b.Denom, + } + + if strings.HasPrefix(b.Denom, "ibc/") { + // is ibc token + balance.Ibc = account.IbcInfo{ + Hash: b.Denom[4:], + } + // if err is nil, the ibc denom has a denom trace associated with it + // so we return it, along with its verified status as well as the complete ibc + // path + // otherwise, since we don't touch `verified` and `baseDenom` variables, we stick to the + // original `ibc/...` denom, which will be unverified by default + denomTrace, err := a.db.DenomTrace(ctx, b.ChainName, b.Denom[4:]) + if err == nil { + balance.Ibc.Path = denomTrace.Path + balance.BaseDenom = denomTrace.BaseDenom + balance.Verified = verifiedDenoms[denomTrace.BaseDenom] + } + } + + res = append(res, balance) + } + return res, nil +} + +// StakingBalance returns the staking balances of addresses. +func (a *App) StakingBalances(ctx context.Context, addresses []string) ([]account.StakingBalance, error) { + defer sentry.StartSpan(ctx, "usecase.StakingBalances").Finish() + + if len(addresses) == 0 { + return []account.StakingBalance{}, nil + } + + delegations, err := a.db.Delegations(ctx, addresses) + if err != nil { + return nil, err + } + if len(delegations) == 0 { + return []account.StakingBalance{}, nil + } + + res := make([]account.StakingBalance, 0, len(delegations)) + for _, del := range delegations { + delegationAmount, err := sdktypes.NewDecFromStr(del.Amount) + if err != nil { + return nil, fmt.Errorf("cannot convert delegation amount to Dec: %w", err) + } + + validatorShares, err := sdktypes.NewDecFromStr(del.ValidatorShares) + if err != nil { + return nil, fmt.Errorf("cannot convert validator total shares to Dec: %w", err) + } + + validatorTokens, err := sdktypes.NewDecFromStr(del.ValidatorTokens) + if err != nil { + return nil, fmt.Errorf("cannot convert validator total tokens to Dec: %w", err) + } + + // apply shares * total_validator_balance / total_validator_shares + balance := delegationAmount.Mul(validatorTokens).Quo(validatorShares) + res = append(res, account.StakingBalance{ + ValidatorAddress: del.Validator, + Amount: balance.String(), + ChainName: del.ChainName, + }) + } + return res, nil +} + +func (a *App) UnbondingDelegations(ctx context.Context, addresses []string) ([]account.UnbondingDelegation, error) { + defer sentry.StartSpan(ctx, "usecase.UnbondingDelegations").Finish() + + if len(addresses) == 0 { + return []account.UnbondingDelegation{}, nil + } + + unbondings, err := a.db.UnbondingDelegations(ctx, addresses) + if err != nil { + return nil, err + } + res := make([]account.UnbondingDelegation, len(unbondings)) + for i, unbonding := range unbondings { + res[i] = account.UnbondingDelegation{ + ValidatorAddress: unbonding.Validator, + Entries: unbonding.Entries, + ChainName: unbonding.ChainName, + } + } + return res, nil +} + +func (a *App) verifiedDenomsMap(ctx context.Context) (map[string]bool, error) { + chains, err := a.db.VerifiedDenoms(ctx) + if err != nil { + return nil, err + } + ret := make(map[string]bool) + for _, cc := range chains { + for _, vd := range cc { + ret[vd.Name] = vd.Verified + } + } + return ret, nil +} diff --git a/usecase/balance_test.go b/usecase/balance_test.go new file mode 100644 index 00000000..02fd1e60 --- /dev/null +++ b/usecase/balance_test.go @@ -0,0 +1,371 @@ +package usecase_test + +import ( + "context" + "errors" + "testing" + + "github.com/emerishq/demeris-api-server/api/account" + "github.com/emerishq/demeris-api-server/api/database" + "github.com/emerishq/demeris-backend-models/cns" + "github.com/emerishq/demeris-backend-models/tracelistener" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBalances(t *testing.T) { + ctx := context.Background() + tests := []struct { + name string + addresses []string + expectedError string + expectedBalances []account.Balance + setup func(mocks) + }{ + { + name: "ok: empty addresses", + expectedBalances: []account.Balance{}, + }, + { + name: "ok: balances not found", + addresses: []string{"adr1"}, + expectedBalances: []account.Balance{}, + + setup: func(m mocks) { + m.db.EXPECT().Balances(ctx, []string{"adr1"}).Return(nil, nil) + }, + }, + { + name: "ok: denom unverified", + addresses: []string{"adr1"}, + expectedBalances: []account.Balance{ + { + Address: "adr1", + BaseDenom: "denom1", + Amount: "42", + }, + }, + + setup: func(m mocks) { + m.db.EXPECT().Balances(ctx, []string{"adr1"}).Return( + []tracelistener.BalanceRow{ + {Address: "adr1", Denom: "denom1", Amount: "42"}, + }, + nil, + ) + m.db.EXPECT().VerifiedDenoms(ctx).Return(map[string]cns.DenomList{}, nil) + }, + }, + { + name: "ok: denom verified", + addresses: []string{"adr1"}, + expectedBalances: []account.Balance{ + { + Address: "adr1", + BaseDenom: "denom1", + Amount: "42", + Verified: true, + }, + }, + + setup: func(m mocks) { + m.db.EXPECT().Balances(ctx, []string{"adr1"}).Return( + []tracelistener.BalanceRow{ + {Address: "adr1", Denom: "denom1", Amount: "42"}, + }, + nil, + ) + m.db.EXPECT().VerifiedDenoms(ctx).Return(map[string]cns.DenomList{ + "xxx": { + {Name: "denom1", Verified: true}, + }, + }, nil) + }, + }, + { + name: "ok: unverified ibc denom from chain2", + addresses: []string{"adr1"}, + expectedBalances: []account.Balance{ + { + Address: "adr1", + BaseDenom: "denom2", + Amount: "42", + OnChain: "chain2", + Ibc: account.IbcInfo{ + Path: "path", + Hash: "xxx", + }, + }, + }, + + setup: func(m mocks) { + m.db.EXPECT().Balances(ctx, []string{"adr1"}).Return( + []tracelistener.BalanceRow{ + { + TracelistenerDatabaseRow: tracelistener.TracelistenerDatabaseRow{ + ChainName: "chain2", + }, + Address: "adr1", + Denom: "ibc/xxx", + Amount: "42", + }, + }, + nil, + ) + m.db.EXPECT().VerifiedDenoms(ctx).Return(map[string]cns.DenomList{ + "xxx": { + {Name: "denom1", Verified: true}, + {Name: "denom2", Verified: false}, + }}, nil) + m.db.EXPECT().DenomTrace(ctx, "chain2", "xxx").Return( + tracelistener.IBCDenomTraceRow{ + BaseDenom: "denom2", + Path: "path", + }, + nil, + ) + }, + }, + { + name: "ok: verified ibc denom from chain2", + addresses: []string{"adr1"}, + expectedBalances: []account.Balance{ + { + Address: "adr1", + BaseDenom: "denom2", + Amount: "42", + OnChain: "chain2", + Verified: true, + Ibc: account.IbcInfo{ + Path: "path", + Hash: "xxx", + }, + }, + }, + + setup: func(m mocks) { + m.db.EXPECT().Balances(ctx, []string{"adr1"}).Return( + []tracelistener.BalanceRow{ + { + TracelistenerDatabaseRow: tracelistener.TracelistenerDatabaseRow{ + ChainName: "chain2", + }, + Address: "adr1", + Denom: "ibc/xxx", + Amount: "42", + }, + }, + nil, + ) + m.db.EXPECT().VerifiedDenoms(ctx).Return(map[string]cns.DenomList{ + "xxx": { + {Name: "denom2", Verified: true}, + }}, nil) + m.db.EXPECT().DenomTrace(ctx, "chain2", "xxx").Return( + tracelistener.IBCDenomTraceRow{ + BaseDenom: "denom2", + Path: "path", + }, + nil, + ) + }, + }, + { + name: "ok: DenomTrace returns an error", + addresses: []string{"adr1"}, + expectedBalances: []account.Balance{ + { + Address: "adr1", + BaseDenom: "ibc/xxx", + Amount: "42", + OnChain: "chain2", + Ibc: account.IbcInfo{ + Path: "", + Hash: "xxx", + }, + }, + }, + + setup: func(m mocks) { + m.db.EXPECT().Balances(ctx, []string{"adr1"}).Return( + []tracelistener.BalanceRow{ + { + TracelistenerDatabaseRow: tracelistener.TracelistenerDatabaseRow{ + ChainName: "chain2", + }, + Address: "adr1", + Denom: "ibc/xxx", + Amount: "42", + }, + }, + nil, + ) + m.db.EXPECT().VerifiedDenoms(ctx).Return(map[string]cns.DenomList{ + "xxx": { + {Name: "denom1", Verified: true}, + {Name: "denom2", Verified: true}, + }}, nil) + m.db.EXPECT().DenomTrace(ctx, "chain2", "xxx"). + Return(tracelistener.IBCDenomTraceRow{}, errors.New("oups")) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + app := newApp(t, tt.setup) + + balances, err := app.Balances(ctx, tt.addresses) + + if tt.expectedError != "" { + require.EqualError(t, err, tt.expectedError) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expectedBalances, balances) + }) + } +} + +func TestStakingBalances(t *testing.T) { + ctx := context.Background() + tests := []struct { + name string + addresses []string + expectedError string + expectedStakingBalances []account.StakingBalance + setup func(mocks) + }{ + { + name: "ok: empty addresses", + expectedStakingBalances: []account.StakingBalance{}, + }, + { + name: "ok: delegations not found", + addresses: []string{"adr1"}, + expectedStakingBalances: []account.StakingBalance{}, + + setup: func(m mocks) { + m.db.EXPECT().Delegations(ctx, []string{"adr1"}).Return(nil, nil) + }, + }, + { + name: "ok", + addresses: []string{"adr1"}, + expectedStakingBalances: []account.StakingBalance{ + {ChainName: "chain1", Amount: "84.000000000000000000"}, + }, + + setup: func(m mocks) { + m.db.EXPECT().Delegations(ctx, []string{"adr1"}).Return( + []database.DelegationResponse{ + { + DelegationRow: tracelistener.DelegationRow{ + TracelistenerDatabaseRow: tracelistener.TracelistenerDatabaseRow{ + ChainName: "chain1", + }, + Amount: "42", + }, + ValidatorTokens: "10000", + ValidatorShares: "5000", + }, + }, + nil, + ) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + app := newApp(t, tt.setup) + + stakingBalances, err := app.StakingBalances(ctx, tt.addresses) + + if tt.expectedError != "" { + require.EqualError(t, err, tt.expectedError) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expectedStakingBalances, stakingBalances) + }) + } +} + +func TestUnbondingDelegations(t *testing.T) { + ctx := context.Background() + tests := []struct { + name string + addresses []string + expectedError string + expectedUnbondingDelegation []account.UnbondingDelegation + setup func(mocks) + }{ + { + name: "ok: empty addresses", + expectedUnbondingDelegation: []account.UnbondingDelegation{}, + }, + { + name: "ok: delegations not found", + addresses: []string{"adr1"}, + expectedUnbondingDelegation: []account.UnbondingDelegation{}, + + setup: func(m mocks) { + m.db.EXPECT().UnbondingDelegations(ctx, []string{"adr1"}).Return(nil, nil) + }, + }, + { + name: "ok", + addresses: []string{"adr1"}, + expectedUnbondingDelegation: []account.UnbondingDelegation{ + { + ChainName: "chain1", + ValidatorAddress: "vadr1", + Entries: []tracelistener.UnbondingDelegationEntry{ + { + Balance: "42", + InitialBalance: "1", + CreationHeight: 1024, + CompletionTime: "time", + }, + }, + }, + }, + + setup: func(m mocks) { + m.db.EXPECT().UnbondingDelegations(ctx, []string{"adr1"}).Return( + []tracelistener.UnbondingDelegationRow{ + { + TracelistenerDatabaseRow: tracelistener.TracelistenerDatabaseRow{ + ChainName: "chain1", + }, + Validator: "vadr1", + Delegator: "dadr1", + Entries: []tracelistener.UnbondingDelegationEntry{ + { + Balance: "42", + InitialBalance: "1", + CreationHeight: 1024, + CompletionTime: "time", + }, + }, + }, + }, + nil, + ) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + app := newApp(t, tt.setup) + + unbondingDelegations, err := app.UnbondingDelegations(ctx, tt.addresses) + + if tt.expectedError != "" { + require.EqualError(t, err, tt.expectedError) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expectedUnbondingDelegation, unbondingDelegations) + }) + } +} diff --git a/usecase/ports.go b/usecase/ports.go index 9816c432..768bfc55 100644 --- a/usecase/ports.go +++ b/usecase/ports.go @@ -3,11 +3,23 @@ package usecase import ( "context" + "github.com/emerishq/demeris-api-server/api/database" + "github.com/emerishq/demeris-backend-models/cns" + "github.com/emerishq/demeris-backend-models/tracelistener" sdkutilities "github.com/emerishq/sdk-service-meta/gen/sdk_utilities" ) //go:generate mockgen -package usecase_test -source ports.go -destination ports_mocks_test.go +type DB interface { + Chains(ctx context.Context) ([]cns.Chain, error) + Balances(ctx context.Context, addresses []string) ([]tracelistener.BalanceRow, error) + Delegations(ctx context.Context, addresses []string) ([]database.DelegationResponse, error) + VerifiedDenoms(context.Context) (map[string]cns.DenomList, error) + DenomTrace(ctx context.Context, chain string, hash string) (tracelistener.IBCDenomTraceRow, error) + UnbondingDelegations(ctx context.Context, addresses []string) ([]tracelistener.UnbondingDelegationRow, error) +} + type SDKServiceClients interface { GetSDKServiceClient(version string) (sdkutilities.Service, error) } diff --git a/usecase/ports_mocks_test.go b/usecase/ports_mocks_test.go index fe20b607..1e4becc8 100644 --- a/usecase/ports_mocks_test.go +++ b/usecase/ports_mocks_test.go @@ -8,10 +8,126 @@ import ( context "context" reflect "reflect" + database "github.com/emerishq/demeris-api-server/api/database" + cns "github.com/emerishq/demeris-backend-models/cns" + tracelistener "github.com/emerishq/demeris-backend-models/tracelistener" sdkutilities "github.com/emerishq/sdk-service-meta/gen/sdk_utilities" gomock "github.com/golang/mock/gomock" ) +// MockDB is a mock of DB interface. +type MockDB struct { + ctrl *gomock.Controller + recorder *MockDBMockRecorder +} + +// MockDBMockRecorder is the mock recorder for MockDB. +type MockDBMockRecorder struct { + mock *MockDB +} + +// NewMockDB creates a new mock instance. +func NewMockDB(ctrl *gomock.Controller) *MockDB { + mock := &MockDB{ctrl: ctrl} + mock.recorder = &MockDBMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDB) EXPECT() *MockDBMockRecorder { + return m.recorder +} + +// Balances mocks base method. +func (m *MockDB) Balances(ctx context.Context, addresses []string) ([]tracelistener.BalanceRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Balances", ctx, addresses) + ret0, _ := ret[0].([]tracelistener.BalanceRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Balances indicates an expected call of Balances. +func (mr *MockDBMockRecorder) Balances(ctx, addresses interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Balances", reflect.TypeOf((*MockDB)(nil).Balances), ctx, addresses) +} + +// Chains mocks base method. +func (m *MockDB) Chains(ctx context.Context) ([]cns.Chain, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Chains", ctx) + ret0, _ := ret[0].([]cns.Chain) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Chains indicates an expected call of Chains. +func (mr *MockDBMockRecorder) Chains(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Chains", reflect.TypeOf((*MockDB)(nil).Chains), ctx) +} + +// Delegations mocks base method. +func (m *MockDB) Delegations(ctx context.Context, addresses []string) ([]database.DelegationResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delegations", ctx, addresses) + ret0, _ := ret[0].([]database.DelegationResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delegations indicates an expected call of Delegations. +func (mr *MockDBMockRecorder) Delegations(ctx, addresses interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delegations", reflect.TypeOf((*MockDB)(nil).Delegations), ctx, addresses) +} + +// DenomTrace mocks base method. +func (m *MockDB) DenomTrace(ctx context.Context, chain, hash string) (tracelistener.IBCDenomTraceRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DenomTrace", ctx, chain, hash) + ret0, _ := ret[0].(tracelistener.IBCDenomTraceRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DenomTrace indicates an expected call of DenomTrace. +func (mr *MockDBMockRecorder) DenomTrace(ctx, chain, hash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DenomTrace", reflect.TypeOf((*MockDB)(nil).DenomTrace), ctx, chain, hash) +} + +// UnbondingDelegations mocks base method. +func (m *MockDB) UnbondingDelegations(ctx context.Context, addresses []string) ([]tracelistener.UnbondingDelegationRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UnbondingDelegations", ctx, addresses) + ret0, _ := ret[0].([]tracelistener.UnbondingDelegationRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UnbondingDelegations indicates an expected call of UnbondingDelegations. +func (mr *MockDBMockRecorder) UnbondingDelegations(ctx, addresses interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnbondingDelegations", reflect.TypeOf((*MockDB)(nil).UnbondingDelegations), ctx, addresses) +} + +// VerifiedDenoms mocks base method. +func (m *MockDB) VerifiedDenoms(arg0 context.Context) (map[string]cns.DenomList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "VerifiedDenoms", arg0) + ret0, _ := ret[0].(map[string]cns.DenomList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// VerifiedDenoms indicates an expected call of VerifiedDenoms. +func (mr *MockDBMockRecorder) VerifiedDenoms(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifiedDenoms", reflect.TypeOf((*MockDB)(nil).VerifiedDenoms), arg0) +} + // MockSDKServiceClients is a mock of SDKServiceClients interface. type MockSDKServiceClients struct { ctrl *gomock.Controller diff --git a/usecase/usecase.go b/usecase/usecase.go index 93114f0c..a3898f67 100644 --- a/usecase/usecase.go +++ b/usecase/usecase.go @@ -6,11 +6,13 @@ const ( ) type App struct { + db DB sdkServiceClients SDKServiceClients } -func NewApp(sdk SDKServiceClients) *App { +func NewApp(db DB, sdk SDKServiceClients) *App { return &App{ + db: db, sdkServiceClients: sdk, } } diff --git a/usecase/usecase_test.go b/usecase/usecase_test.go index dcde5acb..959890b8 100644 --- a/usecase/usecase_test.go +++ b/usecase/usecase_test.go @@ -10,6 +10,7 @@ import ( type mocks struct { t *testing.T + db *MockDB sdkServiceClients *MockSDKServiceClients sdkServiceClient *MockSDKServiceClient } @@ -18,6 +19,7 @@ func newApp(t *testing.T, setup func(mocks)) *usecase.App { ctrl := gomock.NewController(t) m := mocks{ t: t, + db: NewMockDB(ctrl), sdkServiceClients: NewMockSDKServiceClients(ctrl), sdkServiceClient: NewMockSDKServiceClient(ctrl), } @@ -33,5 +35,5 @@ func newApp(t *testing.T, setup func(mocks)) *usecase.App { if setup != nil { setup(m) } - return usecase.NewApp(m.sdkServiceClients) + return usecase.NewApp(m.db, m.sdkServiceClients) }