From e7560f64caeb02ae01b9db68c9f95abaa65aac97 Mon Sep 17 00:00:00 2001 From: Sean McGary Date: Thu, 23 Jan 2025 16:15:05 -0600 Subject: [PATCH] feat: GetSummarizedRewardsForEarner rpc --- go.mod | 4 +- go.sum | 28 +- pkg/rpcServer/rewardsHandlers.go | 30 +- pkg/service/rewardsDataService/rewards.go | 338 +++++++++++++++++++++- 4 files changed, 361 insertions(+), 39 deletions(-) diff --git a/go.mod b/go.mod index b19e7d27..bc50c19c 100644 --- a/go.mod +++ b/go.mod @@ -6,12 +6,13 @@ require ( github.com/DataDog/datadog-go/v5 v5.5.0 github.com/Layr-Labs/eigenlayer-contracts v0.4.1-holesky-pepe.0.20240813143901-00fc4b95e9c1 github.com/Layr-Labs/eigenlayer-rewards-proofs v0.2.13 - github.com/Layr-Labs/protocol-apis v1.1.1-0.20250122201559-ae5f5b65ce44 + github.com/Layr-Labs/protocol-apis v1.1.1-0.20250122221613-65afff84af27 github.com/ethereum/go-ethereum v1.14.9 github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 + github.com/habx/pg-commands v0.6.1 github.com/lib/pq v1.10.9 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.12.0 @@ -53,7 +54,6 @@ require ( github.com/go-ole/go-ole v1.3.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/gorilla/websocket v1.4.2 // indirect - github.com/habx/pg-commands v0.6.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/holiman/uint256 v1.3.1 // indirect github.com/iden3/go-iden3-crypto v0.0.16 // indirect diff --git a/go.sum b/go.sum index 31106f70..14edafe4 100644 --- a/go.sum +++ b/go.sum @@ -41,32 +41,8 @@ github.com/Layr-Labs/eigenlayer-contracts v0.4.1-holesky-pepe.0.20240813143901-0 github.com/Layr-Labs/eigenlayer-contracts v0.4.1-holesky-pepe.0.20240813143901-00fc4b95e9c1/go.mod h1:Ie8YE3EQkTHqG6/tnUS0He7/UPMkXPo/3OFXwSy0iRo= github.com/Layr-Labs/eigenlayer-rewards-proofs v0.2.13 h1:Blb4AE+jC/vddV71w4/MQAPooM+8EVqv9w2bL4OytgY= github.com/Layr-Labs/eigenlayer-rewards-proofs v0.2.13/go.mod h1:PD/HoyzZjxDw1tAcZw3yD0yGddo+yhmwQAi+lk298r4= -github.com/Layr-Labs/protocol-apis v1.1.0 h1:PO6x+Y9ORiac2dkaWJayRFqhyzcvMbvRQkDIpLTNtVc= -github.com/Layr-Labs/protocol-apis v1.1.0/go.mod h1:prNA2/mLO5vpMZ2q78Nsn0m97wm28uiRnwO+/yOxigk= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250110201222-e8670ac00c32 h1:nRHAH0dn5qkQXUjdrPlGThtKLt154UKAHfzCEyMqfr0= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250110201222-e8670ac00c32/go.mod h1:prNA2/mLO5vpMZ2q78Nsn0m97wm28uiRnwO+/yOxigk= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250110201843-c2f2cf37e910 h1:X3t1mr1kAOGDJ3paPS/lzHJchK4y+oWSJ+D/7+MgOmY= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250110201843-c2f2cf37e910/go.mod h1:prNA2/mLO5vpMZ2q78Nsn0m97wm28uiRnwO+/yOxigk= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250114180833-6f2487a7e08c h1:kcTwHJVRDQAGqVacRJ4h6r6LKIZP6nBkxaBEJvZ9A3Q= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250114180833-6f2487a7e08c/go.mod h1:prNA2/mLO5vpMZ2q78Nsn0m97wm28uiRnwO+/yOxigk= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250114181701-acb87ef4eeb5 h1:0PLxb8fpwdpWpfk24yhdZzETFCxVMN2yJjRDyBBf6wM= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250114181701-acb87ef4eeb5/go.mod h1:prNA2/mLO5vpMZ2q78Nsn0m97wm28uiRnwO+/yOxigk= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250115220323-135176acb92b h1:eJmPAq3s+AwOrQUjSFXILCzUstDZobwYEraOZ2NFC1M= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250115220323-135176acb92b/go.mod h1:prNA2/mLO5vpMZ2q78Nsn0m97wm28uiRnwO+/yOxigk= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250115230325-93c4ebccbeb7 h1:zTOIFjJcCzOZ1PBk9jtoW/bsKSyRzvQyTG2Beutpiuk= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250115230325-93c4ebccbeb7/go.mod h1:prNA2/mLO5vpMZ2q78Nsn0m97wm28uiRnwO+/yOxigk= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250116155113-c22028af9e00 h1:7N3ta4X5YRygobnCuAAcKr4T76eUboXVcj+IGW0P2eA= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250116155113-c22028af9e00/go.mod h1:prNA2/mLO5vpMZ2q78Nsn0m97wm28uiRnwO+/yOxigk= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250116155715-919a0a9a27e5 h1:m/I5hGK4JpOE7OL7wYlKt9HwlCAgQ0SxeT3dNHfjzFY= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250116155715-919a0a9a27e5/go.mod h1:prNA2/mLO5vpMZ2q78Nsn0m97wm28uiRnwO+/yOxigk= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250121193118-8112817d1079 h1:B1b9ghilo70y4MJ7ZkF5w8BuPtslAUP2oE6vfzG6DBA= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250121193118-8112817d1079/go.mod h1:prNA2/mLO5vpMZ2q78Nsn0m97wm28uiRnwO+/yOxigk= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250122185055-8ce2cc7afa86 h1:XfVh8deohE3LYXAjYOAjzD4wRPNgbIP3OjV6Syy0FIw= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250122185055-8ce2cc7afa86/go.mod h1:prNA2/mLO5vpMZ2q78Nsn0m97wm28uiRnwO+/yOxigk= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250122190013-01d27f288892 h1:OJs2VEsSVhdrPcS+CcX6sWr2UOIYhFY473y0+QDaq/4= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250122190013-01d27f288892/go.mod h1:prNA2/mLO5vpMZ2q78Nsn0m97wm28uiRnwO+/yOxigk= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250122201559-ae5f5b65ce44 h1:jaWO3t0OvIKFBDP9zK6/mmwSeEA4P1FQKWqB5qdKjuU= -github.com/Layr-Labs/protocol-apis v1.1.1-0.20250122201559-ae5f5b65ce44/go.mod h1:prNA2/mLO5vpMZ2q78Nsn0m97wm28uiRnwO+/yOxigk= +github.com/Layr-Labs/protocol-apis v1.1.1-0.20250122221613-65afff84af27 h1:3jWR07FKc5qTmJR8VKlQFfHv26ZdnbwFi34CvtWuRRY= +github.com/Layr-Labs/protocol-apis v1.1.1-0.20250122221613-65afff84af27/go.mod h1:prNA2/mLO5vpMZ2q78Nsn0m97wm28uiRnwO+/yOxigk= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= diff --git a/pkg/rpcServer/rewardsHandlers.go b/pkg/rpcServer/rewardsHandlers.go index 2f334246..60a4653a 100644 --- a/pkg/rpcServer/rewardsHandlers.go +++ b/pkg/rpcServer/rewardsHandlers.go @@ -218,7 +218,7 @@ func (rpc *RpcServer) GetTotalClaimedRewards(ctx context.Context, req *rewardsV1 return nil, status.Error(codes.InvalidArgument, "earner address is required") } - totalClaimed, err := rpc.rewardsDataService.GetTotalClaimedRewards(ctx, earner, blockHeight) + totalClaimed, err := rpc.rewardsDataService.GetTotalClaimedRewards(ctx, earner, nil, blockHeight) if err != nil { return nil, status.Error(codes.Internal, err.Error()) } @@ -238,14 +238,36 @@ func (rpc *RpcServer) GetAvailableRewardsTokens(ctx context.Context, req *reward } func (rpc *RpcServer) GetSummarizedRewardsForEarner(ctx context.Context, req *rewardsV1.GetSummarizedRewardsForEarnerRequest) (*rewardsV1.GetSummarizedRewardsForEarnerResponse, error) { - return nil, status.Error(codes.Unimplemented, "method GetSummarizedRewardsForEarner not implemented") + earner := req.GetEarnerAddress() + blockHeight := req.GetBlockHeight() + + if earner == "" { + return nil, status.Error(codes.InvalidArgument, "earner address is required") + } + + summarizedRewards, err := rpc.rewardsDataService.GetSummarizedRewards(ctx, earner, nil, blockHeight) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &rewardsV1.GetSummarizedRewardsForEarnerResponse{ + Rewards: utils.Map(summarizedRewards, func(r *rewardsDataService.SummarizedReward, i uint64) *rewardsV1.SummarizedEarnerReward { + return &rewardsV1.SummarizedEarnerReward{ + Token: r.Token, + Earned: r.Earned, + Active: r.Active, + Claimed: r.Claimed, + Claimable: r.Claimable, + } + }), + }, nil } // GetClaimedRewardsByBlock returns the claimed rewards for an earner for a specific block. func (rpc *RpcServer) GetClaimedRewardsByBlock(ctx context.Context, req *rewardsV1.GetClaimedRewardsByBlockRequest) (*rewardsV1.GetClaimedRewardsByBlockResponse, error) { blockHeight := req.GetBlockHeight() - claims, err := rpc.rewardsDataService.ListClaimedRewardsByBlockRange(ctx, "", blockHeight, blockHeight) + claims, err := rpc.rewardsDataService.ListClaimedRewardsByBlockRange(ctx, "", blockHeight, blockHeight, nil) if err != nil { return nil, status.Error(codes.Internal, err.Error()) } @@ -272,7 +294,7 @@ func (rpc *RpcServer) ListClaimedRewardsByBlockRange(ctx context.Context, req *r return nil, status.Error(codes.InvalidArgument, "earner address is required") } - claims, err := rpc.rewardsDataService.ListClaimedRewardsByBlockRange(ctx, earner, startBlockHeight, endBlockHeight) + claims, err := rpc.rewardsDataService.ListClaimedRewardsByBlockRange(ctx, earner, startBlockHeight, endBlockHeight, nil) if err != nil { return nil, status.Error(codes.Internal, err.Error()) } diff --git a/pkg/service/rewardsDataService/rewards.go b/pkg/service/rewardsDataService/rewards.go index 93f4a951..daeeb589 100644 --- a/pkg/service/rewardsDataService/rewards.go +++ b/pkg/service/rewardsDataService/rewards.go @@ -3,14 +3,21 @@ package rewardsDataService import ( "context" "database/sql" + "errors" "fmt" "github.com/Layr-Labs/sidecar/internal/config" + eigenStateTypes "github.com/Layr-Labs/sidecar/pkg/eigenState/types" "github.com/Layr-Labs/sidecar/pkg/metaState/types" "github.com/Layr-Labs/sidecar/pkg/rewards" "github.com/Layr-Labs/sidecar/pkg/rewards/rewardsTypes" + "github.com/Layr-Labs/sidecar/pkg/rewardsUtils" "github.com/Layr-Labs/sidecar/pkg/service/baseDataService" + "github.com/Layr-Labs/sidecar/pkg/utils" "go.uber.org/zap" "gorm.io/gorm" + "reflect" + "strings" + "sync" ) type RewardsDataService struct { @@ -48,7 +55,7 @@ type TotalClaimedReward struct { Amount string } -func (rds *RewardsDataService) GetTotalClaimedRewards(ctx context.Context, earner string, blockHeight uint64) ([]*TotalClaimedReward, error) { +func (rds *RewardsDataService) GetTotalClaimedRewards(ctx context.Context, earner string, tokens []string, blockHeight uint64) ([]*TotalClaimedReward, error) { blockHeight, err := rds.BaseDataService.GetCurrentBlockHeightIfNotPresent(ctx, blockHeight) if err != nil { return nil, err @@ -59,18 +66,27 @@ func (rds *RewardsDataService) GetTotalClaimedRewards(ctx context.Context, earne earner, token, sum(claimed_amount) as amount - from erwards_claimed as rc + from rewards_claimed as rc where earner = @earner and block_number <= @blockHeight - group by 1, 2 ` + args := []interface{}{ + sql.Named("earner", earner), + sql.Named("blockHeight", blockHeight), + } + if len(tokens) > 0 { + query += " and token in (?)" + formattedTokens := utils.Map(tokens, func(token string, i uint64) string { + return strings.ToLower(token) + }) + args = append(args, sql.Named("tokens", formattedTokens)) + } + + query += " group by earner, token" claimedAmounts := make([]*TotalClaimedReward, 0) - res := rds.db.Raw(query, - sql.Named("earner", earner), - sql.Named("block_height", blockHeight), - ).Scan(&claimedAmounts) + res := rds.db.Raw(query, args...).Scan(&claimedAmounts) if res.Error != nil { return nil, res.Error @@ -86,6 +102,7 @@ func (rds *RewardsDataService) ListClaimedRewardsByBlockRange( earner string, startBlockHeight uint64, endBlockHeight uint64, + tokens []string, ) ([]*types.RewardsClaimed, error) { if endBlockHeight == 0 { return nil, fmt.Errorf("endBlockHeight must be greater than 0") @@ -118,6 +135,13 @@ func (rds *RewardsDataService) ListClaimedRewardsByBlockRange( query += " and earner = @earner" args = append(args, sql.Named("earner", earner)) } + if len(tokens) > 0 { + query += " and token in (?)" + formattedTokens := utils.Map(tokens, func(token string, i uint64) string { + return strings.ToLower(token) + }) + args = append(args, sql.Named("tokens", formattedTokens)) + } query += " order by block_number, log_index" claimedRewards := make([]*types.RewardsClaimed, 0) @@ -128,3 +152,303 @@ func (rds *RewardsDataService) ListClaimedRewardsByBlockRange( } return claimedRewards, nil } + +type RewardAmount struct { + Token string + Amount string +} + +// GetTotalRewardsForEarner returns the total earned rewards for a given earner at a given block height. +func (rds *RewardsDataService) GetTotalRewardsForEarner(earner string, tokens []string, blockHeight uint64, claimable bool) ([]*RewardAmount, error) { + if earner == "" { + return nil, fmt.Errorf("earner is required") + } + + snapshot, err := rds.findDistributionRootClosestToBlockHeight(blockHeight, claimable) + if err != nil { + return nil, err + } + + query := ` + select + token, + sum(amount) as amount + from sidecar_mainnet_ethereum.gold_table as gt + where + earner = @earner + and snapshot <= @snapshot + ` + args := []interface{}{ + sql.Named("earner", earner), + sql.Named("snapshot", snapshot.GetSnapshotDate()), + } + if len(tokens) > 0 { + query += " and token in (?)" + formattedTokens := utils.Map(tokens, func(token string, i uint64) string { + return strings.ToLower(token) + }) + args = append(args, sql.Named("tokens", formattedTokens)) + } + query += ` order by snapshot desc` + + rewardAmounts := make([]*RewardAmount, 0) + res := rds.db.Raw(query, args...).Scan(&tokens) + + if res.Error != nil { + return nil, res.Error + } + + return rewardAmounts, nil +} + +// GetClaimableRewardsForEarner returns the rewards that are claimable for a given earner at a given block height (totalActiveRewards - claimed) +func (rds *RewardsDataService) GetClaimableRewardsForEarner(earner string, tokens []string, blockHeight uint64) ([]*RewardAmount, error) { + if earner == "" { + return nil, fmt.Errorf("earner is required") + } + snapshot, err := rds.findDistributionRootClosestToBlockHeight(blockHeight, true) + if err != nil { + return nil, err + } + query := ` + with claimed_tokens as ( + select + earner, + token, + sum(claimed_amount) as amount + from rewards_claimed as rc + where + earner = @earner + and block_number <= @blockNumber + group by 1, 2 + ), + earner_tokens as ( + select + earner, + token, + sum(amount) as amount + from gold_table as gt + where + earner = @earner + and snapshot <= @snapshot + group by earner, token + ) + select + et.earner, + et.token, + et.amount::numeric as earned_amount, + coalesce(ct.amount, 0)::numeric as claimed_amount, + (coalesce(et.amount, 0) - coalesce(ct.amount, 0))::numeric as claimable + from earner_tokens as et + left join claimed_tokens as ct on ( + ct.token = et.token + and ct.earner = et.earner + ) + ` + args := []interface{}{ + sql.Named("earner", earner), + sql.Named("blockHeight", blockHeight), + sql.Named("snapshot", snapshot.GetSnapshotDate()), + } + if len(tokens) > 0 { + query += " and token in (?)" + formattedTokens := utils.Map(tokens, func(token string, i uint64) string { + return strings.ToLower(token) + }) + args = append(args, sql.Named("tokens", formattedTokens)) + } + + claimableRewards := make([]*RewardAmount, 0) + res := rds.db.Raw(query, args...).Scan(&claimableRewards) + if res.Error != nil { + return nil, res.Error + } + return claimableRewards, nil +} + +// findDistributionRootClosestToBlockHeight returns the distribution root that is closest to the provided block height +// that is also not disabled. +func (rds *RewardsDataService) findDistributionRootClosestToBlockHeight(blockHeight uint64, claimable bool) (*eigenStateTypes.SubmittedDistributionRoot, error) { + query := ` + select + * + from submitted_distribution_roots as sdr + left join disabled_distribution_roots as ddr on (sdr.root_index = ddr.root_index) + where + ddr.root_index is null + and sdr.block_number <= @blockHeight + {{ if eq .claimable "true" }} + and sdr.activated_at <= now() + {{ end }} + order by sdr.block_number desc + limit 1 + ` + + claimableStr := "false" + if claimable { + claimableStr = "true" + } + + // only render claimable since it's safe; blockHeight should be sanitized + renderedQuery, err := rewardsUtils.RenderQueryTemplate(query, map[string]interface{}{ + "claimable": claimableStr, + }) + if err != nil { + rds.logger.Sugar().Errorw("failed to render query template", + zap.Uint64("blockHeight", blockHeight), + zap.Bool("claimable", claimable), + zap.Error(err), + ) + return nil, err + } + + var root *eigenStateTypes.SubmittedDistributionRoot + res := rds.db.Raw(renderedQuery, sql.Named("blockHeight", blockHeight)).Scan(&root) + if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { + return nil, res.Error + } + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("no distribution root found for blockHeight '%d'", blockHeight) + } + return root, nil +} + +type SummarizedReward struct { + Token string + Earned string + Active string + Claimed string + Claimable string +} + +func setTokenValueInMap(tokenMap map[string]*SummarizedReward, values []*RewardAmount, fieldName string) { + for _, value := range values { + v, ok := tokenMap[value.Token] + if !ok { + v = &SummarizedReward{ + Token: value.Token, + } + tokenMap[value.Token] = v + } + f := reflect.ValueOf(v).Elem().FieldByName(fieldName) + if f.IsValid() && f.CanSet() { + f.SetString(value.Amount) + } + } +} + +// GetSummarizedRewards returns the summarized rewards for a given earner at a given block height. +// The blockHeight will be used to find the root that is <= the provided blockHeight +func (rds *RewardsDataService) GetSummarizedRewards(ctx context.Context, earner string, tokens []string, blockHeight uint64) ([]*SummarizedReward, error) { + if earner == "" { + return nil, fmt.Errorf("earner is required") + } + + blockHeight, err := rds.BaseDataService.GetCurrentBlockHeightIfNotPresent(context.Background(), blockHeight) + if err != nil { + return nil, err + } + + tokenMap := make(map[string]*SummarizedReward) + + type ChanResult[T any] struct { + Data T + Error error + } + + // channels to aggregate results together in a thread safe way + earnedRewardsChan := make(chan *ChanResult[[]*RewardAmount]) + activeRewardsChan := make(chan *ChanResult[[]*RewardAmount]) + claimableRewardsChan := make(chan *ChanResult[[]*RewardAmount]) + claimedRewardsChan := make(chan *ChanResult[[]*RewardAmount]) + var wg sync.WaitGroup + wg.Add(4) + + go func() { + defer wg.Done() + res := &ChanResult[[]*RewardAmount]{} + earnedRewards, err := rds.GetTotalRewardsForEarner(earner, tokens, blockHeight, false) + if err != nil { + res.Error = err + } else { + res.Data = earnedRewards + } + earnedRewardsChan <- res + }() + + go func() { + defer wg.Done() + res := &ChanResult[[]*RewardAmount]{} + activeRewards, err := rds.GetTotalRewardsForEarner(earner, tokens, blockHeight, true) + if err != nil { + res.Error = err + } else { + res.Data = activeRewards + } + activeRewardsChan <- res + }() + + go func() { + defer wg.Done() + res := &ChanResult[[]*RewardAmount]{} + claimableRewards, err := rds.GetClaimableRewardsForEarner(earner, tokens, blockHeight) + if err != nil { + res.Error = err + } else { + res.Data = claimableRewards + } + claimableRewardsChan <- res + }() + + go func() { + defer wg.Done() + res := &ChanResult[[]*RewardAmount]{} + claimedRewards, err := rds.GetTotalClaimedRewards(context.Background(), earner, tokens, blockHeight) + if err != nil { + res.Error = err + } else { + res.Data = utils.Map(claimedRewards, func(cr *TotalClaimedReward, i uint64) *RewardAmount { + return &RewardAmount{ + Token: cr.Token, + Amount: cr.Amount, + } + }) + } + claimedRewardsChan <- res + }() + wg.Wait() + close(earnedRewardsChan) + close(activeRewardsChan) + close(claimableRewardsChan) + close(claimedRewardsChan) + + earnedRewards := <-earnedRewardsChan + if earnedRewards.Error != nil { + return nil, earnedRewards.Error + } + setTokenValueInMap(tokenMap, earnedRewards.Data, "Earned") + + activeRewards := <-activeRewardsChan + if activeRewards.Error != nil { + return nil, activeRewards.Error + } + setTokenValueInMap(tokenMap, activeRewards.Data, "Active") + + claimableRewards := <-claimableRewardsChan + if claimableRewards.Error != nil { + return nil, claimableRewards.Error + } + setTokenValueInMap(tokenMap, claimableRewards.Data, "Claimable") + + claimedRewards := <-claimedRewardsChan + if claimedRewards.Error != nil { + return nil, claimedRewards.Error + } + setTokenValueInMap(tokenMap, claimedRewards.Data, "Claimed") + + tokenList := make([]*SummarizedReward, 0) + for _, v := range tokenMap { + tokenList = append(tokenList, v) + } + return tokenList, nil +}