Skip to content

Commit

Permalink
added push_tx and updated get_fee_estimate (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
cameroncooper authored Dec 19, 2024
1 parent feae6e1 commit 4ca8cde
Show file tree
Hide file tree
Showing 2 changed files with 239 additions and 21 deletions.
173 changes: 152 additions & 21 deletions internal/cmd/coinset/get_fee_estimate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<json>' or get_fee_estimate -f <filename>")
}
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)
}
}
87 changes: 87 additions & 0 deletions internal/cmd/coinset/push_tx.go
Original file line number Diff line number Diff line change
@@ -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 '<json>' or push_tx -f <filename>")
}

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

0 comments on commit 4ca8cde

Please sign in to comment.