From 0f00b7ccc99078890355eed7cf40a27d2b280796 Mon Sep 17 00:00:00 2001 From: Madhur Shrimal Date: Thu, 1 Aug 2024 09:52:41 -0700 Subject: [PATCH] feat: add calldata format support for rewards commands (#177) * init * feat: add calldata generation support to rewards method * fmt * fix --- pkg/eigenpod/status.go | 2 +- pkg/internal/common/constants.go | 6 +++ pkg/internal/common/fileio.go | 2 +- pkg/internal/common/flags/general.go | 2 +- pkg/internal/common/helper.go | 15 +++++- pkg/rewards/README.md | 54 ++++++++++---------- pkg/rewards/claim.go | 73 ++++++++++++++++++++-------- pkg/rewards/setclaimer.go | 56 +++++++++++++++++---- 8 files changed, 151 insertions(+), 59 deletions(-) diff --git a/pkg/eigenpod/status.go b/pkg/eigenpod/status.go index df26c0f9..006b5ce0 100644 --- a/pkg/eigenpod/status.go +++ b/pkg/eigenpod/status.go @@ -66,7 +66,7 @@ func status(cCtx *cli.Context, p utils.Prompter) error { } if cfg.outputFile != "" { - err := common.WriteToJSON(jsonData, cfg.outputFile) + err := common.WriteToFile(jsonData, cfg.outputFile) if err != nil { return err } diff --git a/pkg/internal/common/constants.go b/pkg/internal/common/constants.go index d7a38fa1..7bcb6ec6 100644 --- a/pkg/internal/common/constants.go +++ b/pkg/internal/common/constants.go @@ -1,7 +1,13 @@ package common +type OutputType string + const ( // MaxAddressLength Magic number 42 is the max length of an address. // But it's also answer to the life, universe and everything. MaxAddressLength = 42 + + OutputType_Calldata OutputType = "calldata" + OutputType_Pretty OutputType = "pretty" + OutputType_Json OutputType = "json" ) diff --git a/pkg/internal/common/fileio.go b/pkg/internal/common/fileio.go index 6efda223..f018fe82 100644 --- a/pkg/internal/common/fileio.go +++ b/pkg/internal/common/fileio.go @@ -8,7 +8,7 @@ import ( "github.com/gocarina/gocsv" ) -func WriteToJSON(data []byte, filePath string) error { +func WriteToFile(data []byte, filePath string) error { dir := path.Dir(filePath) // Ensure the directory exists err := ensureDir(dir) diff --git a/pkg/internal/common/flags/general.go b/pkg/internal/common/flags/general.go index db40440e..58b4ca0f 100644 --- a/pkg/internal/common/flags/general.go +++ b/pkg/internal/common/flags/general.go @@ -37,7 +37,7 @@ var ( Name: "output-type", Aliases: []string{"ot"}, Value: "pretty", - Usage: "Output type to for that respective command. One of 'pretty' or 'json'", + Usage: "Output format of the command. One of 'pretty', 'json' or 'calldata'", EnvVars: []string{"OUTPUT_TYPE"}, } diff --git a/pkg/internal/common/helper.go b/pkg/internal/common/helper.go index 3a5e2234..bc461e04 100644 --- a/pkg/internal/common/helper.go +++ b/pkg/internal/common/helper.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "log/slog" "math/big" "os" @@ -27,7 +26,9 @@ import ( eigensdkTypes "github.com/Layr-Labs/eigensdk-go/types" eigenSdkUtils "github.com/Layr-Labs/eigensdk-go/utils" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" @@ -425,3 +426,15 @@ func GetLogger(cCtx *cli.Context) eigensdkLogger.Logger { logger := eigensdkLogger.NewTextSLogger(os.Stdout, &eigensdkLogger.SLoggerOptions{Level: logLevel}) return logger } + +func noopSigner(addr common.Address, tx *gethtypes.Transaction) (*gethtypes.Transaction, error) { + return tx, nil +} + +func GetNoSendTxOpts(from common.Address) *bind.TransactOpts { + return &bind.TransactOpts{ + From: from, + Signer: noopSigner, + NoSend: true, + } +} diff --git a/pkg/rewards/README.md b/pkg/rewards/README.md index edd3abde..78f3e559 100644 --- a/pkg/rewards/README.md +++ b/pkg/rewards/README.md @@ -10,28 +10,29 @@ USAGE: eigenlayer rewards claim [command options] OPTIONS: - --network value, -n value Network to use. Currently supports 'holesky' and 'mainnet' (default: "holesky") [$NETWORK] - --eth-rpc-url value, -r value URL of the Ethereum RPC [$ETH_RPC_URL] - --output-file value, -o value Output file to write the data [$OUTPUT_FILE] --broadcast, -b Use this flag to broadcast the transaction (default: false) [$BROADCAST] - --earner-address value, --ea value Address of the earner [$REWARDS_EARNER_ADDRESS] - --environment value, --env value Environment to use. Currently supports 'preprod' ,'testnet' and 'prod'. If not provided, it will be inferred based on network [$ENVIRONMENT] - --recipient-address value, --ra value Specify the address of the recipient. If this is not provided, the earner address will be used [$RECIPIENT_ADDRESS] - --token-addresses value, -t value Specify the addresses of the tokens to claim. Comma separated list of addresses [$TOKEN_ADDRESSES] - --rewards-coordinator-address value, --rc value Specify the address of the rewards coordinator. If not provided, the address will be used based on provided network [$REWARDS_COORDINATOR_ADDRESS] --claim-timestamp value, -c value Specify the timestamp. Only 'latest' is supported (default: "latest") [$CLAIM_TIMESTAMP] - --proof-store-base-url value, --psbu value Specify the base URL of the proof store. If not provided, the value based on network will be used [$PROOF_STORE_BASE_URL] - --path-to-key-store value, -k value Path to the key store used to send transactions [$PATH_TO_KEY_STORE] + --earner-address value, --ea value Address of the earner [$REWARDS_EARNER_ADDRESS] --ecdsa-private-key value, -e value ECDSA private key hex to send transaction [$ECDSA_PRIVATE_KEY] + --environment value, --env value Environment to use. Currently supports 'preprod' ,'testnet' and 'prod'. If not provided, it will be inferred based on network [$ENVIRONMENT] + --eth-rpc-url value, -r value URL of the Ethereum RPC [$ETH_RPC_URL] --fireblocks-api-key value, --ff value Fireblocks API key [$FIREBLOCKS_API_KEY] - --fireblocks-secret-key value, --fs value Fireblocks secret key. If you are using AWS Secret Manager, this should be the secret name. [$FIREBLOCKS_SECRET_KEY] + --fireblocks-aws-region value, --fa value AWS region if secret is stored in AWS KMS (default: "us-east-1") [$FIREBLOCKS_AWS_REGION] --fireblocks-base-url value, --fb value Fireblocks base URL [$FIREBLOCKS_BASE_URL] - --fireblocks-vault-account-name value, --fv value Fireblocks vault account name [$FIREBLOCKS_VAULT_ACCOUNT_NAME] - --fireblocks-timeout value, --ft value Fireblocks timeout (default: 30) [$FIREBLOCKS_TIMEOUT] + --fireblocks-secret-key value, --fs value Fireblocks secret key. If you are using AWS Secret Manager, this should be the secret name. [$FIREBLOCKS_SECRET_KEY] --fireblocks-secret-storage-type value, --fst value Fireblocks secret storage type. Supported values are 'plaintext' and 'aws_secret_manager' [$FIREBLOCKS_SECRET_STORAGE_TYPE] - --fireblocks-aws-region value, --fa value AWS region if secret is stored in AWS KMS (default: "us-east-1") [$FIREBLOCKS_AWS_REGION] - --web3signer-url value, -w value URL of the Web3Signer [$WEB3SIGNER_URL] + --fireblocks-timeout value, --ft value Fireblocks timeout (default: 30) [$FIREBLOCKS_TIMEOUT] + --fireblocks-vault-account-name value, --fv value Fireblocks vault account name [$FIREBLOCKS_VAULT_ACCOUNT_NAME] + --network value, -n value Network to use. Currently supports 'holesky' and 'mainnet' (default: "holesky") [$NETWORK] + --output-file value, -o value Output file to write the data [$OUTPUT_FILE] + --output-type value, --ot value Output format of the command. One of 'pretty', 'json' or 'calldata' (default: "pretty") [$OUTPUT_TYPE] + --path-to-key-store value, -k value Path to the key store used to send transactions [$PATH_TO_KEY_STORE] + --proof-store-base-url value, --psbu value Specify the base URL of the proof store. If not provided, the value based on network will be used [$PROOF_STORE_BASE_URL] + --recipient-address value, --ra value Specify the address of the recipient. If this is not provided, the earner address will be used [$RECIPIENT_ADDRESS] + --rewards-coordinator-address value, --rc value Specify the address of the rewards coordinator. If not provided, the address will be used based on provided network [$REWARDS_COORDINATOR_ADDRESS] + --token-addresses value, -t value Specify the addresses of the tokens to claim. Comma separated list of addresses [$TOKEN_ADDRESSES] --verbose, -v Enable verbose logging (default: false) [$VERBOSE] + --web3signer-url value, -w value URL of the Web3Signer [$WEB3SIGNER_URL] --help, -h show help ``` @@ -76,24 +77,25 @@ DESCRIPTION: OPTIONS: - --network value, -n value Network to use. Currently supports 'holesky' and 'mainnet' (default: "holesky") [$NETWORK] - --eth-rpc-url value, -r value URL of the Ethereum RPC [$ETH_RPC_URL] - --output-file value, -o value Output file to write the data [$OUTPUT_FILE] --broadcast, -b Use this flag to broadcast the transaction (default: false) [$BROADCAST] - --earner-address value, --ea value Address of the earner [$REWARDS_EARNER_ADDRESS] - --rewards-coordinator-address value, --rc value Specify the address of the rewards coordinator. If not provided, the address will be used based on provided network [$REWARDS_COORDINATOR_ADDRESS] --claimer-address value, -a value Address of the claimer [$NODE_OPERATOR_CLAIMER_ADDRESS] - --path-to-key-store value, -k value Path to the key store used to send transactions [$PATH_TO_KEY_STORE] + --earner-address value, --ea value Address of the earner [$REWARDS_EARNER_ADDRESS] --ecdsa-private-key value, -e value ECDSA private key hex to send transaction [$ECDSA_PRIVATE_KEY] + --eth-rpc-url value, -r value URL of the Ethereum RPC [$ETH_RPC_URL] --fireblocks-api-key value, --ff value Fireblocks API key [$FIREBLOCKS_API_KEY] - --fireblocks-secret-key value, --fs value Fireblocks secret key. If you are using AWS Secret Manager, this should be the secret name. [$FIREBLOCKS_SECRET_KEY] + --fireblocks-aws-region value, --fa value AWS region if secret is stored in AWS KMS (default: "us-east-1") [$FIREBLOCKS_AWS_REGION] --fireblocks-base-url value, --fb value Fireblocks base URL [$FIREBLOCKS_BASE_URL] - --fireblocks-vault-account-name value, --fv value Fireblocks vault account name [$FIREBLOCKS_VAULT_ACCOUNT_NAME] - --fireblocks-timeout value, --ft value Fireblocks timeout (default: 30) [$FIREBLOCKS_TIMEOUT] + --fireblocks-secret-key value, --fs value Fireblocks secret key. If you are using AWS Secret Manager, this should be the secret name. [$FIREBLOCKS_SECRET_KEY] --fireblocks-secret-storage-type value, --fst value Fireblocks secret storage type. Supported values are 'plaintext' and 'aws_secret_manager' [$FIREBLOCKS_SECRET_STORAGE_TYPE] - --fireblocks-aws-region value, --fa value AWS region if secret is stored in AWS KMS (default: "us-east-1") [$FIREBLOCKS_AWS_REGION] - --web3signer-url value, -w value URL of the Web3Signer [$WEB3SIGNER_URL] + --fireblocks-timeout value, --ft value Fireblocks timeout (default: 30) [$FIREBLOCKS_TIMEOUT] + --fireblocks-vault-account-name value, --fv value Fireblocks vault account name [$FIREBLOCKS_VAULT_ACCOUNT_NAME] + --network value, -n value Network to use. Currently supports 'holesky' and 'mainnet' (default: "holesky") [$NETWORK] + --output-file value, -o value Output file to write the data [$OUTPUT_FILE] + --output-type value, --ot value Output format of the command. One of 'pretty', 'json' or 'calldata' (default: "pretty") [$OUTPUT_TYPE] + --path-to-key-store value, -k value Path to the key store used to send transactions [$PATH_TO_KEY_STORE] + --rewards-coordinator-address value, --rc value Specify the address of the rewards coordinator. If not provided, the address will be used based on provided network [$REWARDS_COORDINATOR_ADDRESS] --verbose, -v Enable verbose logging (default: false) [$VERBOSE] + --web3signer-url value, -w value URL of the Web3Signer [$WEB3SIGNER_URL] --help, -h show help ``` diff --git a/pkg/rewards/claim.go b/pkg/rewards/claim.go index 3b1d54ba..f1b69194 100644 --- a/pkg/rewards/claim.go +++ b/pkg/rewards/claim.go @@ -43,6 +43,7 @@ type ClaimConfig struct { EarnerAddress gethcommon.Address RecipientAddress gethcommon.Address Output string + OutputType string Broadcast bool TokenAddresses []gethcommon.Address RewardsCoordinatorAddress gethcommon.Address @@ -72,6 +73,7 @@ func getClaimFlags() []cli.Flag { &flags.NetworkFlag, &flags.ETHRpcUrlFlag, &flags.OutputFileFlag, + &flags.OutputTypeFlag, &flags.BroadcastFlag, &EarnerAddressFlag, &EnvironmentFlag, @@ -148,11 +150,23 @@ func Claim(cCtx *cli.Context, p utils.Prompter) error { config.TokenAddresses, rootIndex, ) - 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), + } + if config.Broadcast { if config.SignerConfig == nil { return errors.New("signer is required for broadcasting") @@ -185,18 +199,6 @@ func Claim(cCtx *cli.Context, p utils.Prompter) error { return eigenSdkUtils.WrapError("failed to create new writer from config", 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), - } receipt, err := eLWriter.ProcessClaim(ctx, elClaim, config.RecipientAddress) if err != nil { return eigenSdkUtils.WrapError("failed to process claim", err) @@ -205,20 +207,51 @@ func Claim(cCtx *cli.Context, p utils.Prompter) error { logger.Infof("Claim transaction submitted successfully") common.PrintTransactionInfo(receipt.TxHash.String(), config.ChainID) } else { - solidityClaim := claimgen.FormatProofForSolidity(accounts.Root(), claim) - if !common.IsEmptyString(config.Output) { - jsonData, err := json.MarshalIndent(solidityClaim, "", " ") + if config.OutputType == string(common.OutputType_Calldata) { + noSendTxOpts := common.GetNoSendTxOpts(config.EarnerAddress) + _, _, contractBindings, err := elcontracts.BuildClients(elcontracts.Config{ + RewardsCoordinatorAddress: config.RewardsCoordinatorAddress, + }, ethClient, nil, logger, nil) if err != nil { - fmt.Println("Error marshaling JSON:", err) return err } - err = common.WriteToJSON(jsonData, config.Output) + unsignedTx, err := contractBindings.RewardsCoordinator.ProcessClaim(noSendTxOpts, elClaim, config.RecipientAddress) if err != nil { return err } - logger.Infof("Claim written to file: %s", config.Output) + + calldataHex := gethcommon.Bytes2Hex(unsignedTx.Data()) + + if !common.IsEmptyString(config.Output) { + err = common.WriteToFile([]byte(calldataHex), config.Output) + if err != nil { + return err + } + logger.Infof("Call data written to file: %s", config.Output) + } else { + 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 { + fmt.Println("Error marshaling JSON:", err) + return err + } + 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 { + solidityClaim := claimgen.FormatProofForSolidity(accounts.Root(), claim) fmt.Println("------- Claim generated -------") common.PrettyPrintStruct(*solidityClaim) fmt.Println("-------------------------------") @@ -249,6 +282,7 @@ func readAndValidateClaimConfig(cCtx *cli.Context, logger logging.Logger) (*Clai rpcUrl := cCtx.String(flags.ETHRpcUrlFlag.Name) earnerAddress := gethcommon.HexToAddress(cCtx.String(EarnerAddressFlag.Name)) output := cCtx.String(flags.OutputFileFlag.Name) + outputType := cCtx.String(flags.OutputTypeFlag.Name) broadcast := cCtx.Bool(flags.BroadcastFlag.Name) tokenAddresses := cCtx.String(TokenAddressesFlag.Name) tokenAddressArray := stringToAddressArray(strings.Split(tokenAddresses, ",")) @@ -311,6 +345,7 @@ func readAndValidateClaimConfig(cCtx *cli.Context, logger logging.Logger) (*Clai RPCUrl: rpcUrl, EarnerAddress: earnerAddress, Output: output, + OutputType: outputType, Broadcast: broadcast, TokenAddresses: tokenAddressArray, RewardsCoordinatorAddress: gethcommon.HexToAddress(rewardsCoordinatorAddress), diff --git a/pkg/rewards/setclaimer.go b/pkg/rewards/setclaimer.go index a7e6a702..d7cafb40 100644 --- a/pkg/rewards/setclaimer.go +++ b/pkg/rewards/setclaimer.go @@ -32,6 +32,8 @@ type SetClaimerConfig struct { ChainID *big.Int SignerConfig *types.SignerConfig EarnerAddress gethcommon.Address + Output string + OutputType string } func SetClaimerCmd(p utils.Prompter) *cli.Command { @@ -57,6 +59,7 @@ func getSetClaimerFlags() []cli.Flag { &flags.NetworkFlag, &flags.ETHRpcUrlFlag, &flags.OutputFileFlag, + &flags.OutputTypeFlag, &flags.BroadcastFlag, &EarnerAddressFlag, &RewardsCoordinatorAddressFlag, @@ -81,20 +84,47 @@ func SetClaimer(cCtx *cli.Context, p utils.Prompter) error { return fmt.Errorf("set claimer currently unsupported on mainnet") } - if !config.Broadcast { - fmt.Printf( - "Claimer address %s will be set for earner %s\n", - config.ClaimerAddress.String(), - config.EarnerAddress.String(), - ) - return nil - } - ethClient, err := ethclient.Dial(config.RPCUrl) if err != nil { return err } + if !config.Broadcast { + if config.OutputType == string(common.OutputType_Calldata) { + _, _, contractBindings, err := elcontracts.BuildClients(elcontracts.Config{ + RewardsCoordinatorAddress: config.RewardsCoordinatorAddress, + }, ethClient, nil, logger, nil) + if err != nil { + return err + } + + noSendTxOpts := common.GetNoSendTxOpts(config.EarnerAddress) + unsignedTx, err := contractBindings.RewardsCoordinator.SetClaimerFor(noSendTxOpts, config.ClaimerAddress) + if err != nil { + return err + } + calldataHex := gethcommon.Bytes2Hex(unsignedTx.Data()) + if !common.IsEmptyString(config.Output) { + err := common.WriteToFile([]byte(calldataHex), config.Output) + if err != nil { + return err + } + } else { + fmt.Println(calldataHex) + } + } else if config.OutputType == string(common.OutputType_Pretty) { + fmt.Printf( + "Claimer address %s will be set for earner %s\n", + config.ClaimerAddress.String(), + config.EarnerAddress.String(), + ) + } else { + return fmt.Errorf("unsupported output type for this command %s", config.Output) + } + + return nil + } + keyWallet, sender, err := common.GetWallet( *config.SignerConfig, config.EarnerAddress.Hex(), @@ -150,6 +180,8 @@ func readAndValidateSetClaimerConfig(cCtx *cli.Context, logger logging.Logger) ( network := cCtx.String(flags.NetworkFlag.Name) environment := cCtx.String(EnvironmentFlag.Name) rpcUrl := cCtx.String(flags.ETHRpcUrlFlag.Name) + output := cCtx.String(flags.OutputFileFlag.Name) + outputType := cCtx.String(flags.OutputTypeFlag.Name) earnerAddress := gethcommon.HexToAddress(cCtx.String(EarnerAddressFlag.Name)) broadcast := cCtx.Bool(flags.BroadcastFlag.Name) claimerAddress := cCtx.String(ClaimerAddressFlag.Name) @@ -174,7 +206,9 @@ func readAndValidateSetClaimerConfig(cCtx *cli.Context, logger logging.Logger) ( // Get SignerConfig signerConfig, err := common.GetSignerConfig(cCtx, logger) if err != nil { - return nil, err + // We don't want to throw error since people can still use it to generate the + // set claimer calldata/output without broadcasting it + logger.Debugf("Failed to get signer config: %s", err) } return &SetClaimerConfig{ @@ -186,5 +220,7 @@ func readAndValidateSetClaimerConfig(cCtx *cli.Context, logger logging.Logger) ( ChainID: chainID, SignerConfig: signerConfig, EarnerAddress: earnerAddress, + Output: output, + OutputType: outputType, }, nil }