diff --git a/go.mod b/go.mod index df6f6200..d96aada1 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Layr-Labs/eigenlayer-contracts v0.3.2-mainnet-rewards github.com/Layr-Labs/eigenlayer-rewards-proofs v0.2.12 github.com/Layr-Labs/eigenpod-proofs-generation v0.0.14-stable.0.20240730152248-5c11a259293e - github.com/Layr-Labs/eigensdk-go v0.1.13-0.20241023200243-565bb4438918 + github.com/Layr-Labs/eigensdk-go v0.1.14-0.20241212190947-9985122d81fe github.com/blang/semver/v4 v4.0.0 github.com/consensys/gnark-crypto v0.12.1 github.com/ethereum/go-ethereum v1.14.5 @@ -19,6 +19,7 @@ require ( github.com/tyler-smith/go-bip39 v1.1.0 github.com/urfave/cli/v2 v2.27.2 github.com/wagslane/go-password-validator v0.3.0 + github.com/wealdtech/go-merkletree/v2 v2.5.2-0.20240302222400-69219c450662 github.com/wk8/go-ordered-map/v2 v2.1.8 go.uber.org/mock v0.4.0 gopkg.in/yaml.v2 v2.4.0 @@ -100,7 +101,6 @@ require ( github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect - github.com/wealdtech/go-merkletree/v2 v2.5.2-0.20240302222400-69219c450662 // indirect github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect diff --git a/go.sum b/go.sum index 1784a9a8..3ec027e3 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,10 @@ github.com/Layr-Labs/eigenlayer-rewards-proofs v0.2.12 h1:G5Q1SnLmFbEjhOkky3vIHk github.com/Layr-Labs/eigenlayer-rewards-proofs v0.2.12/go.mod h1:OlJd1QjqEW53wfWG/lJyPCGvrXwWVEjPQsP4TV+gttQ= github.com/Layr-Labs/eigenpod-proofs-generation v0.0.14-stable.0.20240730152248-5c11a259293e h1:DvW0/kWHV9mZsbH2KOjEHKTSIONNPUj6X05FJvUohy4= github.com/Layr-Labs/eigenpod-proofs-generation v0.0.14-stable.0.20240730152248-5c11a259293e/go.mod h1:T7tYN8bTdca2pkMnz9G2+ZwXYWw5gWqQUIu4KLgC/vM= -github.com/Layr-Labs/eigensdk-go v0.1.13-0.20241023200243-565bb4438918 h1:Itl141PoMFzq58ZTo4Nu/CyH+x8f4BH6OmBNhZ6Z2/I= -github.com/Layr-Labs/eigensdk-go v0.1.13-0.20241023200243-565bb4438918/go.mod h1:aYdNURUhaqeYOS+Cq12TfSdPbjFfiLaHkxPdR4Exq/s= +github.com/Layr-Labs/eigensdk-go v0.1.14-0.20241211225219-79336bf6e886 h1:+7AijqdfRXdDc3zvj02Alqsk6Qd3owvlqPYQN1Hc1ME= +github.com/Layr-Labs/eigensdk-go v0.1.14-0.20241211225219-79336bf6e886/go.mod h1:aYdNURUhaqeYOS+Cq12TfSdPbjFfiLaHkxPdR4Exq/s= +github.com/Layr-Labs/eigensdk-go v0.1.14-0.20241212190947-9985122d81fe h1:FeXxapvtEbbTbEWsrcBTTzQ2u2quGJ9HNYQVSk5JZ8g= +github.com/Layr-Labs/eigensdk-go v0.1.14-0.20241212190947-9985122d81fe/go.mod h1:aYdNURUhaqeYOS+Cq12TfSdPbjFfiLaHkxPdR4Exq/s= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= diff --git a/pkg/internal/common/flags/general.go b/pkg/internal/common/flags/general.go index 194421ce..4077c1a5 100644 --- a/pkg/internal/common/flags/general.go +++ b/pkg/internal/common/flags/general.go @@ -90,4 +90,18 @@ var ( EnvVars: []string{"EXPIRY"}, Value: 3600, } + + OperatorAddressFlag = cli.StringFlag{ + Name: "operator-address", + Aliases: []string{"oa", "operator"}, + Usage: "Operator address", + EnvVars: []string{"OPERATOR_ADDRESS"}, + } + + BatchClaimFile = cli.StringFlag{ + Name: "batch-claim-file", + Aliases: []string{"bcf"}, + Usage: "Input file for batch rewards claim", + EnvVars: []string{"BATCH_CLAIM_FILE"}, + } ) diff --git a/pkg/operator.go b/pkg/operator.go index f3ca2ce4..d34d1742 100644 --- a/pkg/operator.go +++ b/pkg/operator.go @@ -18,6 +18,10 @@ func OperatorCmd(p utils.Prompter) *cli.Command { operator.UpdateCmd(p), operator.UpdateMetadataURICmd(p), operator.GetApprovalCmd(p), + operator.SetOperatorSplitCmd(p), + operator.GetOperatorSplitCmd(p), + operator.GetOperatorPISplitCmd(p), + operator.SetOperatorPISplitCmd(p), }, } diff --git a/pkg/operator/get_operator_pi_split.go b/pkg/operator/get_operator_pi_split.go new file mode 100644 index 00000000..3e6a4cce --- /dev/null +++ b/pkg/operator/get_operator_pi_split.go @@ -0,0 +1,39 @@ +package operator + +import ( + "sort" + + "github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common/flags" + "github.com/Layr-Labs/eigenlayer-cli/pkg/operator/split" + "github.com/Layr-Labs/eigenlayer-cli/pkg/rewards" + "github.com/Layr-Labs/eigenlayer-cli/pkg/telemetry" + "github.com/Layr-Labs/eigenlayer-cli/pkg/utils" + "github.com/urfave/cli/v2" +) + +func GetOperatorPISplitCmd(p utils.Prompter) *cli.Command { + var operatorSplitCmd = &cli.Command{ + Name: "get-pi-split", + Usage: "Get programmatic incentives rewards split", + Action: func(cCtx *cli.Context) error { + return GetOperatorSplit(cCtx, true) + }, + After: telemetry.AfterRunAction(), + Flags: getGetOperatorPISplitFlags(), + } + + return operatorSplitCmd +} + +func getGetOperatorPISplitFlags() []cli.Flag { + baseFlags := []cli.Flag{ + &flags.NetworkFlag, + &flags.ETHRpcUrlFlag, + &flags.OperatorAddressFlag, + &split.OperatorSplitFlag, + &rewards.RewardsCoordinatorAddressFlag, + } + + sort.Sort(cli.FlagsByName(baseFlags)) + return baseFlags +} diff --git a/pkg/operator/get_operator_split.go b/pkg/operator/get_operator_split.go new file mode 100644 index 00000000..9e94c2e4 --- /dev/null +++ b/pkg/operator/get_operator_split.go @@ -0,0 +1,136 @@ +package operator + +import ( + "errors" + "sort" + + "github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common" + "github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common/flags" + "github.com/Layr-Labs/eigenlayer-cli/pkg/operator/split" + "github.com/Layr-Labs/eigenlayer-cli/pkg/rewards" + "github.com/Layr-Labs/eigenlayer-cli/pkg/telemetry" + "github.com/Layr-Labs/eigenlayer-cli/pkg/utils" + "github.com/Layr-Labs/eigensdk-go/chainio/clients/elcontracts" + "github.com/Layr-Labs/eigensdk-go/logging" + eigenSdkUtils "github.com/Layr-Labs/eigensdk-go/utils" + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/urfave/cli/v2" +) + +func GetOperatorSplitCmd(p utils.Prompter) *cli.Command { + var operatorSplitCmd = &cli.Command{ + Name: "get-rewards-split", + Usage: "Get operator rewards split", + Action: func(cCtx *cli.Context) error { + return GetOperatorSplit(cCtx, false) + }, + After: telemetry.AfterRunAction(), + Flags: getGetOperatorSplitFlags(), + } + + return operatorSplitCmd +} + +func getGetOperatorSplitFlags() []cli.Flag { + baseFlags := []cli.Flag{ + &flags.NetworkFlag, + &flags.ETHRpcUrlFlag, + &flags.OperatorAddressFlag, + &split.OperatorSplitFlag, + &rewards.RewardsCoordinatorAddressFlag, + &split.AVSAddressFlag, + } + + sort.Sort(cli.FlagsByName(baseFlags)) + return baseFlags +} + +func GetOperatorSplit(cCtx *cli.Context, isProgrammaticIncentive bool) error { + ctx := cCtx.Context + logger := common.GetLogger(cCtx) + + config, err := readAndValidateGetOperatorSplitConfig(cCtx, logger, isProgrammaticIncentive) + if config.Network != "holesky" { + return errors.New("getting operator split only supported on holesky with this version of CLI") + } + + if err != nil { + return eigenSdkUtils.WrapError("failed to read and validate operator split 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 get EL writer", err) + } + + logger.Infof("Getting operator split...") + + var split uint16 + if isProgrammaticIncentive { + split, err = elReader.GetOperatorPISplit(ctx, config.OperatorAddress) + } else { + split, err = elReader.GetOperatorAVSSplit(ctx, config.OperatorAddress, config.AVSAddress) + } + if err != nil { + return eigenSdkUtils.WrapError("failed to get operator split", err) + } + + logger.Infof("Operator split is %d", split) + + return nil +} + +func readAndValidateGetOperatorSplitConfig( + cCtx *cli.Context, + logger logging.Logger, + isProgrammaticIncentive bool, +) (*split.GetOperatorAVSSplitConfig, error) { + network := cCtx.String(flags.NetworkFlag.Name) + rpcUrl := cCtx.String(flags.ETHRpcUrlFlag.Name) + + rewardsCoordinatorAddress := cCtx.String(rewards.RewardsCoordinatorAddressFlag.Name) + + var err error + if common.IsEmptyString(rewardsCoordinatorAddress) { + rewardsCoordinatorAddress, err = common.GetRewardCoordinatorAddress(utils.NetworkNameToChainId(network)) + if err != nil { + return nil, err + } + } + logger.Debugf("Using Rewards Coordinator address: %s", rewardsCoordinatorAddress) + + operatorAddress := gethcommon.HexToAddress(cCtx.String(flags.OperatorAddressFlag.Name)) + logger.Infof("Using operator address: %s", operatorAddress.String()) + + avsAddress := gethcommon.HexToAddress(cCtx.String(split.AVSAddressFlag.Name)) + if !isProgrammaticIncentive { + logger.Infof("Using AVS address: %s", avsAddress.String()) + } + + chainID := utils.NetworkNameToChainId(network) + logger.Debugf("Using chain ID: %s", chainID.String()) + + return &split.GetOperatorAVSSplitConfig{ + Network: network, + RPCUrl: rpcUrl, + RewardsCoordinatorAddress: gethcommon.HexToAddress(rewardsCoordinatorAddress), + ChainID: chainID, + OperatorAddress: operatorAddress, + AVSAddress: avsAddress, + }, nil +} diff --git a/pkg/operator/set_operator_pi_split.go b/pkg/operator/set_operator_pi_split.go new file mode 100644 index 00000000..63919e3a --- /dev/null +++ b/pkg/operator/set_operator_pi_split.go @@ -0,0 +1,44 @@ +package operator + +import ( + "sort" + + "github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common/flags" + "github.com/Layr-Labs/eigenlayer-cli/pkg/operator/split" + "github.com/Layr-Labs/eigenlayer-cli/pkg/rewards" + "github.com/Layr-Labs/eigenlayer-cli/pkg/telemetry" + "github.com/Layr-Labs/eigenlayer-cli/pkg/utils" + "github.com/urfave/cli/v2" +) + +func SetOperatorPISplitCmd(p utils.Prompter) *cli.Command { + var operatorSplitCmd = &cli.Command{ + Name: "set-pi-split", + Usage: "Set operator programmatic incentives split", + Action: func(cCtx *cli.Context) error { + return SetOperatorSplit(cCtx, p, true) + }, + After: telemetry.AfterRunAction(), + Flags: getSetOperatorPISplitFlags(), + } + + return operatorSplitCmd +} + +func getSetOperatorPISplitFlags() []cli.Flag { + baseFlags := []cli.Flag{ + &flags.NetworkFlag, + &flags.ETHRpcUrlFlag, + &flags.OperatorAddressFlag, + &split.OperatorSplitFlag, + &rewards.RewardsCoordinatorAddressFlag, + &flags.BroadcastFlag, + &flags.OutputTypeFlag, + &flags.OutputFileFlag, + &flags.SilentFlag, + } + + allFlags := append(baseFlags, flags.GetSignerFlags()...) + sort.Sort(cli.FlagsByName(allFlags)) + return allFlags +} diff --git a/pkg/operator/set_operator_split.go b/pkg/operator/set_operator_split.go new file mode 100644 index 00000000..aee16add --- /dev/null +++ b/pkg/operator/set_operator_split.go @@ -0,0 +1,222 @@ +package operator + +import ( + "errors" + "fmt" + "sort" + + "github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common" + "github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common/flags" + "github.com/Layr-Labs/eigenlayer-cli/pkg/operator/split" + "github.com/Layr-Labs/eigenlayer-cli/pkg/rewards" + "github.com/Layr-Labs/eigenlayer-cli/pkg/telemetry" + "github.com/Layr-Labs/eigenlayer-cli/pkg/utils" + "github.com/Layr-Labs/eigensdk-go/chainio/clients/elcontracts" + "github.com/Layr-Labs/eigensdk-go/logging" + eigenSdkUtils "github.com/Layr-Labs/eigensdk-go/utils" + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/urfave/cli/v2" +) + +func SetOperatorSplitCmd(p utils.Prompter) *cli.Command { + var operatorSplitCmd = &cli.Command{ + Name: "set-rewards-split", + Usage: "Set operator rewards split", + Action: func(cCtx *cli.Context) error { + return SetOperatorSplit(cCtx, p, false) + }, + After: telemetry.AfterRunAction(), + Flags: getSetOperatorSplitFlags(), + } + + return operatorSplitCmd +} + +func SetOperatorSplit(cCtx *cli.Context, p utils.Prompter, isProgrammaticIncentive bool) error { + ctx := cCtx.Context + logger := common.GetLogger(cCtx) + + config, err := readAndValidateSetOperatorSplitConfig(cCtx, logger, isProgrammaticIncentive) + if err != nil { + return eigenSdkUtils.WrapError("failed to read and validate operator split config", err) + } + + if config.Network != "holesky" { + return errors.New("setting operator split only supported on holesky with this version of CLI") + } + + 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) + } + + if config.Broadcast { + + eLWriter, err := common.GetELWriter( + config.OperatorAddress, + 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 set operator transaction...") + + var receipt *types.Receipt + if isProgrammaticIncentive { + receipt, err = eLWriter.SetOperatorPISplit(ctx, config.OperatorAddress, config.Split, true) + + } else { + receipt, err = eLWriter.SetOperatorAVSSplit(ctx, config.OperatorAddress, config.AVSAddress, config.Split, true) + } + if err != nil { + return eigenSdkUtils.WrapError("failed to process claim", err) + } + + logger.Infof("Set operator transaction submitted successfully") + common.PrintTransactionInfo(receipt.TxHash.String(), config.ChainID) + } else { + noSendTxOpts := common.GetNoSendTxOpts(config.OperatorAddress) + _, _, contractBindings, err := elcontracts.BuildClients(elcontracts.Config{ + RewardsCoordinatorAddress: config.RewardsCoordinatorAddress, + }, ethClient, nil, logger, nil) + if err != nil { + return err + } + + code, err := ethClient.CodeAt(ctx, config.OperatorAddress, nil) + if err != nil { + return eigenSdkUtils.WrapError("failed to get code at address", err) + } + if len(code) > 0 { + // Operator is a smart contract + noSendTxOpts.GasLimit = 150_000 + } + + var unsignedTx *types.Transaction + if isProgrammaticIncentive { + unsignedTx, err = contractBindings.RewardsCoordinator.SetOperatorPISplit(noSendTxOpts, config.OperatorAddress, config.Split) + } else { + unsignedTx, err = contractBindings.RewardsCoordinator.SetOperatorAVSSplit(noSendTxOpts, config.OperatorAddress, config.AVSAddress, config.Split) + } + + if err != nil { + return eigenSdkUtils.WrapError("failed to create unsigned tx", err) + } + if config.OutputType == string(common.OutputType_Calldata) { + calldataHex := gethcommon.Bytes2Hex(unsignedTx.Data()) + + if !common.IsEmptyString(config.OutputFile) { + err = common.WriteToFile([]byte(calldataHex), config.OutputFile) + if err != nil { + return err + } + logger.Infof("Call data written to file: %s", config.OutputFile) + } else { + fmt.Println(calldataHex) + } + } else { + logger.Infof("This transaction would set the operator split to %d", config.Split) + } + + if !config.IsSilent { + txFeeDetails := common.GetTxFeeDetails(unsignedTx) + fmt.Println() + txFeeDetails.Print() + + fmt.Println("To broadcast the operator set split, use the --broadcast flag") + } + } + return nil +} + +func getSetOperatorSplitFlags() []cli.Flag { + baseFlags := []cli.Flag{ + &flags.NetworkFlag, + &flags.ETHRpcUrlFlag, + &flags.OperatorAddressFlag, + &split.OperatorSplitFlag, + &rewards.RewardsCoordinatorAddressFlag, + &split.AVSAddressFlag, + &flags.BroadcastFlag, + &flags.OutputTypeFlag, + &flags.OutputFileFlag, + &flags.SilentFlag, + } + + allFlags := append(baseFlags, flags.GetSignerFlags()...) + sort.Sort(cli.FlagsByName(allFlags)) + return allFlags +} + +func readAndValidateSetOperatorSplitConfig( + cCtx *cli.Context, + logger logging.Logger, + isProgrammaticIncentive bool, +) (*split.SetOperatorAVSSplitConfig, error) { + network := cCtx.String(flags.NetworkFlag.Name) + rpcUrl := cCtx.String(flags.ETHRpcUrlFlag.Name) + opSplit := cCtx.Int(split.OperatorSplitFlag.Name) + broadcast := cCtx.Bool(flags.BroadcastFlag.Name) + outputType := cCtx.String(flags.OutputTypeFlag.Name) + outputFile := cCtx.String(flags.OutputFileFlag.Name) + isSilent := cCtx.Bool(flags.SilentFlag.Name) + + rewardsCoordinatorAddress := cCtx.String(rewards.RewardsCoordinatorAddressFlag.Name) + + var err error + if common.IsEmptyString(rewardsCoordinatorAddress) { + rewardsCoordinatorAddress, err = common.GetRewardCoordinatorAddress(utils.NetworkNameToChainId(network)) + if err != nil { + return nil, err + } + } + logger.Debugf("Using Rewards Coordinator address: %s", rewardsCoordinatorAddress) + + operatorAddress := gethcommon.HexToAddress(cCtx.String(flags.OperatorAddressFlag.Name)) + logger.Infof("Using operator address: %s", operatorAddress.String()) + + avsAddress := gethcommon.HexToAddress(cCtx.String(split.AVSAddressFlag.Name)) + + if !isProgrammaticIncentive { + logger.Infof("Using AVS address: %s", avsAddress.String()) + } + + chainID := utils.NetworkNameToChainId(network) + logger.Debugf("Using chain ID: %s", chainID.String()) + + // Get SignerConfig + signerConfig, err := common.GetSignerConfig(cCtx, logger) + if err != nil { + // We don't want to throw error since people can still use it to generate the claim + // without broadcasting it + logger.Debugf("Failed to get signer config: %s", err) + } + + return &split.SetOperatorAVSSplitConfig{ + Network: network, + RPCUrl: rpcUrl, + RewardsCoordinatorAddress: gethcommon.HexToAddress(rewardsCoordinatorAddress), + ChainID: chainID, + SignerConfig: signerConfig, + OperatorAddress: operatorAddress, + AVSAddress: avsAddress, + Split: uint16(opSplit), + Broadcast: broadcast, + OutputType: outputType, + OutputFile: outputFile, + IsSilent: isSilent, + }, nil +} diff --git a/pkg/operator/split/flags.go b/pkg/operator/split/flags.go new file mode 100644 index 00000000..c8ec6bcf --- /dev/null +++ b/pkg/operator/split/flags.go @@ -0,0 +1,20 @@ +package split + +import "github.com/urfave/cli/v2" + +var ( + OperatorSplitFlag = cli.IntFlag{ + Name: "operator-split", + Aliases: []string{"os"}, + Usage: "Split for the operator in bips (e.g. 1000 = 10%)", + Required: false, + EnvVars: []string{"OPERATOR_SPLIT"}, + } + + AVSAddressFlag = cli.StringFlag{ + Name: "avs-address", + Aliases: []string{"aa"}, + Usage: "AVS address to set operator split", + EnvVars: []string{"AVS_ADDRESS"}, + } +) diff --git a/pkg/operator/split/types.go b/pkg/operator/split/types.go new file mode 100644 index 00000000..fd98bc39 --- /dev/null +++ b/pkg/operator/split/types.go @@ -0,0 +1,33 @@ +package split + +import ( + "math/big" + + "github.com/Layr-Labs/eigenlayer-cli/pkg/types" + gethcommon "github.com/ethereum/go-ethereum/common" +) + +type SetOperatorAVSSplitConfig struct { + Network string + RPCUrl string + RewardsCoordinatorAddress gethcommon.Address + ChainID *big.Int + SignerConfig *types.SignerConfig + Broadcast bool + OperatorAddress gethcommon.Address + AVSAddress gethcommon.Address + Split uint16 + OutputType string + OutputFile string + IsSilent bool +} + +type GetOperatorAVSSplitConfig struct { + Network string + RPCUrl string + RewardsCoordinatorAddress gethcommon.Address + ChainID *big.Int + + OperatorAddress gethcommon.Address + AVSAddress gethcommon.Address +} diff --git a/pkg/rewards/claim.go b/pkg/rewards/claim.go index 6a825bcd..c50a5127 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,11 +16,14 @@ 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" "github.com/Layr-Labs/eigenlayer-rewards-proofs/pkg/claimgen" "github.com/Layr-Labs/eigenlayer-rewards-proofs/pkg/distribution" + "github.com/Layr-Labs/eigenlayer-rewards-proofs/pkg/proofDataFetcher" "github.com/Layr-Labs/eigenlayer-rewards-proofs/pkg/proofDataFetcher/httpProofDataFetcher" "github.com/Layr-Labs/eigensdk-go/chainio/clients/elcontracts" @@ -28,6 +32,7 @@ import ( eigenSdkUtils "github.com/Layr-Labs/eigensdk-go/utils" gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/urfave/cli/v2" @@ -75,6 +80,7 @@ func getClaimFlags() []cli.Flag { &ProofStoreBaseURLFlag, &flags.VerboseFlag, &flags.SilentFlag, + &flags.BatchClaimFile, } allFlags := append(baseFlags, flags.GetSignerFlags()...) @@ -82,6 +88,131 @@ func getClaimFlags() []cli.Flag { return allFlags } +func batchClaim( + ctx context.Context, + logger logging.Logger, + ethClient *ethclient.Client, + elReader *elcontracts.ChainReader, + config *ClaimConfig, + p utils.Prompter, + rootIndex uint32, + proofData *proofDataFetcher.RewardProofData, +) error { + + yamlFile, err := os.ReadFile(config.BatchClaimFile) + if err != nil { + 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 + var claims []contractrewardscoordinator.IRewardsCoordinatorRewardsMerkleClaim + var accounts []merkletree.MerkleTree + + for _, claimConfig := range claimConfigs { + earnerAddr := gethcommon.HexToAddress(claimConfig.EarnerAddress) + + var tokenAddrs []gethcommon.Address + + // Empty token addresses list will create a claim for all tokens claimable + // by the earner address. + if len(claimConfig.TokenAddresses) != 0 { + for _, addr := range claimConfig.TokenAddresses { + tokenAddrs = append(tokenAddrs, gethcommon.HexToAddress(addr)) + } + } + + elClaim, claim, account, err := generateClaimPayload( + ctx, + rootIndex, + proofData, + elReader, + logger, + earnerAddr, + tokenAddrs, + ) + + if err != nil { + logger.Warnf("Failed to process claim for earner %s: %v", earnerAddr.String(), err) + continue + } + + elClaims = append(elClaims, *elClaim) + claims = append(claims, *claim) + accounts = append(accounts, *account) + + } + + return broadcastClaims(config, ethClient, logger, p, ctx, elClaims, claims, accounts) +} + +func generateClaimPayload( + ctx context.Context, + rootIndex uint32, + proofData *proofDataFetcher.RewardProofData, + elReader *elcontracts.ChainReader, + logger logging.Logger, + earnerAddress gethcommon.Address, + tokenAddresses []gethcommon.Address, +) (*rewardscoordinator.IRewardsCoordinatorRewardsMerkleClaim, *contractrewardscoordinator.IRewardsCoordinatorRewardsMerkleClaim, *merkletree.MerkleTree, error) { + + claimableTokensOrderMap, present := proofData.Distribution.GetTokensForEarner(earnerAddress) + if !present { + return nil, nil, nil, errors.New("no tokens claimable by earner") + } + + claimableTokensMap := getTokensToClaim(claimableTokensOrderMap, tokenAddresses) + + claimableTokens, err := filterClaimableTokens(ctx, elReader, earnerAddress, claimableTokensMap) + if err != nil { + return nil, nil, nil, eigenSdkUtils.WrapError("failed to get claimable tokens", err) + } + + cg := claimgen.NewClaimgen(proofData.Distribution) + accounts, claim, err := cg.GenerateClaimProofForEarner( + earnerAddress, + claimableTokens, + rootIndex, + ) + 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, + EarnerTreeProof: claim.EarnerTreeProof, + EarnerLeaf: rewardscoordinator.IRewardsCoordinatorEarnerTreeMerkleLeaf{ + Earner: claim.EarnerLeaf.Earner, + EarnerTokenRoot: claim.EarnerLeaf.EarnerTokenRoot, + }, + TokenIndices: claim.TokenIndices, + TokenTreeProofs: claim.TokenTreeProofs, + TokenLeaves: convertClaimTokenLeaves(claim.TokenLeaves), + } + + logger.Infof("Validating claim proof for earner %s...", earnerAddress) + ok, err := elReader.CheckClaim(ctx, elClaim) + if err != nil { + return nil, nil, nil, err + } + if !ok { + 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) @@ -90,6 +221,11 @@ func Claim(cCtx *cli.Context, p utils.Prompter) error { if err != nil { return eigenSdkUtils.WrapError("failed to read and validate claim config", err) } + + if config.Network != "holesky" && config.BatchClaimFile != "" { + return errors.New("batch claim only supported on holesky with this version of CLI") + } + cCtx.App.Metadata["network"] = config.ChainID.String() ethClient, err := ethclient.Dial(config.RPCUrl) @@ -101,7 +237,8 @@ func Claim(cCtx *cli.Context, p utils.Prompter) error { elcontracts.Config{ RewardsCoordinatorAddress: config.RewardsCoordinatorAddress, }, - ethClient, logger, + ethClient, + logger, ) if err != nil { return eigenSdkUtils.WrapError("failed to create new reader from config", err) @@ -124,51 +261,45 @@ func Claim(cCtx *cli.Context, p utils.Prompter) error { return eigenSdkUtils.WrapError("failed to fetch claim amounts for date", err) } - claimableTokensOrderMap, present := proofData.Distribution.GetTokensForEarner(config.EarnerAddress) - if !present { - return errors.New("no tokens claimable by earner") - } - - claimableTokensMap := getTokensToClaim(claimableTokensOrderMap, config.TokenAddresses) - - claimableTokens, err := filterClaimableTokens(ctx, elReader, config.EarnerAddress, claimableTokensMap) - if err != nil { - return eigenSdkUtils.WrapError("failed to get claimable tokens", err) + if config.BatchClaimFile != "" { + return batchClaim(ctx, logger, ethClient, elReader, config, p, rootIndex, proofData) } - cg := claimgen.NewClaimgen(proofData.Distribution) - accounts, claim, err := cg.GenerateClaimProofForEarner( - config.EarnerAddress, - claimableTokens, + elClaim, claim, account, err := generateClaimPayload( + ctx, rootIndex, + proofData, + elReader, + logger, + config.EarnerAddress, + config.TokenAddresses, ) - if err != nil { - return eigenSdkUtils.WrapError("failed to generate claim proof for earner", err) - } - elClaim := rewardscoordinator.IRewardsCoordinatorRewardsMerkleClaim{ - RootIndex: claim.RootIndex, - EarnerIndex: claim.EarnerIndex, - EarnerTreeProof: claim.EarnerTreeProof, - EarnerLeaf: rewardscoordinator.IRewardsCoordinatorEarnerTreeMerkleLeaf{ - Earner: claim.EarnerLeaf.Earner, - EarnerTokenRoot: claim.EarnerLeaf.EarnerTokenRoot, - }, - TokenIndices: claim.TokenIndices, - TokenTreeProofs: claim.TokenTreeProofs, - TokenLeaves: convertClaimTokenLeaves(claim.TokenLeaves), - } - - logger.Info("Validating claim proof...") - ok, err := elReader.CheckClaim(ctx, elClaim) if err != nil { return err } - if !ok { - return errors.New("failed to validate claim") - } - logger.Info("Claim proof validated successfully") + elClaims := []rewardscoordinator.IRewardsCoordinatorRewardsMerkleClaim{*elClaim} + claims := []contractrewardscoordinator.IRewardsCoordinatorRewardsMerkleClaim{*claim} + accounts := []merkletree.MerkleTree{*account} + err = broadcastClaims(config, ethClient, logger, p, ctx, elClaims, claims, accounts) + + return err +} + +func broadcastClaims( + config *ClaimConfig, + ethClient *ethclient.Client, + logger logging.Logger, + p utils.Prompter, + ctx context.Context, + elClaims []rewardscoordinator.IRewardsCoordinatorRewardsMerkleClaim, + claims []contractrewardscoordinator.IRewardsCoordinatorRewardsMerkleClaim, + accounts []merkletree.MerkleTree, +) error { + if len(elClaims) == 0 { + return fmt.Errorf("at least one claim is required") + } if config.Broadcast { eLWriter, err := common.GetELWriter( config.ClaimerAddress, @@ -187,7 +318,15 @@ func Claim(cCtx *cli.Context, p utils.Prompter) error { } logger.Infof("Broadcasting claim transaction...") - receipt, err := eLWriter.ProcessClaim(ctx, elClaim, config.RecipientAddress, true) + + var receipt *types.Receipt + + if len(elClaims) > 1 { + receipt, err = eLWriter.ProcessClaims(ctx, elClaims, config.RecipientAddress, true) + } else { + receipt, err = eLWriter.ProcessClaim(ctx, elClaims[0], config.RecipientAddress, true) + } + if err != nil { return eigenSdkUtils.WrapError("failed to process claim", err) } @@ -215,8 +354,12 @@ func Claim(cCtx *cli.Context, p utils.Prompter) error { // Claimer is a smart contract noSendTxOpts.GasLimit = 150_000 } - - unsignedTx, err := contractBindings.RewardsCoordinator.ProcessClaim(noSendTxOpts, elClaim, config.RecipientAddress) + var unsignedTx *types.Transaction + if len(elClaims) > 1 { + unsignedTx, err = contractBindings.RewardsCoordinator.ProcessClaims(noSendTxOpts, elClaims, config.RecipientAddress) + } else { + unsignedTx, err = contractBindings.RewardsCoordinator.ProcessClaim(noSendTxOpts, elClaims[0], config.RecipientAddress) + } if err != nil { return eigenSdkUtils.WrapError("failed to create unsigned tx", err) } @@ -234,33 +377,36 @@ func Claim(cCtx *cli.Context, p utils.Prompter) error { fmt.Println(calldataHex) } } else if config.OutputType == string(common.OutputType_Json) { - solidityClaim := claimgen.FormatProofForSolidity(accounts.Root(), claim) - jsonData, err := json.MarshalIndent(solidityClaim, "", " ") - if err != nil { - logger.Error("Error marshaling JSON:", err) - return err - } - if !common.IsEmptyString(config.Output) { - err = common.WriteToFile(jsonData, config.Output) + for idx, claim := range claims { + solidityClaim := claimgen.FormatProofForSolidity(accounts[idx].Root(), &claim) + jsonData, err := json.MarshalIndent(solidityClaim, "", " ") if err != nil { return err } - logger.Infof("Claim written to file: %s", config.Output) - } else { - fmt.Println(string(jsonData)) - fmt.Println() - fmt.Println("To write to a file, use the --output flag") + if !common.IsEmptyString(config.Output) { + err = common.WriteToFile(jsonData, config.Output) + if err != nil { + return err + } + logger.Infof("Claim written to file: %s", config.Output) + } else { + fmt.Println(string(jsonData)) + fmt.Println() + fmt.Println("To write to a file, use the --output flag") + } } } else { if !common.IsEmptyString(config.Output) { fmt.Println("output file not supported for pretty output type") fmt.Println() } - solidityClaim := claimgen.FormatProofForSolidity(accounts.Root(), claim) - if !config.IsSilent { - fmt.Println("------- Claim generated -------") + for idx, claim := range claims { + solidityClaim := claimgen.FormatProofForSolidity(accounts[idx].Root(), &claim) + if !config.IsSilent { + fmt.Println("------- Claim generated -------") + } + common.PrettyPrintStruct(*solidityClaim) } - common.PrettyPrintStruct(*solidityClaim) if !config.IsSilent { fmt.Println("-------------------------------") fmt.Println("To write to a file, use the --output flag") @@ -406,6 +552,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) { @@ -490,6 +637,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..3aa78308 --- /dev/null +++ b/samples/batch-claims.yaml @@ -0,0 +1,7 @@ +- earner_address: "0x025246421e7247a729bbcff652c5cc1815ac6373" + token_addresses: + - "0x3B78576F7D6837500bA3De27A60c7f594934027E" +- earner_address: "0x025246421e7247a729bbcff652c5cc1815ac6373" + token_addresses: + - "0x3B78576F7D6837500bA3De27A60c7f594934027E" + \ No newline at end of file