From cefa7a927f67120040269c955e9cc1a445e70779 Mon Sep 17 00:00:00 2001 From: Cameron Cooper Date: Fri, 16 Jan 2026 15:51:08 -0600 Subject: [PATCH] added describe option --- go.mod | 1 + go.sum | 2 + internal/cmd/coinset/root.go | 2 + internal/cmd/coinset/util.go | 220 +++++++++++++++++++++++++++++++++++ 4 files changed, 225 insertions(+) diff --git a/go.mod b/go.mod index ba1662b..231d7f5 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( ) require ( + github.com/dustin/go-humanize v1.0.1 // indirect github.com/fatih/color v1.13.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/go.sum b/go.sum index 8ceb9d9..4de8c58 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/chia-network/go-chia-libs v0.15.0/go.mod h1:npTqaFSjTdMxE7hc0LOmWJmWG github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= diff --git a/internal/cmd/coinset/root.go b/internal/cmd/coinset/root.go index e20b87d..07d645e 100644 --- a/internal/cmd/coinset/root.go +++ b/internal/cmd/coinset/root.go @@ -33,6 +33,7 @@ var testnet bool var local bool var raw bool var api string +var describe bool var version = "dev" func init() { @@ -42,5 +43,6 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&local, "local", "l", false, "Use the local full node") rootCmd.PersistentFlags().StringVarP(&api, "api", "a", "", "api host to use") rootCmd.PersistentFlags().BoolVarP(&raw, "raw", "r", false, "display output in raw json") + rootCmd.PersistentFlags().BoolVarP(&describe, "describe", "d", false, "add human-readable descriptions to output fields") rootCmd.MarkFlagsMutuallyExclusive("mainnet", "testnet", "api") } diff --git a/internal/cmd/coinset/util.go b/internal/cmd/coinset/util.go index 391018f..20d0147 100644 --- a/internal/cmd/coinset/util.go +++ b/internal/cmd/coinset/util.go @@ -4,14 +4,18 @@ import ( "encoding/json" "fmt" "log" + "math/big" "net/url" "regexp" "strconv" + "sync" + "time" "github.com/TylerBrock/colorjson" "github.com/chia-network/go-chia-libs/pkg/bech32m" "github.com/chia-network/go-chia-libs/pkg/rpc" "github.com/chia-network/go-chia-libs/pkg/rpcinterface" + "github.com/dustin/go-humanize" "github.com/itchyny/gojq" ) @@ -144,6 +148,217 @@ func makeRequest(path string, jsonData map[string]interface{}) { printJson(jsonResponse) } +// Cache for block records (height -> timestamp) +var blockRecordCache = make(map[int]int64) +var blockRecordCacheMutex sync.RWMutex + +// Helper function to get current block height (cached) +var cachedBlockHeight *int +var cachedBlockHeightTime time.Time +var cachedBlockHeightMutex sync.RWMutex + +func getBlockTimestamp(height int) (int64, error) { + // Check cache first + blockRecordCacheMutex.RLock() + if timestamp, ok := blockRecordCache[height]; ok { + blockRecordCacheMutex.RUnlock() + return timestamp, nil + } + blockRecordCacheMutex.RUnlock() + + // Fetch block record + jsonData := map[string]interface{}{ + "height": height, + } + + jsonResponse, err := doRpc("get_block_record_by_height", jsonData) + if err != nil { + return 0, err + } + + var response map[string]interface{} + if err := json.Unmarshal(jsonResponse, &response); err != nil { + return 0, err + } + + blockRecord, ok := response["block_record"].(map[string]interface{}) + if !ok { + return 0, fmt.Errorf("invalid response format") + } + + timestamp, ok := blockRecord["timestamp"].(float64) + if !ok { + return 0, fmt.Errorf("timestamp not found") + } + + timestampInt := int64(timestamp) + + // Cache it + blockRecordCacheMutex.Lock() + blockRecordCache[height] = timestampInt + blockRecordCacheMutex.Unlock() + + return timestampInt, nil +} + +func getCurrentBlockHeight() (int, error) { + cachedBlockHeightMutex.RLock() + if cachedBlockHeight != nil && time.Since(cachedBlockHeightTime) < 30*time.Second { + height := *cachedBlockHeight + cachedBlockHeightMutex.RUnlock() + return height, nil + } + cachedBlockHeightMutex.RUnlock() + + jsonResponse, err := doRpc("get_blockchain_state", nil) + if err != nil { + return 0, err + } + + var response map[string]interface{} + if err := json.Unmarshal(jsonResponse, &response); err != nil { + return 0, err + } + + peak, ok := response["peak"].(map[string]interface{}) + if !ok { + return 0, fmt.Errorf("invalid response format") + } + + height, ok := peak["height"].(float64) + if !ok { + return 0, fmt.Errorf("height not found") + } + + heightInt := int(height) + cachedBlockHeightMutex.Lock() + cachedBlockHeight = &heightInt + cachedBlockHeightTime = time.Now() + cachedBlockHeightMutex.Unlock() + return heightInt, nil +} + +func formatAmount(amount interface{}) string { + var amountInt int64 + switch v := amount.(type) { + case float64: + amountInt = int64(v) + case int64: + amountInt = v + case int: + amountInt = int64(v) + default: + return "" + } + + // 1 XCH = 1 trillion mojos + trillion := big.NewInt(1000000000000) + amountBig := big.NewInt(amountInt) + + // Divide by trillion to get XCH + xch := new(big.Float).SetInt(amountBig) + xch.Quo(xch, new(big.Float).SetInt(trillion)) + + return fmt.Sprintf("%.12f XCH", xch) +} + +func formatTimestampWithRelative(timestamp interface{}) string { + var ts int64 + switch v := timestamp.(type) { + case float64: + ts = int64(v) + case int64: + ts = v + case int: + ts = int64(v) + default: + return "" + } + + if ts == 0 { + return "Never" + } + + blockTime := time.Unix(ts, 0).Local() + relativeTime := humanize.Time(blockTime) + absoluteTime := blockTime.Format("2006-01-02 15:04:05") + + return fmt.Sprintf("%s, %s", relativeTime, absoluteTime) +} + +func formatBlockHeight(height interface{}) string { + var heightInt int + switch v := height.(type) { + case float64: + heightInt = int(v) + case int64: + heightInt = int(v) + case int: + heightInt = v + default: + return "" + } + + // Always fetch the block timestamp to show relative time + timestamp, err := getBlockTimestamp(heightInt) + if err != nil { + // Fallback to estimation if fetch fails + currentHeight, err := getCurrentBlockHeight() + if err != nil { + return fmt.Sprintf("Block %d", heightInt) + } + diff := currentHeight - heightInt + approxMinutes := diff * 18 / 60 + if approxMinutes < 60 { + return fmt.Sprintf("~%d minutes ago", approxMinutes) + } + approxHours := approxMinutes / 60 + if approxHours < 24 { + return fmt.Sprintf("~%d hours ago", approxHours) + } + approxDays := approxHours / 24 + return fmt.Sprintf("~%d days ago", approxDays) + } + + // Use actual timestamp + return formatTimestampWithRelative(timestamp) +} + +func addDescriptions(data interface{}) interface{} { + switch v := data.(type) { + case map[string]interface{}: + result := make(map[string]interface{}) + for key, value := range v { + result[key] = addDescriptions(value) + + // Add descriptions for specific fields + switch key { + case "amount": + if desc := formatAmount(value); desc != "" { + result[key+"_description"] = desc + } + case "timestamp": + if desc := formatTimestampWithRelative(value); desc != "" { + result[key+"_description"] = desc + } + case "confirmed_block_index", "spent_block_index", "block_index", "height": + if desc := formatBlockHeight(value); desc != "" { + result[key+"_description"] = desc + } + } + } + return result + case []interface{}: + result := make([]interface{}, len(v)) + for i, item := range v { + result[i] = addDescriptions(item) + } + return result + default: + return v + } +} + func printJson(jsonBytes []byte) { query, err := gojq.Parse(jq) if err != nil { @@ -153,6 +368,11 @@ func printJson(jsonBytes []byte) { var jsonStrings map[string]interface{} json.Unmarshal(jsonBytes, &jsonStrings) + // Add descriptions if flag is set + if describe { + jsonStrings = addDescriptions(jsonStrings).(map[string]interface{}) + } + iter := query.Run(jsonStrings) for { v, ok := iter.Next()