From d1b30792dd9a176ea04de89f905d9ebaa3118eb1 Mon Sep 17 00:00:00 2001 From: gpabst Date: Wed, 13 Nov 2024 13:44:55 -0500 Subject: [PATCH 1/7] feat: Rewards v2 batch claiming support --- pkg/internal/common/flags/general.go | 7 ++ pkg/rewards/claim.go | 157 ++++++++++++++++++++++++--- pkg/rewards/types.go | 1 + samples/batch-claims.yaml | 6 + 4 files changed, 158 insertions(+), 13 deletions(-) create mode 100644 samples/batch-claims.yaml diff --git a/pkg/internal/common/flags/general.go b/pkg/internal/common/flags/general.go index 194421ce..b2521902 100644 --- a/pkg/internal/common/flags/general.go +++ b/pkg/internal/common/flags/general.go @@ -90,4 +90,11 @@ var ( EnvVars: []string{"EXPIRY"}, Value: 3600, } + + BatchClaimFile = cli.StringFlag{ + Name: "batchClaimFile", + Aliases: []string{"bcf"}, + Usage: "Input file for batch rewards claim", + EnvVars: []string{"BATCH_CLAIM_FILE"}, + } ) diff --git a/pkg/rewards/claim.go b/pkg/rewards/claim.go index 2e8d384d..2d34b457 100644 --- a/pkg/rewards/claim.go +++ b/pkg/rewards/claim.go @@ -7,6 +7,7 @@ import ( "fmt" "math/big" "net/http" + "os" "sort" "strings" "time" @@ -15,6 +16,8 @@ import ( "github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common/flags" "github.com/Layr-Labs/eigenlayer-cli/pkg/telemetry" "github.com/Layr-Labs/eigenlayer-cli/pkg/utils" + "github.com/wealdtech/go-merkletree/v2" + "gopkg.in/yaml.v2" contractrewardscoordinator "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/IRewardsCoordinator" @@ -74,6 +77,7 @@ func getClaimFlags() []cli.Flag { &ProofStoreBaseURLFlag, &flags.VerboseFlag, &flags.SilentFlag, + &flags.BatchClaimFile, } allFlags := append(baseFlags, flags.GetSignerFlags()...) @@ -81,7 +85,7 @@ func getClaimFlags() []cli.Flag { return allFlags } -func Claim(cCtx *cli.Context, p utils.Prompter) error { +func BatchClaim(cCtx *cli.Context, p utils.Prompter) error { ctx := cCtx.Context logger := common.GetLogger(cCtx) @@ -113,29 +117,108 @@ func Claim(cCtx *cli.Context, p utils.Prompter) error { http.DefaultClient, ) - claimDate, rootIndex, err := getClaimDistributionRoot(ctx, config.ClaimTimestamp, elReader, logger) + yamlFile, err := os.ReadFile(config.BatchClaimFile) if err != nil { - return eigenSdkUtils.WrapError("failed to get claim distribution root", err) + return eigenSdkUtils.WrapError("failed to read YAML config file", err) + } + + var claimConfigs []struct { + EarnerAddress string `yaml:"earner_address"` + TokenAddresses []string `yaml:"token_addresses"` + } + + err = yaml.Unmarshal(yamlFile, &claimConfigs) + if err != nil { + return eigenSdkUtils.WrapError("failed to parse YAML config", err) + } + + var elClaims []rewardscoordinator.IRewardsCoordinatorRewardsMerkleClaim + + for _, claimConfig := range claimConfigs { + earnerAddr := gethcommon.HexToAddress(claimConfig.EarnerAddress) + + var tokenAddrs []gethcommon.Address + for _, addr := range claimConfig.TokenAddresses { + tokenAddrs = append(tokenAddrs, gethcommon.HexToAddress(addr)) + } + + elClaim, _, _, err := claimHelper(config.ClaimTimestamp, ctx, elReader, logger, earnerAddr, df, tokenAddrs) + + elClaims = append(elClaims, *elClaim) + + if err != nil { + logger.Errorf("Failed to process claim for earner %s: %v", earnerAddr.String(), err) + continue + } + } + + if config.Broadcast { + eLWriter, err := common.GetELWriter( + config.ClaimerAddress, + config.SignerConfig, + ethClient, + elcontracts.Config{ + RewardsCoordinatorAddress: config.RewardsCoordinatorAddress, + }, + p, + config.ChainID, + logger, + ) + + if err != nil { + return eigenSdkUtils.WrapError("failed to get EL writer", err) + } + + logger.Infof("Broadcasting batch claim transaction...") + receipt, err := eLWriter.ProcessClaims(ctx, elClaims, config.RecipientAddress, true) + if err != nil { + return eigenSdkUtils.WrapError("failed to process claim", err) + } + + logger.Infof("Claim transaction submitted successfully") + common.PrintTransactionInfo(receipt.TxHash.String(), config.ChainID) + } else { + return eigenSdkUtils.WrapError("Batch claim currently only supported with broadcast flag", err) + } + return nil +} + +func claimHelper( + claimTimestamp string, + ctx context.Context, + elReader *elcontracts.ChainReader, + logger logging.Logger, + earnerAddress gethcommon.Address, + df *httpProofDataFetcher.HttpProofDataFetcher, + tokenAddresses []gethcommon.Address, +) (*rewardscoordinator.IRewardsCoordinatorRewardsMerkleClaim, *contractrewardscoordinator.IRewardsCoordinatorRewardsMerkleClaim, *merkletree.MerkleTree, error) { + claimDate, rootIndex, err := getClaimDistributionRoot(ctx, claimTimestamp, elReader, logger) + if err != nil { + return nil, nil, nil, eigenSdkUtils.WrapError("failed to get claim distribution root", err) } proofData, err := df.FetchClaimAmountsForDate(ctx, claimDate) if err != nil { - return eigenSdkUtils.WrapError("failed to fetch claim amounts for date", err) + return nil, nil, nil, eigenSdkUtils.WrapError("failed to fetch claim amounts for date", err) } - claimableTokens, present := proofData.Distribution.GetTokensForEarner(config.EarnerAddress) + claimableTokens, present := proofData.Distribution.GetTokensForEarner(earnerAddress) if !present { - return errors.New("no tokens claimable by earner") + return nil, nil, nil, errors.New("no tokens claimable by earner") } cg := claimgen.NewClaimgen(proofData.Distribution) accounts, claim, err := cg.GenerateClaimProofForEarner( - config.EarnerAddress, - getTokensToClaim(claimableTokens, config.TokenAddresses), + earnerAddress, + getTokensToClaim(claimableTokens, tokenAddresses), rootIndex, ) if err != nil { - return eigenSdkUtils.WrapError("failed to generate claim proof for earner", err) + return nil, nil, nil, eigenSdkUtils.WrapError("failed to generate claim proof for earner", err) + } + + if err != nil { + return nil, nil, nil, eigenSdkUtils.WrapError("failed to generate claim proof for earner", err) } elClaim := rewardscoordinator.IRewardsCoordinatorRewardsMerkleClaim{ @@ -154,13 +237,59 @@ func Claim(cCtx *cli.Context, p utils.Prompter) error { logger.Info("Validating claim proof...") ok, err := elReader.CheckClaim(ctx, elClaim) if err != nil { - return err + return nil, nil, nil, err } if !ok { - return errors.New("failed to validate claim") + return nil, nil, nil, errors.New("failed to validate claim") } logger.Info("Claim proof validated successfully") + return &elClaim, claim, accounts, nil +} + +func Claim(cCtx *cli.Context, p utils.Prompter) error { + ctx := cCtx.Context + logger := common.GetLogger(cCtx) + + config, err := readAndValidateClaimConfig(cCtx, logger) + if err != nil { + return eigenSdkUtils.WrapError("failed to read and validate claim config", err) + } + + if config.BatchClaimFile != "" { + return BatchClaim(cCtx, p) + } + + cCtx.App.Metadata["network"] = config.ChainID.String() + + ethClient, err := ethclient.Dial(config.RPCUrl) + if err != nil { + return eigenSdkUtils.WrapError("failed to create new eth client", err) + } + + elReader, err := elcontracts.NewReaderFromConfig( + elcontracts.Config{ + RewardsCoordinatorAddress: config.RewardsCoordinatorAddress, + }, + ethClient, logger, + ) + if err != nil { + return eigenSdkUtils.WrapError("failed to create new reader from config", err) + } + + df := httpProofDataFetcher.NewHttpProofDataFetcher( + config.ProofStoreBaseURL, + config.Environment, + config.Network, + http.DefaultClient, + ) + + elClaim, claim, accounts, err := claimHelper(config.ClaimTimestamp, ctx, elReader, logger, config.EarnerAddress, df, config.TokenAddresses) + + if err != nil { + return err + } + if config.Broadcast { eLWriter, err := common.GetELWriter( config.ClaimerAddress, @@ -179,7 +308,7 @@ func Claim(cCtx *cli.Context, p utils.Prompter) error { } logger.Infof("Broadcasting claim transaction...") - receipt, err := eLWriter.ProcessClaim(ctx, elClaim, config.RecipientAddress, true) + receipt, err := eLWriter.ProcessClaim(ctx, *elClaim, config.RecipientAddress, true) if err != nil { return eigenSdkUtils.WrapError("failed to process claim", err) } @@ -208,7 +337,7 @@ func Claim(cCtx *cli.Context, p utils.Prompter) error { noSendTxOpts.GasLimit = 150_000 } - unsignedTx, err := contractBindings.RewardsCoordinator.ProcessClaim(noSendTxOpts, elClaim, config.RecipientAddress) + unsignedTx, err := contractBindings.RewardsCoordinator.ProcessClaim(noSendTxOpts, *elClaim, config.RecipientAddress) if err != nil { return eigenSdkUtils.WrapError("failed to create unsigned tx", err) } @@ -373,6 +502,7 @@ func readAndValidateClaimConfig(cCtx *cli.Context, logger logging.Logger) (*Clai validTokenAddresses := getValidHexAddresses(splitTokenAddresses) rewardsCoordinatorAddress := cCtx.String(RewardsCoordinatorAddressFlag.Name) isSilent := cCtx.Bool(flags.SilentFlag.Name) + batchClaimFile := cCtx.String(flags.BatchClaimFile.Name) var err error if common.IsEmptyString(rewardsCoordinatorAddress) { @@ -457,6 +587,7 @@ func readAndValidateClaimConfig(cCtx *cli.Context, logger logging.Logger) (*Clai ClaimTimestamp: claimTimestamp, ClaimerAddress: claimerAddress, IsSilent: isSilent, + BatchClaimFile: batchClaimFile, }, nil } diff --git a/pkg/rewards/types.go b/pkg/rewards/types.go index 6f020515..50e88ea6 100644 --- a/pkg/rewards/types.go +++ b/pkg/rewards/types.go @@ -32,6 +32,7 @@ type ClaimConfig struct { Environment string SignerConfig *types.SignerConfig IsSilent bool + BatchClaimFile string } type SetClaimerConfig struct { diff --git a/samples/batch-claims.yaml b/samples/batch-claims.yaml new file mode 100644 index 00000000..812cb0f9 --- /dev/null +++ b/samples/batch-claims.yaml @@ -0,0 +1,6 @@ +- earner_address: "0x025246421e7247a729bbcff652c5cc1815ac6373" + token_addresses: + - "0x3B78576F7D6837500bA3De27A60c7f594934027E" +- earner_address: "0x025246421e7247a729bbcff652c5cc1815ac6373" + token_addresses: + - "0x3B78576F7D6837500bA3De27A60c7f594934027E" \ No newline at end of file From 44936d0153ba3439ea280ba0c709ad9b89ad3d8c Mon Sep 17 00:00:00 2001 From: gpabst Date: Fri, 15 Nov 2024 09:46:54 -0500 Subject: [PATCH 2/7] fix: update parameter name to batch-claim-file --- pkg/internal/common/flags/general.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/internal/common/flags/general.go b/pkg/internal/common/flags/general.go index b2521902..2ae9378c 100644 --- a/pkg/internal/common/flags/general.go +++ b/pkg/internal/common/flags/general.go @@ -92,7 +92,7 @@ var ( } BatchClaimFile = cli.StringFlag{ - Name: "batchClaimFile", + Name: "batch-claim-file", Aliases: []string{"bcf"}, Usage: "Input file for batch rewards claim", EnvVars: []string{"BATCH_CLAIM_FILE"}, From 6ea4abb7f96a3141bf34ba8f6ae5447dcabe1bc6 Mon Sep 17 00:00:00 2001 From: gpabst Date: Fri, 15 Nov 2024 10:00:49 -0500 Subject: [PATCH 3/7] fix: remove duplicated code now that its not a separate command --- pkg/rewards/claim.go | 38 +++++--------------------------------- 1 file changed, 5 insertions(+), 33 deletions(-) diff --git a/pkg/rewards/claim.go b/pkg/rewards/claim.go index 2d34b457..45f7163a 100644 --- a/pkg/rewards/claim.go +++ b/pkg/rewards/claim.go @@ -85,38 +85,10 @@ func getClaimFlags() []cli.Flag { return allFlags } -func BatchClaim(cCtx *cli.Context, p utils.Prompter) error { +func BatchClaim(cCtx *cli.Context, ethClient *ethclient.Client, elReader *elcontracts.ChainReader, df *httpProofDataFetcher.HttpProofDataFetcher, config *ClaimConfig, p utils.Prompter) error { ctx := cCtx.Context logger := common.GetLogger(cCtx) - config, err := readAndValidateClaimConfig(cCtx, logger) - if err != nil { - return eigenSdkUtils.WrapError("failed to read and validate claim config", err) - } - cCtx.App.Metadata["network"] = config.ChainID.String() - - ethClient, err := ethclient.Dial(config.RPCUrl) - if err != nil { - return eigenSdkUtils.WrapError("failed to create new eth client", err) - } - - elReader, err := elcontracts.NewReaderFromConfig( - elcontracts.Config{ - RewardsCoordinatorAddress: config.RewardsCoordinatorAddress, - }, - ethClient, logger, - ) - if err != nil { - return eigenSdkUtils.WrapError("failed to create new reader from config", err) - } - - df := httpProofDataFetcher.NewHttpProofDataFetcher( - config.ProofStoreBaseURL, - config.Environment, - config.Network, - http.DefaultClient, - ) - yamlFile, err := os.ReadFile(config.BatchClaimFile) if err != nil { return eigenSdkUtils.WrapError("failed to read YAML config file", err) @@ -256,10 +228,6 @@ func Claim(cCtx *cli.Context, p utils.Prompter) error { return eigenSdkUtils.WrapError("failed to read and validate claim config", err) } - if config.BatchClaimFile != "" { - return BatchClaim(cCtx, p) - } - cCtx.App.Metadata["network"] = config.ChainID.String() ethClient, err := ethclient.Dial(config.RPCUrl) @@ -284,6 +252,10 @@ func Claim(cCtx *cli.Context, p utils.Prompter) error { http.DefaultClient, ) + if config.BatchClaimFile != "" { + return BatchClaim(cCtx, ethClient, elReader, df, config, p) + } + elClaim, claim, accounts, err := claimHelper(config.ClaimTimestamp, ctx, elReader, logger, config.EarnerAddress, df, config.TokenAddresses) if err != nil { From ec69badf9ff38512ba7500cf0024bcc0aab675d4 Mon Sep 17 00:00:00 2001 From: gpabst Date: Fri, 15 Nov 2024 10:11:46 -0500 Subject: [PATCH 4/7] fix: remove dupe error check --- pkg/rewards/claim.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/rewards/claim.go b/pkg/rewards/claim.go index 45f7163a..a6404919 100644 --- a/pkg/rewards/claim.go +++ b/pkg/rewards/claim.go @@ -189,10 +189,6 @@ func claimHelper( return nil, nil, nil, eigenSdkUtils.WrapError("failed to generate claim proof for earner", err) } - if err != nil { - return nil, nil, nil, eigenSdkUtils.WrapError("failed to generate claim proof for earner", err) - } - elClaim := rewardscoordinator.IRewardsCoordinatorRewardsMerkleClaim{ RootIndex: claim.RootIndex, EarnerIndex: claim.EarnerIndex, From e67396055239d981c7e91721754285266bc152e3 Mon Sep 17 00:00:00 2001 From: gpabst Date: Fri, 15 Nov 2024 10:13:58 -0500 Subject: [PATCH 5/7] fix: context should always be first argument --- pkg/rewards/claim.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/rewards/claim.go b/pkg/rewards/claim.go index a6404919..15b81769 100644 --- a/pkg/rewards/claim.go +++ b/pkg/rewards/claim.go @@ -114,7 +114,7 @@ func BatchClaim(cCtx *cli.Context, ethClient *ethclient.Client, elReader *elcont tokenAddrs = append(tokenAddrs, gethcommon.HexToAddress(addr)) } - elClaim, _, _, err := claimHelper(config.ClaimTimestamp, ctx, elReader, logger, earnerAddr, df, tokenAddrs) + elClaim, _, _, err := claimHelper(ctx, config.ClaimTimestamp, elReader, logger, earnerAddr, df, tokenAddrs) elClaims = append(elClaims, *elClaim) @@ -156,8 +156,8 @@ func BatchClaim(cCtx *cli.Context, ethClient *ethclient.Client, elReader *elcont } func claimHelper( - claimTimestamp string, ctx context.Context, + claimTimestamp string, elReader *elcontracts.ChainReader, logger logging.Logger, earnerAddress gethcommon.Address, @@ -252,7 +252,7 @@ func Claim(cCtx *cli.Context, p utils.Prompter) error { return BatchClaim(cCtx, ethClient, elReader, df, config, p) } - elClaim, claim, accounts, err := claimHelper(config.ClaimTimestamp, ctx, elReader, logger, config.EarnerAddress, df, config.TokenAddresses) + elClaim, claim, accounts, err := claimHelper(ctx, config.ClaimTimestamp, elReader, logger, config.EarnerAddress, df, config.TokenAddresses) if err != nil { return err From 7db6452d42ff8b9543e6c6215c5523213822509a Mon Sep 17 00:00:00 2001 From: gpabst Date: Fri, 15 Nov 2024 10:15:13 -0500 Subject: [PATCH 6/7] fix: more descriptive name for claim helper --- pkg/rewards/claim.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/rewards/claim.go b/pkg/rewards/claim.go index 15b81769..4a9506d0 100644 --- a/pkg/rewards/claim.go +++ b/pkg/rewards/claim.go @@ -114,7 +114,7 @@ func BatchClaim(cCtx *cli.Context, ethClient *ethclient.Client, elReader *elcont tokenAddrs = append(tokenAddrs, gethcommon.HexToAddress(addr)) } - elClaim, _, _, err := claimHelper(ctx, config.ClaimTimestamp, elReader, logger, earnerAddr, df, tokenAddrs) + elClaim, _, _, err := generateClaimPayload(ctx, config.ClaimTimestamp, elReader, logger, earnerAddr, df, tokenAddrs) elClaims = append(elClaims, *elClaim) @@ -155,7 +155,7 @@ func BatchClaim(cCtx *cli.Context, ethClient *ethclient.Client, elReader *elcont return nil } -func claimHelper( +func generateClaimPayload( ctx context.Context, claimTimestamp string, elReader *elcontracts.ChainReader, @@ -252,7 +252,7 @@ func Claim(cCtx *cli.Context, p utils.Prompter) error { return BatchClaim(cCtx, ethClient, elReader, df, config, p) } - elClaim, claim, accounts, err := claimHelper(ctx, config.ClaimTimestamp, elReader, logger, config.EarnerAddress, df, config.TokenAddresses) + elClaim, claim, accounts, err := generateClaimPayload(ctx, config.ClaimTimestamp, elReader, logger, config.EarnerAddress, df, config.TokenAddresses) if err != nil { return err From 9ac4c779b804937e2ab76f4b220a01240bd285df Mon Sep 17 00:00:00 2001 From: gpabst Date: Fri, 15 Nov 2024 10:22:41 -0500 Subject: [PATCH 7/7] fix: include earner address in claim proof logs --- pkg/rewards/claim.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/rewards/claim.go b/pkg/rewards/claim.go index 4a9506d0..5f409fb6 100644 --- a/pkg/rewards/claim.go +++ b/pkg/rewards/claim.go @@ -202,7 +202,7 @@ func generateClaimPayload( TokenLeaves: convertClaimTokenLeaves(claim.TokenLeaves), } - logger.Info("Validating claim proof...") + logger.Infof("Validating claim proof for earner %s...", earnerAddress) ok, err := elReader.CheckClaim(ctx, elClaim) if err != nil { return nil, nil, nil, err @@ -210,7 +210,7 @@ func generateClaimPayload( if !ok { return nil, nil, nil, errors.New("failed to validate claim") } - logger.Info("Claim proof validated successfully") + logger.Infof("Claim proof for earner %s validated successfully", earnerAddress) return &elClaim, claim, accounts, nil }