-
Notifications
You must be signed in to change notification settings - Fork 106
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
Changes from all commits
d1b3079
44936d0
6ea4abb
ec69bad
e673960
7db6452
9ac4c77
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
"fmt" | ||
"math/big" | ||
"net/http" | ||
"os" | ||
"sort" | ||
"strings" | ||
"time" | ||
|
@@ -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" | ||
|
||
|
@@ -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 { | ||
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 GitHub Actions / Build
Check failure on line 145 in pkg/rewards/claim.go GitHub Actions / Unit Test
Check failure on line 145 in pkg/rewards/claim.go GitHub Actions / Lint
|
||
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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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{ | ||
|
@@ -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( | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
if err != nil { | ||
return eigenSdkUtils.WrapError("failed to process claim", err) | ||
} | ||
|
@@ -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) | ||
} | ||
|
@@ -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) { | ||
|
@@ -457,6 +555,7 @@ | |
ClaimTimestamp: claimTimestamp, | ||
ClaimerAddress: claimerAddress, | ||
IsSilent: isSilent, | ||
BatchClaimFile: batchClaimFile, | ||
}, nil | ||
} | ||
|
||
|
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" |
There was a problem hiding this comment.
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.