From 91f6429913a2a4736fce2e5c103fab37a7c3247b Mon Sep 17 00:00:00 2001 From: Cameron Cooper Date: Thu, 19 Dec 2024 17:08:41 -0600 Subject: [PATCH] added push_tx and updated get_fee_estimate --- internal/cmd/coinset/get_fee_estimate.go | 173 ++++++++++++++++++++--- internal/cmd/coinset/push_tx.go | 87 ++++++++++++ 2 files changed, 239 insertions(+), 21 deletions(-) create mode 100644 internal/cmd/coinset/push_tx.go diff --git a/internal/cmd/coinset/get_fee_estimate.go b/internal/cmd/coinset/get_fee_estimate.go index 6e4f27b..3c23ec4 100644 --- a/internal/cmd/coinset/get_fee_estimate.go +++ b/internal/cmd/coinset/get_fee_estimate.go @@ -4,55 +4,186 @@ import ( "encoding/json" "fmt" "os" + "strconv" + "strings" "github.com/spf13/cobra" ) +func validateSpendBundle(data map[string]interface{}) error { + aggregatedSig, hasAggSig := data["aggregated_signature"].(string) + if !hasAggSig { + return fmt.Errorf("spend bundle missing or invalid aggregated_signature field") + } + if !strings.HasPrefix(aggregatedSig, "0x") { + return fmt.Errorf("aggregated_signature must start with 0x") + } + + coinSpends, hasCoinSpends := data["coin_spends"].([]interface{}) + if !hasCoinSpends { + return fmt.Errorf("spend bundle missing or invalid coin_spends field") + } + + for i, spend := range coinSpends { + spendMap, ok := spend.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid coin spend at index %d", i) + } + + coin, hasCoin := spendMap["coin"].(map[string]interface{}) + if !hasCoin { + return fmt.Errorf("missing or invalid coin field in coin spend at index %d", i) + } + + required := []string{"amount", "parent_coin_info", "puzzle_hash"} + for _, field := range required { + val, exists := coin[field] + if !exists { + return fmt.Errorf("coin missing required field %s at index %d", field, i) + } + if field != "amount" { + // Validate hex fields + hexStr, ok := val.(string) + if !ok || !strings.HasPrefix(hexStr, "0x") { + return fmt.Errorf("coin field %s must be a hex string starting with 0x at index %d", field, i) + } + } + } + + required = []string{"puzzle_reveal", "solution"} + for _, field := range required { + val, exists := spendMap[field].(string) + if !exists { + return fmt.Errorf("coin spend missing required field %s at index %d", field, i) + } + if !strings.HasPrefix(val, "0x") { + return fmt.Errorf("%s must start with 0x at index %d", field, i) + } + } + } + + return nil +} + func init() { getFeeEstimateCmd.Flags().StringP("file", "f", "", "Path to file containing the spend bundle JSON") + getFeeEstimateCmd.Flags().Int64P("cost", "c", 0, "Cost value for fee estimation") + getFeeEstimateCmd.Flags().String("times", "60,300,600", "Comma-separated list of target times in seconds") rootCmd.AddCommand(getFeeEstimateCmd) } var getFeeEstimateCmd = &cobra.Command{ - Use: "get_fee_estimate [spend_bundle_json]", - Short: "Get fee estimate for a spend bundle", - Long: `Get fee estimate for a spend bundle. The spend bundle can be provided directly as an argument or via a file using the -f flag.`, + Use: "get_fee_estimate [cost_or_spend_bundle]", + Short: "Get fee estimate based on cost or spend bundle", + Long: `Get fee estimate based on either a cost value or spend bundle. +Examples: + coinset get_fee_estimate 20000000 + coinset get_fee_estimate spend_bundle.json + coinset get_fee_estimate '{"coin_spends":[...]}' + coinset get_fee_estimate -f spend_bundle.json + coinset get_fee_estimate -f spend_bundle.json --times 60,120,300 + coinset get_fee_estimate -c 20000000`, } func init() { - var spendBundleJson string - var parsedJson map[string]interface{} + var requestData map[string]interface{} getFeeEstimateCmd.Args = func(cmd *cobra.Command, args []string) error { - fileFlag, _ := cmd.Flags().GetString("file") + dataFile, _ := cmd.Flags().GetString("file") + cost, _ := cmd.Flags().GetInt64("cost") + timesStr, _ := cmd.Flags().GetString("times") - if (len(args) == 0 && fileFlag == "") || (len(args) > 0 && fileFlag != "") { - return fmt.Errorf("must provide either spend bundle JSON as argument or file path with -f flag, but not both") + times := []int64{} + for _, t := range strings.Split(timesStr, ",") { + time, err := strconv.ParseInt(strings.TrimSpace(t), 10, 64) + if err != nil { + return fmt.Errorf("invalid target time: %s", t) + } + times = append(times, time) } - if len(args) > 0 { - if len(args) > 1 { - return fmt.Errorf("too many arguments provided. Did you forget to quote your JSON? Expected: get_fee_estimate '' or get_fee_estimate -f ") - } - spendBundleJson = args[0] + requestData = map[string]interface{}{ + "target_times": times, + } + + if dataFile != "" && cost != 0 { + return fmt.Errorf("cannot specify both -f and -c flags") + } + if dataFile != "" && len(args) > 0 { + return fmt.Errorf("cannot provide both -f flag and direct argument") + } + if cost != 0 && len(args) > 0 { + return fmt.Errorf("cannot provide both -c flag and direct argument") } - if fileFlag != "" { - data, err := os.ReadFile(fileFlag) + if dataFile != "" { + data, err := os.ReadFile(dataFile) if err != nil { - return fmt.Errorf("unable to read file %s: %v", fileFlag, err) + return fmt.Errorf("unable to read file %s: %v", dataFile, err) + } + + var spendBundle map[string]interface{} + if err := json.Unmarshal(data, &spendBundle); err != nil { + return fmt.Errorf("invalid JSON in file: %v", err) } - spendBundleJson = string(data) - } - if err := json.Unmarshal([]byte(spendBundleJson), &parsedJson); err != nil { - return fmt.Errorf("invalid JSON: %v", err) + if err := validateSpendBundle(spendBundle); err != nil { + return fmt.Errorf("invalid spend bundle in file: %v", err) + } + + requestData["spend_bundle"] = spendBundle + } else { + if cost != 0 { + requestData = map[string]interface{}{ + "cost": cost, + "target_times": times, + } + return nil + } + + if len(args) != 1 { + return fmt.Errorf("must provide either a cost value or spend bundle JSON, or use -f or -c flags") + } + + if parsedCost, err := strconv.ParseInt(args[0], 10, 64); err == nil { + requestData = map[string]interface{}{ + "cost": parsedCost, + "target_times": times, + } + } else { + var spendBundle map[string]interface{} + if err := json.Unmarshal([]byte(args[0]), &spendBundle); err == nil { + if err := validateSpendBundle(spendBundle); err != nil { + return fmt.Errorf("invalid spend bundle structure: %v", err) + } + requestData["spend_bundle"] = spendBundle + } else { + if _, err := os.Stat(args[0]); err == nil { + data, err := os.ReadFile(args[0]) + if err != nil { + return fmt.Errorf("unable to read file %s: %v", args[0], err) + } + + if err := json.Unmarshal(data, &spendBundle); err != nil { + return fmt.Errorf("invalid JSON in file %s: %v", args[0], err) + } + + if err := validateSpendBundle(spendBundle); err != nil { + return fmt.Errorf("invalid spend bundle in file %s: %v", args[0], err) + } + + requestData["spend_bundle"] = spendBundle + } else { + return fmt.Errorf("argument must be either a valid number (cost), valid JSON (spend bundle), or a path to a JSON file containing a spend bundle") + } + } + } } return nil } getFeeEstimateCmd.Run = func(cmd *cobra.Command, args []string) { - makeRequest("get_fee_estimate", parsedJson) + makeRequest("get_fee_estimate", requestData) } } diff --git a/internal/cmd/coinset/push_tx.go b/internal/cmd/coinset/push_tx.go new file mode 100644 index 0000000..03e6d02 --- /dev/null +++ b/internal/cmd/coinset/push_tx.go @@ -0,0 +1,87 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/spf13/cobra" +) + +func init() { + getPushTxCmd.Flags().StringP("file", "f", "", "Path to file containing the spend bundle JSON") + rootCmd.AddCommand(getPushTxCmd) +} + +var getPushTxCmd = &cobra.Command{ + Use: "push_tx [spend_bundle_json]", + Short: "Push spend bundle to the mempool", + Long: `Push spend bundle to the mempool. The spend bundle can be provided in three ways: +1. As a JSON string argument: push_tx '{"aggregated_signature":"0x...","coin_spends":[...]}' +2. As a file path argument: push_tx ./spend_bundle.json +3. Using the -f flag: push_tx -f ./spend_bundle.json`, +} + +func init() { + var parsedJson map[string]interface{} + + getPushTxCmd.Args = func(cmd *cobra.Command, args []string) error { + fileFlag, _ := cmd.Flags().GetString("file") + + if fileFlag != "" && len(args) > 0 { + return fmt.Errorf("cannot provide both -f flag and direct argument") + } + + if fileFlag == "" && len(args) == 0 { + return fmt.Errorf("must provide spend bundle either as argument or with -f flag") + } + + if len(args) > 1 { + return fmt.Errorf("too many arguments provided. Did you forget to quote your JSON? Expected: push_tx '' or push_tx -f ") + } + + var spendBundleJson string + + if fileFlag != "" { + data, err := os.ReadFile(fileFlag) + if err != nil { + return fmt.Errorf("unable to read file %s: %v", fileFlag, err) + } + spendBundleJson = string(data) + } else { + if err := json.Unmarshal([]byte(args[0]), &parsedJson); err == nil { + if err := validateSpendBundle(parsedJson); err != nil { + return fmt.Errorf("invalid spend bundle structure: %v", err) + } + return nil + } + + if _, err := os.Stat(args[0]); err == nil { + data, err := os.ReadFile(args[0]) + if err != nil { + return fmt.Errorf("unable to read file %s: %v", args[0], err) + } + spendBundleJson = string(data) + } else { + return fmt.Errorf("argument must be either valid JSON spend bundle or path to a JSON file containing a spend bundle") + } + } + + if err := json.Unmarshal([]byte(spendBundleJson), &parsedJson); err != nil { + return fmt.Errorf("invalid JSON: %v", err) + } + + if err := validateSpendBundle(parsedJson); err != nil { + return fmt.Errorf("invalid spend bundle structure: %v", err) + } + + return nil + } + + getPushTxCmd.Run = func(cmd *cobra.Command, args []string) { + requestData := map[string]interface{}{ + "spend_bundle": parsedJson, + } + makeRequest("push_tx", requestData) + } +}