Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Rewards v2 batch claiming support #239

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions pkg/internal/common/flags/general.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,11 @@ var (
EnvVars: []string{"EXPIRY"},
Value: 3600,
}

BatchClaimFile = cli.StringFlag{
Name: "batch-claim-file",
Aliases: []string{"bcf"},
Usage: "Input file for batch rewards claim",
EnvVars: []string{"BATCH_CLAIM_FILE"},
}
)
167 changes: 133 additions & 34 deletions pkg/rewards/claim.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"fmt"
"math/big"
"net/http"
"os"
"sort"
"strings"
"time"
Expand All @@ -15,6 +16,8 @@
"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"

Expand Down Expand Up @@ -74,68 +77,116 @@
&ProofStoreBaseURLFlag,
&flags.VerboseFlag,
&flags.SilentFlag,
&flags.BatchClaimFile,
}

allFlags := append(baseFlags, flags.GetSignerFlags()...)
sort.Sort(cli.FlagsByName(allFlags))
return allFlags
}

func Claim(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)
yamlFile, err := os.ReadFile(config.BatchClaimFile)
if err != nil {
return eigenSdkUtils.WrapError("failed to read and validate claim config", err)
return eigenSdkUtils.WrapError("failed to read YAML config file", 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)
var claimConfigs []struct {
EarnerAddress string `yaml:"earner_address"`
TokenAddresses []string `yaml:"token_addresses"`
}

elReader, err := elcontracts.NewReaderFromConfig(
elcontracts.Config{
RewardsCoordinatorAddress: config.RewardsCoordinatorAddress,
},
ethClient, logger,
)
err = yaml.Unmarshal(yamlFile, &claimConfigs)
if err != nil {
return eigenSdkUtils.WrapError("failed to create new reader from config", err)
return eigenSdkUtils.WrapError("failed to parse YAML config", err)
}

df := httpProofDataFetcher.NewHttpProofDataFetcher(
config.ProofStoreBaseURL,
config.Environment,
config.Network,
http.DefaultClient,
)
var elClaims []rewardscoordinator.IRewardsCoordinatorRewardsMerkleClaim

for _, claimConfig := range claimConfigs {
earnerAddr := gethcommon.HexToAddress(claimConfig.EarnerAddress)

var tokenAddrs []gethcommon.Address
for _, addr := range claimConfig.TokenAddresses {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to also handle the case where they just only provide earner addresses and we need to fetch all the available tokens and then create and submit claims.

tokenAddrs = append(tokenAddrs, gethcommon.HexToAddress(addr))
}

elClaim, _, _, err := generateClaimPayload(ctx, config.ClaimTimestamp, 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)

Check failure on line 145 in pkg/rewards/claim.go

View workflow job for this annotation

GitHub Actions / Build

eLWriter.ProcessClaims undefined (type *elcontracts.ChainWriter has no field or method ProcessClaims)

Check failure on line 145 in pkg/rewards/claim.go

View workflow job for this annotation

GitHub Actions / Unit Test

eLWriter.ProcessClaims undefined (type *elcontracts.ChainWriter has no field or method ProcessClaims)

Check failure on line 145 in pkg/rewards/claim.go

View workflow job for this annotation

GitHub Actions / Lint

eLWriter.ProcessClaims undefined (type *elcontracts.ChainWriter has no field or method ProcessClaims)) (typecheck)

Check failure on line 145 in pkg/rewards/claim.go

View workflow job for this annotation

GitHub Actions / Lint

eLWriter.ProcessClaims undefined (type *elcontracts.ChainWriter has no field or method ProcessClaims) (typecheck)
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
}

claimDate, rootIndex, err := getClaimDistributionRoot(ctx, config.ClaimTimestamp, elReader, logger)
func generateClaimPayload(
ctx context.Context,
claimTimestamp string,
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 eigenSdkUtils.WrapError("failed to get claim distribution root", err)
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")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in case of multiple earners, this should silently pass as they can have a some earner with no rewards but other earners with rewards

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behavior actually is it passes but logs an error Failed to process claim for earner %s: no tokens claimable by earner

Is the silent part important? Otherwise I think it may be fine to keep it as is and just log the error and continue with the other earners.

}

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)
}

elClaim := rewardscoordinator.IRewardsCoordinatorRewardsMerkleClaim{
Expand All @@ -151,15 +202,61 @@
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 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.Infof("Claim proof for earner %s validated successfully", earnerAddress)

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)
gpabst marked this conversation as resolved.
Show resolved Hide resolved
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,
)

if config.BatchClaimFile != "" {
return BatchClaim(cCtx, ethClient, elReader, df, config, p)
}

elClaim, claim, accounts, err := generateClaimPayload(ctx, config.ClaimTimestamp, elReader, logger, config.EarnerAddress, df, config.TokenAddresses)

if err != nil {
return err
}
logger.Info("Claim proof validated successfully")

if config.Broadcast {
eLWriter, err := common.GetELWriter(
Expand All @@ -179,7 +276,7 @@
}

logger.Infof("Broadcasting claim transaction...")
receipt, err := eLWriter.ProcessClaim(ctx, elClaim, config.RecipientAddress, true)
receipt, err := eLWriter.ProcessClaim(ctx, *elClaim, config.RecipientAddress, true)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we not use batch claim for single claim? since it's an array - single claim should work. is ProcessClaims for 1 earner is costly then ProcessClaim ?

if err != nil {
return eigenSdkUtils.WrapError("failed to process claim", err)
}
Expand Down Expand Up @@ -208,7 +305,7 @@
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)
}
Expand Down Expand Up @@ -373,6 +470,7 @@
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) {
Expand Down Expand Up @@ -457,6 +555,7 @@
ClaimTimestamp: claimTimestamp,
ClaimerAddress: claimerAddress,
IsSilent: isSilent,
BatchClaimFile: batchClaimFile,
}, nil
}

Expand Down
1 change: 1 addition & 0 deletions pkg/rewards/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type ClaimConfig struct {
Environment string
SignerConfig *types.SignerConfig
IsSilent bool
BatchClaimFile string
}

type SetClaimerConfig struct {
Expand Down
6 changes: 6 additions & 0 deletions samples/batch-claims.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- earner_address: "0x025246421e7247a729bbcff652c5cc1815ac6373"
token_addresses:
- "0x3B78576F7D6837500bA3De27A60c7f594934027E"
- earner_address: "0x025246421e7247a729bbcff652c5cc1815ac6373"
token_addresses:
- "0x3B78576F7D6837500bA3De27A60c7f594934027E"
Loading