From 6cbaf2b5b340e961fee8fe1eb56e167a3a386e95 Mon Sep 17 00:00:00 2001 From: Cameron Cooper Date: Wed, 25 Feb 2026 00:32:55 -0600 Subject: [PATCH 1/9] added transaction and balance commands --- README.md | 55 ++++- internal/cmd/coinset/api_common.go | 61 ++++++ internal/cmd/coinset/events.go | 168 ++++++++++++++- .../cmd/coinset/get_additions_and_removals.go | 37 ++++ internal/cmd/coinset/get_all_mempool_items.go | 18 +- .../cmd/coinset/get_all_mempool_tx_ids.go | 18 +- .../cmd/coinset/get_block_count_metrics.go | 18 +- .../cmd/coinset/get_block_record_by_height.go | 45 ++-- internal/cmd/coinset/get_block_records.go | 56 ++--- .../cmd/coinset/get_block_transactions.go | 48 +++++ internal/cmd/coinset/get_blockchain_state.go | 18 +- internal/cmd/coinset/get_blocks.go | 56 ++--- internal/cmd/coinset/get_cat_assets_by_p2.go | 43 ++++ .../cmd/coinset/get_cat_balances_by_p2.go | 58 +++++ .../cmd/coinset/get_cat_coins_by_asset_id.go | 43 ++++ .../get_cat_coins_by_p2_and_asset_id.go | 48 +++++ .../coinset/get_clawback_coins_by_receiver.go | 38 ++++ .../cmd/coinset/get_coin_records_by_hint.go | 21 ++ .../cmd/coinset/get_coin_records_by_hints.go | 21 ++ .../cmd/coinset/get_coin_records_by_names.go | 105 +++++---- .../coinset/get_coin_records_by_parent_ids.go | 105 +++++---- .../get_coin_records_by_puzzle_hash.go | 23 ++ .../get_coin_records_by_puzzle_hashes.go | 21 ++ internal/cmd/coinset/get_fee_estimate.go | 201 +++++++++++------- internal/cmd/coinset/get_fee_tiers_by_cost.go | 38 ++++ .../coinset/get_latest_nft_coin_by_nft_id.go | 30 +++ .../cmd/coinset/get_mempool_item_by_tx_id.go | 42 ++-- internal/cmd/coinset/get_network_info.go | 18 +- internal/cmd/coinset/get_nft_balance_by_p2.go | 25 +++ .../cmd/coinset/get_nft_coins_by_nft_id.go | 46 ++++ internal/cmd/coinset/get_nft_ids_by_p2.go | 43 ++++ internal/cmd/coinset/get_offer.go | 25 +++ .../cmd/coinset/get_offers_by_cat_asset_id.go | 58 +++++ internal/cmd/coinset/get_offers_by_nft_id.go | 57 +++++ internal/cmd/coinset/get_offers_by_p2.go | 50 +++++ .../coinset/get_pending_transactions_by_p2.go | 38 ++++ ...get_pending_transactions_by_puzzle_hash.go | 38 ++++ .../cmd/coinset/get_raw_transaction_by_id.go | 25 +++ internal/cmd/coinset/get_singleton_info.go | 25 +++ internal/cmd/coinset/get_transaction.go | 25 +++ .../get_transactions_by_cat_asset_id.go | 38 ++++ .../coinset/get_transactions_by_coin_name.go | 32 +++ .../cmd/coinset/get_transactions_by_nft_id.go | 41 ++++ .../cmd/coinset/get_transactions_by_p2.go | 38 ++++ .../get_transactions_by_puzzle_hash.go | 38 ++++ .../coinset/get_unfinished_block_headers.go | 18 +- internal/cmd/coinset/get_xch_balance_by_p2.go | 25 +++ internal/cmd/coinset/util.go | 4 + internal/cmd/coinset/version.go | 20 +- 49 files changed, 1826 insertions(+), 336 deletions(-) create mode 100644 internal/cmd/coinset/api_common.go create mode 100644 internal/cmd/coinset/get_block_transactions.go create mode 100644 internal/cmd/coinset/get_cat_assets_by_p2.go create mode 100644 internal/cmd/coinset/get_cat_balances_by_p2.go create mode 100644 internal/cmd/coinset/get_cat_coins_by_asset_id.go create mode 100644 internal/cmd/coinset/get_cat_coins_by_p2_and_asset_id.go create mode 100644 internal/cmd/coinset/get_clawback_coins_by_receiver.go create mode 100644 internal/cmd/coinset/get_fee_tiers_by_cost.go create mode 100644 internal/cmd/coinset/get_latest_nft_coin_by_nft_id.go create mode 100644 internal/cmd/coinset/get_nft_balance_by_p2.go create mode 100644 internal/cmd/coinset/get_nft_coins_by_nft_id.go create mode 100644 internal/cmd/coinset/get_nft_ids_by_p2.go create mode 100644 internal/cmd/coinset/get_offer.go create mode 100644 internal/cmd/coinset/get_offers_by_cat_asset_id.go create mode 100644 internal/cmd/coinset/get_offers_by_nft_id.go create mode 100644 internal/cmd/coinset/get_offers_by_p2.go create mode 100644 internal/cmd/coinset/get_pending_transactions_by_p2.go create mode 100644 internal/cmd/coinset/get_pending_transactions_by_puzzle_hash.go create mode 100644 internal/cmd/coinset/get_raw_transaction_by_id.go create mode 100644 internal/cmd/coinset/get_singleton_info.go create mode 100644 internal/cmd/coinset/get_transaction.go create mode 100644 internal/cmd/coinset/get_transactions_by_cat_asset_id.go create mode 100644 internal/cmd/coinset/get_transactions_by_coin_name.go create mode 100644 internal/cmd/coinset/get_transactions_by_nft_id.go create mode 100644 internal/cmd/coinset/get_transactions_by_p2.go create mode 100644 internal/cmd/coinset/get_transactions_by_puzzle_hash.go create mode 100644 internal/cmd/coinset/get_xch_balance_by_p2.go diff --git a/README.md b/README.md index 9b90b93..d7e3428 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,12 @@ $ coinset get_coin_records_by_parent_ids 0xa908ee64a5821b7bda5d798c053a79c8b3d7c } ``` +### Coinset API Host + +All RPC endpoints (full node and Coinset query APIs) use the same API host configuration. + +- Override with `--api` + ## Available Commands The following table shows all available CLI commands organized by functionality: @@ -104,22 +110,59 @@ The following table shows all available CLI commands organized by functionality: | | `get_unfinished_block_headers` | Retrieve unfinished block headers | | **Blockchain State & Network** | `get_blockchain_state` | Retrieve the current blockchain state | | | `get_network_info` | Retrieve information about the current network | -| | `get_network_space ` | Retrieve network space between two blocks | +| | `get_network_space ` | Retrieve network space between two blocks | | | `get_block_count_metrics` | Retrieve block count metrics | | **Mempool Operations** | `get_all_mempool_items` | Retrieve all mempool items | | | `get_all_mempool_tx_ids` | Retrieve all mempool transaction IDs | | | `get_mempool_item_by_tx_id ` | Retrieve a specific mempool item by transaction ID | | | `get_mempool_items_by_coin_name ` | Retrieve mempool items by coin name | -| **Transaction Operations** | `push_tx [spend_bundle_json]` | Push a spend bundle to the mempool | -| | `get_fee_estimate ` | Get fee estimate for transaction | +| **Transaction Operations (Full Node)** | `push_tx [spend_bundle_json]` | Push a spend bundle to the mempool | +| | `get_fee_estimate [cost_or_spend_bundle]` | Get fee estimate by cost, spend bundle, or spend type | +| | `get_fee_tiers_by_cost ` | Get economy/standard/urgent fee tiers | | | `get_additions_and_removals ` | Get additions and removals for a block | | | `get_aggsig_additional_data ` | Get aggregate signature additional data | -| **Puzzle & Solution** | `get_puzzle_and_solution ` | Retrieve puzzle and solution for a coin | -| | `get_puzzle_and_solution_with_conditions ` | Retrieve puzzle and solution with conditions for a coin | +| **Coinset Transactions** | `get_transaction ` | Get a transaction by tx id | +| | `get_transactions_by_coin_name ` | Get created/spent transaction references for a coin | +| | `get_raw_transaction_by_id ` | Get raw mempool-style transaction details | +| | `get_block_transactions ` | List transactions for a block | +| | `get_transactions_by_p2 ` | List confirmed transactions by p2 | +| | `get_pending_transactions_by_p2 ` | List pending transactions by p2 | +| | `get_transactions_by_puzzle_hash ` | Alias for transactions by p2 | +| | `get_pending_transactions_by_puzzle_hash ` | Alias for pending transactions by p2 | +| | `get_transactions_by_cat_asset_id ` | List transactions by CAT asset id | +| | `get_transactions_by_nft_id ` | List transactions by NFT id | +| **Coinset Offers** | `get_offer ` | Get offer summary/state by offer id | +| | `get_offers_by_p2 ` | List offers by maker p2 | +| | `get_offers_by_cat_asset_id ` | List offers by CAT asset id | +| | `get_offers_by_nft_id ` | List offers by NFT id | +| **Coinset Balances, Assets & NFT** | `get_xch_balance_by_p2 ` | Get XCH balance by p2 | +| | `get_cat_balances_by_p2 ` | List CAT balances by p2 | +| | `get_nft_balance_by_p2 ` | Get NFT balance by p2 | +| | `get_clawback_coins_by_receiver ` | List clawback coins by receiver p2 | +| | `get_cat_assets_by_p2 ` | List CAT asset ids by p2 | +| | `get_cat_coins_by_asset_id ` | List CAT coins by asset id | +| | `get_cat_coins_by_p2_and_asset_id ` | List CAT coins by p2 and asset id | +| | `get_nft_ids_by_p2 ` | List NFT ids by p2 | +| | `get_nft_coins_by_nft_id ` | List NFT coins by NFT id | +| | `get_latest_nft_coin_by_nft_id ` | Get latest NFT coin by NFT id | +| | `get_singleton_info ` | Get singleton info by launcher id | +| **Puzzle & Solution** | `get_puzzle_and_solution ` | Retrieve puzzle and solution for a coin | +| | `get_puzzle_and_solution_with_conditions ` | Retrieve puzzle and solution with conditions for a coin | | **Memos & Metadata** | `get_memos_by_coin_name ` | Retrieve memos associated with a coin | -| **Events & Real-time** | `events [type]` | Connect to WebSocket and display events (peak, transaction, offer) | +| **Events & Real-time** | `events [type]` | Connect to WebSocket and display events (`peak`, `transaction`, `offer`, `balance`, `dashboard`) | | **Utility** | `version` | Display the version number of coinset | +### Smoke Test Checklist + +- `coinset get_coin_records_by_puzzle_hash --limit 5 --order desc` +- `coinset get_additions_and_removals --limit 10 --filter additions` +- `coinset get_fee_estimate --spend-type send_xch_transaction --times 60,300 --spend-count 1` +- `coinset get_fee_tiers_by_cost 9401710` +- `coinset --api https://api.coinset.org get_transaction ` +- `coinset get_offers_by_p2 --status open --limit 20` +- `coinset get_cat_balances_by_p2 --limit 20` +- `coinset events --events peak,transaction --tx-status confirmed` + ### JQ Filtering diff --git a/internal/cmd/coinset/api_common.go b/internal/cmd/coinset/api_common.go new file mode 100644 index 0000000..f0808bd --- /dev/null +++ b/internal/cmd/coinset/api_common.go @@ -0,0 +1,61 @@ +package cmd + +import "fmt" + +func applyPaginationOptions(jsonData map[string]interface{}, cursor string, limit int, order string) error { + if cursor != "" { + jsonData["cursor"] = cursor + } + + if limit < 0 { + return fmt.Errorf("limit must be >= 1 when provided") + } + if limit > 0 { + jsonData["limit"] = limit + } + + if order != "" { + if !isAscOrDesc(order) { + return fmt.Errorf(`invalid order %q: must be "asc" or "desc"`, order) + } + jsonData["order"] = order + } + + return nil +} + +func normalizeHexArg(value, fieldName string) (string, error) { + if !isHex(value) { + return "", fmt.Errorf("invalid hex value specified for %s: %s", fieldName, value) + } + return formatHex(value), nil +} + +func normalizeP2Arg(value string) (string, error) { + return convertAddressOrPuzzleHash(value) +} + +func normalizeMaybeHex(value string) string { + if isHex(value) { + return formatHex(value) + } + return value +} + +func isValidOfferStatus(status string) bool { + switch status { + case "open", "pending", "confirmed", "cancel_pending", "cancelled", "expired": + return true + default: + return false + } +} + +func isValidOfferFilter(filter string) bool { + switch filter { + case "all", "requested", "offered": + return true + default: + return false + } +} diff --git a/internal/cmd/coinset/events.go b/internal/cmd/coinset/events.go index f8a1f54..04a0228 100644 --- a/internal/cmd/coinset/events.go +++ b/internal/cmd/coinset/events.go @@ -4,14 +4,22 @@ import ( "encoding/json" "fmt" "log" + "net/url" "os" "os/signal" + "strings" "time" "github.com/gorilla/websocket" "github.com/spf13/cobra" ) +var ( + eventsFilter string + eventsTxIDs []string + eventsTxStatus string +) + var eventsCmd = &cobra.Command{ Use: "events [type]", Short: "Connect to WebSocket and display events", @@ -20,31 +28,160 @@ Optionally filter by event type. Valid types are: - peak - transaction - offer + - balance + - dashboard If no type is specified, all events will be displayed.`, - ValidArgs: []string{"peak", "transaction", "offer"}, - Args: cobra.MaximumNArgs(1), - Run: runEvents, + ValidArgs: []string{"peak", "transaction", "offer", "balance", "dashboard"}, + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.MaximumNArgs(1)(cmd, args); err != nil { + return err + } + + if len(args) > 0 && eventsFilter != "" { + return fmt.Errorf("use either positional type or --events, not both") + } + + if len(args) > 0 && !isValidEventType(args[0]) { + return fmt.Errorf("invalid event type: %s", args[0]) + } + + if err := validateEventFilter(eventsFilter); err != nil { + return err + } + if err := validateTxStatusFilter(eventsTxStatus); err != nil { + return err + } + + return nil + }, + Run: runEvents, } func init() { + eventsCmd.Flags().StringVar(&eventsFilter, "events", "", "Comma-separated event types to subscribe to") + eventsCmd.Flags().StringArrayVar(&eventsTxIDs, "tx-id", []string{}, "Transaction id filter (repeatable)") + eventsCmd.Flags().StringVar(&eventsTxStatus, "tx-status", "", "Comma-separated tx statuses: pending,confirmed,removed") rootCmd.AddCommand(eventsCmd) } -type Event struct { +type WsMessage struct { Type string `json:"type"` Data json.RawMessage `json:"data"` } +type WsEnvelope struct { + Network string `json:"network"` + Region string `json:"region"` + InstanceID string `json:"instance_id"` + Seq int64 `json:"seq"` + Message WsMessage `json:"message"` +} + +func isValidEventType(eventType string) bool { + switch eventType { + case "peak", "transaction", "offer", "balance", "dashboard": + return true + default: + return false + } +} + +func validateEventFilter(value string) error { + if value == "" { + return nil + } + + for _, item := range strings.Split(value, ",") { + eventType := strings.TrimSpace(item) + if eventType == "" { + continue + } + if !isValidEventType(eventType) { + return fmt.Errorf("invalid event type in --events: %s", eventType) + } + } + + return nil +} + +func validateTxStatusFilter(value string) error { + if value == "" { + return nil + } + + for _, item := range strings.Split(value, ",") { + status := strings.TrimSpace(item) + if status == "" { + continue + } + if status != "pending" && status != "confirmed" && status != "removed" { + return fmt.Errorf("invalid tx status in --tx-status: %s", status) + } + } + + return nil +} + +func websocketEndpoint(singleEventType string) (string, error) { + baseUrl, err := url.Parse(apiRoot()) + if err != nil { + return "", fmt.Errorf("invalid API URL: %v", err) + } + + scheme := "wss" + switch baseUrl.Scheme { + case "http": + scheme = "ws" + case "ws", "wss": + scheme = baseUrl.Scheme + } + + basePath := strings.TrimRight(baseUrl.Path, "/") + wsPath := "/ws" + if basePath != "" { + wsPath = basePath + "/ws" + } + + wsURL := &url.URL{ + Scheme: scheme, + Host: baseUrl.Host, + Path: wsPath, + } + + queryValues := wsURL.Query() + + if eventsFilter != "" { + queryValues.Set("events", eventsFilter) + } else if singleEventType != "" { + queryValues.Set("events", singleEventType) + } else { + queryValues.Set("events", "peak,transaction,offer,balance,dashboard") + } + + for _, txID := range eventsTxIDs { + trimmed := strings.TrimSpace(txID) + if trimmed != "" { + queryValues.Add("tx_id", trimmed) + } + } + + if eventsTxStatus != "" { + queryValues.Set("tx_status", eventsTxStatus) + } + + wsURL.RawQuery = queryValues.Encode() + return wsURL.String(), nil +} + func runEvents(cmd *cobra.Command, args []string) { eventType := "" if len(args) > 0 { eventType = args[0] } - endpoint := fmt.Sprintf("wss://%s/ws", apiHost()) - - if api != "" { - endpoint = api + endpoint, err := websocketEndpoint(eventType) + if err != nil { + log.Fatal(err.Error()) } c, _, err := websocket.DefaultDialer.Dial(endpoint, nil) @@ -70,13 +207,22 @@ func runEvents(cmd *cobra.Command, args []string) { return } - var event Event - if err := json.Unmarshal(message, &event); err != nil { + var envelope WsEnvelope + if err := json.Unmarshal(message, &envelope); err != nil { fmt.Println(err) continue } - if eventType == "" || event.Type == eventType { + messageType := envelope.Message.Type + if messageType == "" { + // Backward compatibility: older stream format sent top-level type/data. + var legacyMessage WsMessage + if err := json.Unmarshal(message, &legacyMessage); err == nil { + messageType = legacyMessage.Type + } + } + + if eventType == "" || messageType == eventType { printJson([]byte(message)) } } diff --git a/internal/cmd/coinset/get_additions_and_removals.go b/internal/cmd/coinset/get_additions_and_removals.go index 8cc760d..33e6377 100644 --- a/internal/cmd/coinset/get_additions_and_removals.go +++ b/internal/cmd/coinset/get_additions_and_removals.go @@ -1,10 +1,23 @@ package cmd import ( + "fmt" + "github.com/spf13/cobra" ) +var ( + getAdditionsAndRemovalsCursor string + getAdditionsAndRemovalsLimit int + getAdditionsAndRemovalsOrder string + getAdditionsAndRemovalsFilter string +) + func init() { + getAdditionsAndRemovalsCmd.Flags().StringVar(&getAdditionsAndRemovalsCursor, "cursor", "", "Pagination cursor from a previous truncated response") + getAdditionsAndRemovalsCmd.Flags().IntVar(&getAdditionsAndRemovalsLimit, "limit", 0, "Maximum number of records to return") + getAdditionsAndRemovalsCmd.Flags().StringVar(&getAdditionsAndRemovalsOrder, "order", "", `Record order: "asc" or "desc"`) + getAdditionsAndRemovalsCmd.Flags().StringVar(&getAdditionsAndRemovalsFilter, "filter", "", `Filter records: "all", "additions", or "removals"`) rootCmd.AddCommand(getAdditionsAndRemovalsCmd) } @@ -18,6 +31,18 @@ var getAdditionsAndRemovalsCmd = &cobra.Command{ if err != nil { return err } + if getAdditionsAndRemovalsLimit < 0 { + return fmt.Errorf("limit must be >= 1 when provided") + } + if getAdditionsAndRemovalsOrder != "" && !isAscOrDesc(getAdditionsAndRemovalsOrder) { + return fmt.Errorf(`invalid order %q: must be "asc" or "desc"`, getAdditionsAndRemovalsOrder) + } + if getAdditionsAndRemovalsFilter != "" && + getAdditionsAndRemovalsFilter != "all" && + getAdditionsAndRemovalsFilter != "additions" && + getAdditionsAndRemovalsFilter != "removals" { + return fmt.Errorf(`invalid filter %q: must be "all", "additions", or "removals"`, getAdditionsAndRemovalsFilter) + } return nil }, Short: "Retrieves the additions and removals for a certain block", @@ -26,6 +51,18 @@ var getAdditionsAndRemovalsCmd = &cobra.Command{ headerHash, _ := convertHeightOrHeaderHash(args[0]) jsonData := map[string]interface{}{} jsonData["header_hash"] = headerHash + if getAdditionsAndRemovalsCursor != "" { + jsonData["cursor"] = getAdditionsAndRemovalsCursor + } + if getAdditionsAndRemovalsLimit > 0 { + jsonData["limit"] = getAdditionsAndRemovalsLimit + } + if getAdditionsAndRemovalsOrder != "" { + jsonData["order"] = getAdditionsAndRemovalsOrder + } + if getAdditionsAndRemovalsFilter != "" { + jsonData["filter"] = getAdditionsAndRemovalsFilter + } makeRequest("get_additions_and_removals", jsonData) }, } diff --git a/internal/cmd/coinset/get_all_mempool_items.go b/internal/cmd/coinset/get_all_mempool_items.go index 34bc411..2e4d9fe 100644 --- a/internal/cmd/coinset/get_all_mempool_items.go +++ b/internal/cmd/coinset/get_all_mempool_items.go @@ -1,18 +1,18 @@ package cmd import ( - "github.com/spf13/cobra" + "github.com/spf13/cobra" ) func init() { - rootCmd.AddCommand(getAllMempoolItemsCmd) + rootCmd.AddCommand(getAllMempoolItemsCmd) } var getAllMempoolItemsCmd = &cobra.Command{ - Use: "get_all_mempool_items", - Short: "Returns all items in the mempool", - Long: "Returns all items in the mempool", - Run: func(cmd *cobra.Command, args []string) { - makeRequest("get_all_mempool_items", nil) - }, -} \ No newline at end of file + Use: "get_all_mempool_items", + Short: "Returns all items in the mempool", + Long: "Returns all items in the mempool", + Run: func(cmd *cobra.Command, args []string) { + makeRequest("get_all_mempool_items", nil) + }, +} diff --git a/internal/cmd/coinset/get_all_mempool_tx_ids.go b/internal/cmd/coinset/get_all_mempool_tx_ids.go index 04f3888..5d44e5a 100644 --- a/internal/cmd/coinset/get_all_mempool_tx_ids.go +++ b/internal/cmd/coinset/get_all_mempool_tx_ids.go @@ -1,18 +1,18 @@ package cmd import ( - "github.com/spf13/cobra" + "github.com/spf13/cobra" ) func init() { - rootCmd.AddCommand(getAllMempoolTxIdsCmd) + rootCmd.AddCommand(getAllMempoolTxIdsCmd) } var getAllMempoolTxIdsCmd = &cobra.Command{ - Use: "get_all_mempool_tx_ids", - Short: "Returns all transaction IDs in the mempool", - Long: "Returns all transaction IDs in the mempool", - Run: func(cmd *cobra.Command, args []string) { - makeRequest("get_all_mempool_tx_ids", nil) - }, -} \ No newline at end of file + Use: "get_all_mempool_tx_ids", + Short: "Returns all transaction IDs in the mempool", + Long: "Returns all transaction IDs in the mempool", + Run: func(cmd *cobra.Command, args []string) { + makeRequest("get_all_mempool_tx_ids", nil) + }, +} diff --git a/internal/cmd/coinset/get_block_count_metrics.go b/internal/cmd/coinset/get_block_count_metrics.go index c8b1950..60bba5d 100644 --- a/internal/cmd/coinset/get_block_count_metrics.go +++ b/internal/cmd/coinset/get_block_count_metrics.go @@ -1,18 +1,18 @@ package cmd import ( - "github.com/spf13/cobra" + "github.com/spf13/cobra" ) func init() { - rootCmd.AddCommand(getBlockCountMetricsCmd) + rootCmd.AddCommand(getBlockCountMetricsCmd) } var getBlockCountMetricsCmd = &cobra.Command{ - Use: "get_block_count_metrics", - Short: "Gets metrics for the blockchain's blocks", - Long: `Gets metrics for the blockchain's blocks`, - Run: func(cmd *cobra.Command, args []string) { - makeRequest("get_block_count_metrics", nil) - }, -} \ No newline at end of file + Use: "get_block_count_metrics", + Short: "Gets metrics for the blockchain's blocks", + Long: `Gets metrics for the blockchain's blocks`, + Run: func(cmd *cobra.Command, args []string) { + makeRequest("get_block_count_metrics", nil) + }, +} diff --git a/internal/cmd/coinset/get_block_record_by_height.go b/internal/cmd/coinset/get_block_record_by_height.go index 8112949..0e1eadd 100644 --- a/internal/cmd/coinset/get_block_record_by_height.go +++ b/internal/cmd/coinset/get_block_record_by_height.go @@ -1,34 +1,33 @@ package cmd import ( - "fmt" - "strconv" + "fmt" + "strconv" - "github.com/spf13/cobra" + "github.com/spf13/cobra" ) func init() { - rootCmd.AddCommand(getBlockRecordByHeightCmd) + rootCmd.AddCommand(getBlockRecordByHeightCmd) } - var getBlockRecordByHeightCmd = &cobra.Command{ - Use: "get_block_record_by_height ", - Args: func(cmd *cobra.Command, args []string) error { - if err := cobra.ExactArgs(1)(cmd, args); err != nil { - return err - } - if _, err := strconv.Atoi(args[0]); err == nil { - return nil - } - return fmt.Errorf("invalid height specified: %s", args[0]) - }, - Short: "Retrieves a block record by height", - Long: `Retrieves a block record by height`, - Run: func(cmd *cobra.Command, args []string) { - height, _ := strconv.Atoi(args[0]) - jsonData := map[string]interface{}{} - jsonData["height"] = height - makeRequest("get_block_record_by_height", jsonData) - }, + Use: "get_block_record_by_height ", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + if _, err := strconv.Atoi(args[0]); err == nil { + return nil + } + return fmt.Errorf("invalid height specified: %s", args[0]) + }, + Short: "Retrieves a block record by height", + Long: `Retrieves a block record by height`, + Run: func(cmd *cobra.Command, args []string) { + height, _ := strconv.Atoi(args[0]) + jsonData := map[string]interface{}{} + jsonData["height"] = height + makeRequest("get_block_record_by_height", jsonData) + }, } diff --git a/internal/cmd/coinset/get_block_records.go b/internal/cmd/coinset/get_block_records.go index f891357..5771d37 100644 --- a/internal/cmd/coinset/get_block_records.go +++ b/internal/cmd/coinset/get_block_records.go @@ -1,38 +1,38 @@ package cmd import ( - "fmt" - "strconv" + "fmt" + "strconv" - "github.com/spf13/cobra" + "github.com/spf13/cobra" ) func init() { - rootCmd.AddCommand(getBlockRecordsCmd) + rootCmd.AddCommand(getBlockRecordsCmd) } var getBlockRecordsCmd = &cobra.Command{ - Use: "get_block_records ", - Args: func(cmd *cobra.Command, args []string) error { - if err := cobra.ExactArgs(2)(cmd, args); err != nil { - return err - } - if _, err := strconv.Atoi(args[0]); err != nil { - return fmt.Errorf("invalid start height specified: %s", args[0]) - } - if _, err := strconv.Atoi(args[1]); err != nil { - return fmt.Errorf("invalid end height specified: %s", args[1]) - } - return nil - }, - Short: "Gets a list of full blocks by height", - Long: `Gets a list of full blocks by height`, - Run: func(cmd *cobra.Command, args []string) { - start, _ := strconv.Atoi(args[0]) - end, _ := strconv.Atoi(args[1]) - jsonData := map[string]interface{}{} - jsonData["start"] = start - jsonData["end"] = end - makeRequest("get_block_records", jsonData) - }, -} \ No newline at end of file + Use: "get_block_records ", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(2)(cmd, args); err != nil { + return err + } + if _, err := strconv.Atoi(args[0]); err != nil { + return fmt.Errorf("invalid start height specified: %s", args[0]) + } + if _, err := strconv.Atoi(args[1]); err != nil { + return fmt.Errorf("invalid end height specified: %s", args[1]) + } + return nil + }, + Short: "Gets a list of full blocks by height", + Long: `Gets a list of full blocks by height`, + Run: func(cmd *cobra.Command, args []string) { + start, _ := strconv.Atoi(args[0]) + end, _ := strconv.Atoi(args[1]) + jsonData := map[string]interface{}{} + jsonData["start"] = start + jsonData["end"] = end + makeRequest("get_block_records", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_block_transactions.go b/internal/cmd/coinset/get_block_transactions.go new file mode 100644 index 0000000..f2f90b4 --- /dev/null +++ b/internal/cmd/coinset/get_block_transactions.go @@ -0,0 +1,48 @@ +package cmd + +import ( + "fmt" + "strconv" + + "github.com/spf13/cobra" +) + +var ( + getBlockTransactionsCursor string + getBlockTransactionsLimit int + getBlockTransactionsOrder string +) + +func init() { + getBlockTransactionsCmd.Flags().StringVar(&getBlockTransactionsCursor, "cursor", "", "Pagination cursor from a previous response") + getBlockTransactionsCmd.Flags().IntVar(&getBlockTransactionsLimit, "limit", 0, "Maximum number of records to return") + getBlockTransactionsCmd.Flags().StringVar(&getBlockTransactionsOrder, "order", "", `Result order: "asc" or "desc"`) + rootCmd.AddCommand(getBlockTransactionsCmd) +} + +var getBlockTransactionsCmd = &cobra.Command{ + Use: "get_block_transactions ", + Short: "List transactions for a block", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + + if _, err := strconv.Atoi(args[0]); err != nil && !isHex(args[0]) { + return fmt.Errorf("invalid block selector: %s", args[0]) + } + + return applyPaginationOptions(map[string]interface{}{}, getBlockTransactionsCursor, getBlockTransactionsLimit, getBlockTransactionsOrder) + }, + Run: func(cmd *cobra.Command, args []string) { + jsonData := map[string]interface{}{} + if height, err := strconv.Atoi(args[0]); err == nil { + jsonData["height"] = height + } else { + jsonData["header_hash"] = formatHex(args[0]) + } + + _ = applyPaginationOptions(jsonData, getBlockTransactionsCursor, getBlockTransactionsLimit, getBlockTransactionsOrder) + makeRequest("get_block_transactions", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_blockchain_state.go b/internal/cmd/coinset/get_blockchain_state.go index 753bf1f..5fe1557 100644 --- a/internal/cmd/coinset/get_blockchain_state.go +++ b/internal/cmd/coinset/get_blockchain_state.go @@ -1,18 +1,18 @@ package cmd import ( - "github.com/spf13/cobra" + "github.com/spf13/cobra" ) func init() { - rootCmd.AddCommand(getBlockchainStateCmd) + rootCmd.AddCommand(getBlockchainStateCmd) } var getBlockchainStateCmd = &cobra.Command{ - Use: "get_blockchain_state", - Short: "Retrieves a summary of the current state of the blockchain and full node", - Long: `Retrieves a summary of the current state of the blockchain and full node`, - Run: func(cmd *cobra.Command, args []string) { - makeRequest("get_blockchain_state", nil) - }, -} \ No newline at end of file + Use: "get_blockchain_state", + Short: "Retrieves a summary of the current state of the blockchain and full node", + Long: `Retrieves a summary of the current state of the blockchain and full node`, + Run: func(cmd *cobra.Command, args []string) { + makeRequest("get_blockchain_state", nil) + }, +} diff --git a/internal/cmd/coinset/get_blocks.go b/internal/cmd/coinset/get_blocks.go index c9c721b..d397905 100644 --- a/internal/cmd/coinset/get_blocks.go +++ b/internal/cmd/coinset/get_blocks.go @@ -1,38 +1,38 @@ package cmd import ( - "fmt" - "strconv" + "fmt" + "strconv" - "github.com/spf13/cobra" + "github.com/spf13/cobra" ) func init() { - rootCmd.AddCommand(getBlocksCmd) + rootCmd.AddCommand(getBlocksCmd) } var getBlocksCmd = &cobra.Command{ - Use: "get_blocks ", - Args: func(cmd *cobra.Command, args []string) error { - if err := cobra.ExactArgs(2)(cmd, args); err != nil { - return err - } - if _, err := strconv.Atoi(args[0]); err != nil { - return fmt.Errorf("invalid start height specified: %s", args[0]) - } - if _, err := strconv.Atoi(args[1]); err != nil { - return fmt.Errorf("invalid end height specified: %s", args[1]) - } - return nil - }, - Short: "Gets a list of full blocks by height", - Long: `Gets a list of full blocks by height`, - Run: func(cmd *cobra.Command, args []string) { - start, _ := strconv.Atoi(args[0]) - end, _ := strconv.Atoi(args[1]) - jsonData := map[string]interface{}{} - jsonData["start"] = start - jsonData["end"] = end - makeRequest("get_blocks", jsonData) - }, -} \ No newline at end of file + Use: "get_blocks ", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(2)(cmd, args); err != nil { + return err + } + if _, err := strconv.Atoi(args[0]); err != nil { + return fmt.Errorf("invalid start height specified: %s", args[0]) + } + if _, err := strconv.Atoi(args[1]); err != nil { + return fmt.Errorf("invalid end height specified: %s", args[1]) + } + return nil + }, + Short: "Gets a list of full blocks by height", + Long: `Gets a list of full blocks by height`, + Run: func(cmd *cobra.Command, args []string) { + start, _ := strconv.Atoi(args[0]) + end, _ := strconv.Atoi(args[1]) + jsonData := map[string]interface{}{} + jsonData["start"] = start + jsonData["end"] = end + makeRequest("get_blocks", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_cat_assets_by_p2.go b/internal/cmd/coinset/get_cat_assets_by_p2.go new file mode 100644 index 0000000..5295536 --- /dev/null +++ b/internal/cmd/coinset/get_cat_assets_by_p2.go @@ -0,0 +1,43 @@ +package cmd + +import "github.com/spf13/cobra" + +var ( + getCatAssetsByP2IncludeSpentCoins bool + getCatAssetsByP2Cursor string + getCatAssetsByP2Limit int + getCatAssetsByP2Order string +) + +func init() { + getCatAssetsByP2Cmd.Flags().BoolVar(&getCatAssetsByP2IncludeSpentCoins, "include-spent-coins", false, "Include spent coins") + getCatAssetsByP2Cmd.Flags().StringVar(&getCatAssetsByP2Cursor, "cursor", "", "Pagination cursor from a previous response") + getCatAssetsByP2Cmd.Flags().IntVar(&getCatAssetsByP2Limit, "limit", 0, "Maximum number of records to return") + getCatAssetsByP2Cmd.Flags().StringVar(&getCatAssetsByP2Order, "order", "", `Result order: "asc" or "desc"`) + rootCmd.AddCommand(getCatAssetsByP2Cmd) +} + +var getCatAssetsByP2Cmd = &cobra.Command{ + Use: "get_cat_assets_by_p2 ", + Short: "List CAT asset ids by p2", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + if _, err := normalizeP2Arg(args[0]); err != nil { + return err + } + return applyPaginationOptions(map[string]interface{}{}, getCatAssetsByP2Cursor, getCatAssetsByP2Limit, getCatAssetsByP2Order) + }, + Run: func(cmd *cobra.Command, args []string) { + p2, _ := normalizeP2Arg(args[0]) + jsonData := map[string]interface{}{ + "p2": p2, + } + if getCatAssetsByP2IncludeSpentCoins { + jsonData["include_spent_coins"] = true + } + _ = applyPaginationOptions(jsonData, getCatAssetsByP2Cursor, getCatAssetsByP2Limit, getCatAssetsByP2Order) + makeRequest("get_cat_assets_by_p2", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_cat_balances_by_p2.go b/internal/cmd/coinset/get_cat_balances_by_p2.go new file mode 100644 index 0000000..f18b125 --- /dev/null +++ b/internal/cmd/coinset/get_cat_balances_by_p2.go @@ -0,0 +1,58 @@ +package cmd + +import "github.com/spf13/cobra" + +var ( + getCatBalancesByP2AssetIDs []string + getCatBalancesByP2Cursor string + getCatBalancesByP2Limit int + getCatBalancesByP2Order string +) + +func init() { + getCatBalancesByP2Cmd.Flags().StringArrayVar(&getCatBalancesByP2AssetIDs, "asset-id", []string{}, "Optional CAT asset id filter (repeatable)") + getCatBalancesByP2Cmd.Flags().StringVar(&getCatBalancesByP2Cursor, "cursor", "", "Pagination cursor from a previous response") + getCatBalancesByP2Cmd.Flags().IntVar(&getCatBalancesByP2Limit, "limit", 0, "Maximum number of records to return") + getCatBalancesByP2Cmd.Flags().StringVar(&getCatBalancesByP2Order, "order", "", `Result order: "asc" or "desc"`) + rootCmd.AddCommand(getCatBalancesByP2Cmd) +} + +var getCatBalancesByP2Cmd = &cobra.Command{ + Use: "get_cat_balances_by_p2 ", + Short: "List CAT balances by p2", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + if _, err := normalizeP2Arg(args[0]); err != nil { + return err + } + + for _, assetID := range getCatBalancesByP2AssetIDs { + if _, err := normalizeHexArg(assetID, "asset_id"); err != nil { + return err + } + } + + return applyPaginationOptions(map[string]interface{}{}, getCatBalancesByP2Cursor, getCatBalancesByP2Limit, getCatBalancesByP2Order) + }, + Run: func(cmd *cobra.Command, args []string) { + p2, _ := normalizeP2Arg(args[0]) + jsonData := map[string]interface{}{ + "p2": p2, + } + + if len(getCatBalancesByP2AssetIDs) > 0 { + normalized := make([]string, 0, len(getCatBalancesByP2AssetIDs)) + for _, assetID := range getCatBalancesByP2AssetIDs { + formattedAssetID, _ := normalizeHexArg(assetID, "asset_id") + normalized = append(normalized, formattedAssetID) + } + jsonData["asset_ids"] = normalized + } else { + _ = applyPaginationOptions(jsonData, getCatBalancesByP2Cursor, getCatBalancesByP2Limit, getCatBalancesByP2Order) + } + + makeRequest("get_cat_balances_by_p2", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_cat_coins_by_asset_id.go b/internal/cmd/coinset/get_cat_coins_by_asset_id.go new file mode 100644 index 0000000..9a3d471 --- /dev/null +++ b/internal/cmd/coinset/get_cat_coins_by_asset_id.go @@ -0,0 +1,43 @@ +package cmd + +import "github.com/spf13/cobra" + +var ( + getCatCoinsByAssetIDIncludeSpentCoins bool + getCatCoinsByAssetIDCursor string + getCatCoinsByAssetIDLimit int + getCatCoinsByAssetIDOrder string +) + +func init() { + getCatCoinsByAssetIDCmd.Flags().BoolVar(&getCatCoinsByAssetIDIncludeSpentCoins, "include-spent-coins", false, "Include spent coins") + getCatCoinsByAssetIDCmd.Flags().StringVar(&getCatCoinsByAssetIDCursor, "cursor", "", "Pagination cursor from a previous response") + getCatCoinsByAssetIDCmd.Flags().IntVar(&getCatCoinsByAssetIDLimit, "limit", 0, "Maximum number of records to return") + getCatCoinsByAssetIDCmd.Flags().StringVar(&getCatCoinsByAssetIDOrder, "order", "", `Result order: "asc" or "desc"`) + rootCmd.AddCommand(getCatCoinsByAssetIDCmd) +} + +var getCatCoinsByAssetIDCmd = &cobra.Command{ + Use: "get_cat_coins_by_asset_id ", + Short: "List CAT coins by asset id", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + if _, err := normalizeHexArg(args[0], "asset_id"); err != nil { + return err + } + return applyPaginationOptions(map[string]interface{}{}, getCatCoinsByAssetIDCursor, getCatCoinsByAssetIDLimit, getCatCoinsByAssetIDOrder) + }, + Run: func(cmd *cobra.Command, args []string) { + assetID, _ := normalizeHexArg(args[0], "asset_id") + jsonData := map[string]interface{}{ + "asset_id": assetID, + } + if getCatCoinsByAssetIDIncludeSpentCoins { + jsonData["include_spent_coins"] = true + } + _ = applyPaginationOptions(jsonData, getCatCoinsByAssetIDCursor, getCatCoinsByAssetIDLimit, getCatCoinsByAssetIDOrder) + makeRequest("get_cat_coins_by_asset_id", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_cat_coins_by_p2_and_asset_id.go b/internal/cmd/coinset/get_cat_coins_by_p2_and_asset_id.go new file mode 100644 index 0000000..c15610c --- /dev/null +++ b/internal/cmd/coinset/get_cat_coins_by_p2_and_asset_id.go @@ -0,0 +1,48 @@ +package cmd + +import "github.com/spf13/cobra" + +var ( + getCatCoinsByP2AndAssetIDIncludeSpentCoins bool + getCatCoinsByP2AndAssetIDCursor string + getCatCoinsByP2AndAssetIDLimit int + getCatCoinsByP2AndAssetIDOrder string +) + +func init() { + getCatCoinsByP2AndAssetIDCmd.Flags().BoolVar(&getCatCoinsByP2AndAssetIDIncludeSpentCoins, "include-spent-coins", false, "Include spent coins") + getCatCoinsByP2AndAssetIDCmd.Flags().StringVar(&getCatCoinsByP2AndAssetIDCursor, "cursor", "", "Pagination cursor from a previous response") + getCatCoinsByP2AndAssetIDCmd.Flags().IntVar(&getCatCoinsByP2AndAssetIDLimit, "limit", 0, "Maximum number of records to return") + getCatCoinsByP2AndAssetIDCmd.Flags().StringVar(&getCatCoinsByP2AndAssetIDOrder, "order", "", `Result order: "asc" or "desc"`) + rootCmd.AddCommand(getCatCoinsByP2AndAssetIDCmd) +} + +var getCatCoinsByP2AndAssetIDCmd = &cobra.Command{ + Use: "get_cat_coins_by_p2_and_asset_id ", + Short: "List CAT coins by p2 and asset id", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(2)(cmd, args); err != nil { + return err + } + if _, err := normalizeP2Arg(args[0]); err != nil { + return err + } + if _, err := normalizeHexArg(args[1], "asset_id"); err != nil { + return err + } + return applyPaginationOptions(map[string]interface{}{}, getCatCoinsByP2AndAssetIDCursor, getCatCoinsByP2AndAssetIDLimit, getCatCoinsByP2AndAssetIDOrder) + }, + Run: func(cmd *cobra.Command, args []string) { + p2, _ := normalizeP2Arg(args[0]) + assetID, _ := normalizeHexArg(args[1], "asset_id") + jsonData := map[string]interface{}{ + "p2": p2, + "asset_id": assetID, + } + if getCatCoinsByP2AndAssetIDIncludeSpentCoins { + jsonData["include_spent_coins"] = true + } + _ = applyPaginationOptions(jsonData, getCatCoinsByP2AndAssetIDCursor, getCatCoinsByP2AndAssetIDLimit, getCatCoinsByP2AndAssetIDOrder) + makeRequest("get_cat_coins_by_p2_and_asset_id", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_clawback_coins_by_receiver.go b/internal/cmd/coinset/get_clawback_coins_by_receiver.go new file mode 100644 index 0000000..954a165 --- /dev/null +++ b/internal/cmd/coinset/get_clawback_coins_by_receiver.go @@ -0,0 +1,38 @@ +package cmd + +import "github.com/spf13/cobra" + +var ( + getClawbackCoinsByReceiverCursor string + getClawbackCoinsByReceiverLimit int + getClawbackCoinsByReceiverOrder string +) + +func init() { + getClawbackCoinsByReceiverCmd.Flags().StringVar(&getClawbackCoinsByReceiverCursor, "cursor", "", "Pagination cursor from a previous response") + getClawbackCoinsByReceiverCmd.Flags().IntVar(&getClawbackCoinsByReceiverLimit, "limit", 0, "Maximum number of records to return") + getClawbackCoinsByReceiverCmd.Flags().StringVar(&getClawbackCoinsByReceiverOrder, "order", "", `Result order: "asc" or "desc"`) + rootCmd.AddCommand(getClawbackCoinsByReceiverCmd) +} + +var getClawbackCoinsByReceiverCmd = &cobra.Command{ + Use: "get_clawback_coins_by_receiver ", + Short: "List clawback coins by receiver p2", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + if _, err := normalizeP2Arg(args[0]); err != nil { + return err + } + return applyPaginationOptions(map[string]interface{}{}, getClawbackCoinsByReceiverCursor, getClawbackCoinsByReceiverLimit, getClawbackCoinsByReceiverOrder) + }, + Run: func(cmd *cobra.Command, args []string) { + p2, _ := normalizeP2Arg(args[0]) + jsonData := map[string]interface{}{ + "p2": p2, + } + _ = applyPaginationOptions(jsonData, getClawbackCoinsByReceiverCursor, getClawbackCoinsByReceiverLimit, getClawbackCoinsByReceiverOrder) + makeRequest("get_clawback_coins_by_receiver", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_coin_records_by_hint.go b/internal/cmd/coinset/get_coin_records_by_hint.go index e24fb3f..59b3036 100644 --- a/internal/cmd/coinset/get_coin_records_by_hint.go +++ b/internal/cmd/coinset/get_coin_records_by_hint.go @@ -10,6 +10,9 @@ var ( crByHintCmdIncludeSpentCoins bool crByHintCmdStart int crByHintCmdEnd int + crByHintCmdCursor string + crByHintCmdLimit int + crByHintCmdOrder string ) func init() { @@ -19,6 +22,9 @@ func init() { getCoinRecordsByHintCmd.Flags().BoolVarP(&crByHintCmdIncludeSpentCoins, "include-spent-coins", "s", false, "Include spent coins") getCoinRecordsByHintCmd.Flags().IntVarP(&crByHintCmdStart, "start-height", "", -1, "Start height") getCoinRecordsByHintCmd.Flags().IntVarP(&crByHintCmdEnd, "end-height", "", -1, "End height") + getCoinRecordsByHintCmd.Flags().StringVar(&crByHintCmdCursor, "cursor", "", "Pagination cursor from a previous truncated response") + getCoinRecordsByHintCmd.Flags().IntVar(&crByHintCmdLimit, "limit", 0, "Maximum number of records to return") + getCoinRecordsByHintCmd.Flags().StringVar(&crByHintCmdOrder, "order", "", `Result order: "asc" or "desc"`) } var getCoinRecordsByHintCmd = &cobra.Command{ @@ -28,6 +34,12 @@ var getCoinRecordsByHintCmd = &cobra.Command{ return err } if isHex(args[0]) { + if crByHintCmdLimit < 0 { + return fmt.Errorf("limit must be >= 1 when provided") + } + if crByHintCmdOrder != "" && !isAscOrDesc(crByHintCmdOrder) { + return fmt.Errorf(`invalid order %q: must be "asc" or "desc"`, crByHintCmdOrder) + } return nil } return fmt.Errorf("invalid hex value specified: %s", args[0]) @@ -48,6 +60,15 @@ var getCoinRecordsByHintCmd = &cobra.Command{ if crByHintCmdEnd != -1 { jsonData["end_height"] = crByHintCmdEnd } + if crByHintCmdCursor != "" { + jsonData["cursor"] = crByHintCmdCursor + } + if crByHintCmdLimit > 0 { + jsonData["limit"] = crByHintCmdLimit + } + if crByHintCmdOrder != "" { + jsonData["order"] = crByHintCmdOrder + } makeRequest("get_coin_records_by_hint", jsonData) }, } diff --git a/internal/cmd/coinset/get_coin_records_by_hints.go b/internal/cmd/coinset/get_coin_records_by_hints.go index 847199b..89eaa39 100644 --- a/internal/cmd/coinset/get_coin_records_by_hints.go +++ b/internal/cmd/coinset/get_coin_records_by_hints.go @@ -10,12 +10,18 @@ var ( crByHintsIncludeSpentCoins bool crByHintsStart int crByHintsEnd int + crByHintsCursor string + crByHintsLimit int + crByHintsOrder string ) func init() { getCoinRecordsByHintsCmd.Flags().BoolVarP(&crByHintsIncludeSpentCoins, "include-spent-coins", "s", false, "Include spent coins") getCoinRecordsByHintsCmd.Flags().IntVarP(&crByHintsStart, "start-height", "", -1, "Start height") getCoinRecordsByHintsCmd.Flags().IntVarP(&crByHintsEnd, "end-height", "", -1, "End height") + getCoinRecordsByHintsCmd.Flags().StringVar(&crByHintsCursor, "cursor", "", "Pagination cursor from a previous truncated response") + getCoinRecordsByHintsCmd.Flags().IntVar(&crByHintsLimit, "limit", 0, "Maximum number of records to return") + getCoinRecordsByHintsCmd.Flags().StringVar(&crByHintsOrder, "order", "", `Result order: "asc" or "desc"`) rootCmd.AddCommand(getCoinRecordsByHintsCmd) } @@ -30,6 +36,12 @@ var getCoinRecordsByHintsCmd = &cobra.Command{ return fmt.Errorf("invalid hex value specified: %s", hint) } } + if crByHintsLimit < 0 { + return fmt.Errorf("limit must be >= 1 when provided") + } + if crByHintsOrder != "" && !isAscOrDesc(crByHintsOrder) { + return fmt.Errorf(`invalid order %q: must be "asc" or "desc"`, crByHintsOrder) + } return nil }, Short: "Retrieves coin records by multiple hints", @@ -51,6 +63,15 @@ var getCoinRecordsByHintsCmd = &cobra.Command{ if crByHintsEnd != -1 { jsonData["end_height"] = crByHintsEnd } + if crByHintsCursor != "" { + jsonData["cursor"] = crByHintsCursor + } + if crByHintsLimit > 0 { + jsonData["limit"] = crByHintsLimit + } + if crByHintsOrder != "" { + jsonData["order"] = crByHintsOrder + } makeRequest("get_coin_records_by_hints", jsonData) }, } diff --git a/internal/cmd/coinset/get_coin_records_by_names.go b/internal/cmd/coinset/get_coin_records_by_names.go index 185a235..4706b60 100644 --- a/internal/cmd/coinset/get_coin_records_by_names.go +++ b/internal/cmd/coinset/get_coin_records_by_names.go @@ -1,56 +1,77 @@ package cmd import ( - "fmt" + "fmt" - "github.com/spf13/cobra" + "github.com/spf13/cobra" ) var ( - crByNamesIncludeSpentCoins bool - crByNamesStart int - crByNamesEnd int + crByNamesIncludeSpentCoins bool + crByNamesStart int + crByNamesEnd int + crByNamesCursor string + crByNamesLimit int + crByNamesOrder string ) func init() { - getCoinRecordsByNamesCmd.Flags().BoolVarP(&crByNamesIncludeSpentCoins, "include-spent-coins", "s", false, "Include spent coins") - getCoinRecordsByNamesCmd.Flags().IntVarP(&crByNamesStart, "start", "", -1, "Start height") - getCoinRecordsByNamesCmd.Flags().IntVarP(&crByNamesEnd, "end", "", -1, "End height") - rootCmd.AddCommand(getCoinRecordsByNamesCmd) + getCoinRecordsByNamesCmd.Flags().BoolVarP(&crByNamesIncludeSpentCoins, "include-spent-coins", "s", false, "Include spent coins") + getCoinRecordsByNamesCmd.Flags().IntVarP(&crByNamesStart, "start", "", -1, "Start height") + getCoinRecordsByNamesCmd.Flags().IntVarP(&crByNamesEnd, "end", "", -1, "End height") + getCoinRecordsByNamesCmd.Flags().StringVar(&crByNamesCursor, "cursor", "", "Pagination cursor from a previous truncated response") + getCoinRecordsByNamesCmd.Flags().IntVar(&crByNamesLimit, "limit", 0, "Maximum number of records to return") + getCoinRecordsByNamesCmd.Flags().StringVar(&crByNamesOrder, "order", "", `Result order: "asc" or "desc"`) + rootCmd.AddCommand(getCoinRecordsByNamesCmd) } var getCoinRecordsByNamesCmd = &cobra.Command{ - Use: "get_coin_records_by_names ...", - Args: func(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("at least one name is required") - } - for _, name := range args { - if !isHex(name) { - return fmt.Errorf("invalid hex value specified: %s", name) - } - } - return nil - }, - Short: "Retrieves coin records by their names", - Long: "Retrieves coin records by their names", - Run: func(cmd *cobra.Command, args []string) { - var names []string - for _, name := range args { - names = append(names, formatHex(name)) - } - jsonData := map[string]interface{}{ - "names": names, - } - if crByNamesIncludeSpentCoins { - jsonData["include_spent_coins"] = true - } - if crByNamesStart != -1 { - jsonData["start_height"] = crByNamesStart - } - if crByNamesEnd != -1 { - jsonData["end_height"] = crByNamesEnd - } - makeRequest("get_coin_records_by_names", jsonData) - }, + Use: "get_coin_records_by_names ...", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("at least one name is required") + } + for _, name := range args { + if !isHex(name) { + return fmt.Errorf("invalid hex value specified: %s", name) + } + } + if crByNamesLimit < 0 { + return fmt.Errorf("limit must be >= 1 when provided") + } + if crByNamesOrder != "" && !isAscOrDesc(crByNamesOrder) { + return fmt.Errorf(`invalid order %q: must be "asc" or "desc"`, crByNamesOrder) + } + return nil + }, + Short: "Retrieves coin records by their names", + Long: "Retrieves coin records by their names", + Run: func(cmd *cobra.Command, args []string) { + var names []string + for _, name := range args { + names = append(names, formatHex(name)) + } + jsonData := map[string]interface{}{ + "names": names, + } + if crByNamesIncludeSpentCoins { + jsonData["include_spent_coins"] = true + } + if crByNamesStart != -1 { + jsonData["start_height"] = crByNamesStart + } + if crByNamesEnd != -1 { + jsonData["end_height"] = crByNamesEnd + } + if crByNamesCursor != "" { + jsonData["cursor"] = crByNamesCursor + } + if crByNamesLimit > 0 { + jsonData["limit"] = crByNamesLimit + } + if crByNamesOrder != "" { + jsonData["order"] = crByNamesOrder + } + makeRequest("get_coin_records_by_names", jsonData) + }, } diff --git a/internal/cmd/coinset/get_coin_records_by_parent_ids.go b/internal/cmd/coinset/get_coin_records_by_parent_ids.go index e82ac1f..b7d171b 100644 --- a/internal/cmd/coinset/get_coin_records_by_parent_ids.go +++ b/internal/cmd/coinset/get_coin_records_by_parent_ids.go @@ -1,57 +1,78 @@ package cmd import ( - "fmt" + "fmt" - "github.com/spf13/cobra" + "github.com/spf13/cobra" ) var ( - crByParentIdsIncludeSpentCoins bool - crByParentIdsStart int - crByParentIdsEnd int + crByParentIdsIncludeSpentCoins bool + crByParentIdsStart int + crByParentIdsEnd int + crByParentIdsCursor string + crByParentIdsLimit int + crByParentIdsOrder string ) func init() { - rootCmd.AddCommand(getCoinRecordsByParentIdsCmd) + rootCmd.AddCommand(getCoinRecordsByParentIdsCmd) - // Define flags for the optional arguments - getCoinRecordsByParentIdsCmd.Flags().BoolVarP(&crByParentIdsIncludeSpentCoins, "include-spent-coins", "s", false, "Include spent coins") - getCoinRecordsByParentIdsCmd.Flags().IntVarP(&crByParentIdsStart, "start-height", "", -1, "Start height") - getCoinRecordsByParentIdsCmd.Flags().IntVarP(&crByParentIdsEnd, "end-height", "", -1, "End height") + // Define flags for the optional arguments + getCoinRecordsByParentIdsCmd.Flags().BoolVarP(&crByParentIdsIncludeSpentCoins, "include-spent-coins", "s", false, "Include spent coins") + getCoinRecordsByParentIdsCmd.Flags().IntVarP(&crByParentIdsStart, "start-height", "", -1, "Start height") + getCoinRecordsByParentIdsCmd.Flags().IntVarP(&crByParentIdsEnd, "end-height", "", -1, "End height") + getCoinRecordsByParentIdsCmd.Flags().StringVar(&crByParentIdsCursor, "cursor", "", "Pagination cursor from a previous truncated response") + getCoinRecordsByParentIdsCmd.Flags().IntVar(&crByParentIdsLimit, "limit", 0, "Maximum number of records to return") + getCoinRecordsByParentIdsCmd.Flags().StringVar(&crByParentIdsOrder, "order", "", `Result order: "asc" or "desc"`) } var getCoinRecordsByParentIdsCmd = &cobra.Command{ - Use: "get_coin_records_by_parent_ids ...", - Args: func(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("at least one parent ID is required") - } - for _, name := range args { - if !isHex(name) { - return fmt.Errorf("invalid hex value specified: %s", name) - } - } - return nil - }, - Short: "Retrieves coin records by parent IDs", - Long: "Retrieves coin records by parent IDs", - Run: func(cmd *cobra.Command, args []string) { - var parentIds []string - for _, parentId := range args { - parentIds = append(parentIds, formatHex(parentId)) - } - jsonData := map[string]interface{}{} - jsonData["parent_ids"] = parentIds - if crByParentIdsIncludeSpentCoins { - jsonData["include_spent_coins"] = true - } - if crByParentIdsStart != -1 { - jsonData["start_height"] = crByParentIdsStart - } - if crByParentIdsEnd != -1 { - jsonData["end_height"] = crByParentIdsEnd - } - makeRequest("get_coin_records_by_parent_ids", jsonData) - }, + Use: "get_coin_records_by_parent_ids ...", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("at least one parent ID is required") + } + for _, name := range args { + if !isHex(name) { + return fmt.Errorf("invalid hex value specified: %s", name) + } + } + if crByParentIdsLimit < 0 { + return fmt.Errorf("limit must be >= 1 when provided") + } + if crByParentIdsOrder != "" && !isAscOrDesc(crByParentIdsOrder) { + return fmt.Errorf(`invalid order %q: must be "asc" or "desc"`, crByParentIdsOrder) + } + return nil + }, + Short: "Retrieves coin records by parent IDs", + Long: "Retrieves coin records by parent IDs", + Run: func(cmd *cobra.Command, args []string) { + var parentIds []string + for _, parentId := range args { + parentIds = append(parentIds, formatHex(parentId)) + } + jsonData := map[string]interface{}{} + jsonData["parent_ids"] = parentIds + if crByParentIdsIncludeSpentCoins { + jsonData["include_spent_coins"] = true + } + if crByParentIdsStart != -1 { + jsonData["start_height"] = crByParentIdsStart + } + if crByParentIdsEnd != -1 { + jsonData["end_height"] = crByParentIdsEnd + } + if crByParentIdsCursor != "" { + jsonData["cursor"] = crByParentIdsCursor + } + if crByParentIdsLimit > 0 { + jsonData["limit"] = crByParentIdsLimit + } + if crByParentIdsOrder != "" { + jsonData["order"] = crByParentIdsOrder + } + makeRequest("get_coin_records_by_parent_ids", jsonData) + }, } diff --git a/internal/cmd/coinset/get_coin_records_by_puzzle_hash.go b/internal/cmd/coinset/get_coin_records_by_puzzle_hash.go index 026b802..aed67a4 100644 --- a/internal/cmd/coinset/get_coin_records_by_puzzle_hash.go +++ b/internal/cmd/coinset/get_coin_records_by_puzzle_hash.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "github.com/spf13/cobra" ) @@ -8,12 +10,18 @@ var ( crByPuzzleHashIncludeSpentCoins bool crByPuzzleHashStart int crByPuzzleHashEnd int + crByPuzzleHashCursor string + crByPuzzleHashLimit int + crByPuzzleHashOrder string ) func init() { getCoinRecordsByPuzzleHashCmd.Flags().BoolVarP(&crByPuzzleHashIncludeSpentCoins, "include-spent-coins", "s", false, "Include spent coins") getCoinRecordsByPuzzleHashCmd.Flags().IntVarP(&crByPuzzleHashStart, "start-height", "", -1, "Start height") getCoinRecordsByPuzzleHashCmd.Flags().IntVarP(&crByPuzzleHashEnd, "end-height", "", -1, "End height") + getCoinRecordsByPuzzleHashCmd.Flags().StringVar(&crByPuzzleHashCursor, "cursor", "", "Pagination cursor from a previous truncated response") + getCoinRecordsByPuzzleHashCmd.Flags().IntVar(&crByPuzzleHashLimit, "limit", 0, "Maximum number of records to return") + getCoinRecordsByPuzzleHashCmd.Flags().StringVar(&crByPuzzleHashOrder, "order", "", `Result order: "asc" or "desc"`) rootCmd.AddCommand(getCoinRecordsByPuzzleHashCmd) } @@ -27,6 +35,12 @@ var getCoinRecordsByPuzzleHashCmd = &cobra.Command{ if err != nil { return err } + if crByPuzzleHashLimit < 0 { + return fmt.Errorf("limit must be >= 1 when provided") + } + if crByPuzzleHashOrder != "" && !isAscOrDesc(crByPuzzleHashOrder) { + return fmt.Errorf(`invalid order %q: must be "asc" or "desc"`, crByPuzzleHashOrder) + } return nil }, Short: "Retrieves coin records by their puzzle hash or address", @@ -44,6 +58,15 @@ var getCoinRecordsByPuzzleHashCmd = &cobra.Command{ if crByPuzzleHashEnd != -1 { jsonData["end_height"] = crByPuzzleHashEnd } + if crByPuzzleHashCursor != "" { + jsonData["cursor"] = crByPuzzleHashCursor + } + if crByPuzzleHashLimit > 0 { + jsonData["limit"] = crByPuzzleHashLimit + } + if crByPuzzleHashOrder != "" { + jsonData["order"] = crByPuzzleHashOrder + } makeRequest("get_coin_records_by_puzzle_hash", jsonData) }, } diff --git a/internal/cmd/coinset/get_coin_records_by_puzzle_hashes.go b/internal/cmd/coinset/get_coin_records_by_puzzle_hashes.go index 247b61a..d2ad572 100644 --- a/internal/cmd/coinset/get_coin_records_by_puzzle_hashes.go +++ b/internal/cmd/coinset/get_coin_records_by_puzzle_hashes.go @@ -10,12 +10,18 @@ var ( crByPuzzleHashesIncludeSpentCoins bool crByPuzzleHashesStart int crByPuzzleHashesEnd int + crByPuzzleHashesCursor string + crByPuzzleHashesLimit int + crByPuzzleHashesOrder string ) func init() { getCoinRecordsByPuzzleHashesCmd.Flags().BoolVarP(&crByPuzzleHashesIncludeSpentCoins, "include-spent-coins", "s", false, "Include spent coins") getCoinRecordsByPuzzleHashesCmd.Flags().IntVarP(&crByPuzzleHashesStart, "start-height", "", -1, "Start height") getCoinRecordsByPuzzleHashesCmd.Flags().IntVarP(&crByPuzzleHashesEnd, "end-height", "", -1, "End height") + getCoinRecordsByPuzzleHashesCmd.Flags().StringVar(&crByPuzzleHashesCursor, "cursor", "", "Pagination cursor from a previous truncated response") + getCoinRecordsByPuzzleHashesCmd.Flags().IntVar(&crByPuzzleHashesLimit, "limit", 0, "Maximum number of records to return") + getCoinRecordsByPuzzleHashesCmd.Flags().StringVar(&crByPuzzleHashesOrder, "order", "", `Result order: "asc" or "desc"`) rootCmd.AddCommand(getCoinRecordsByPuzzleHashesCmd) } @@ -31,6 +37,12 @@ var getCoinRecordsByPuzzleHashesCmd = &cobra.Command{ return fmt.Errorf("invalid input '%s': %v", input, err) } } + if crByPuzzleHashesLimit < 0 { + return fmt.Errorf("limit must be >= 1 when provided") + } + if crByPuzzleHashesOrder != "" && !isAscOrDesc(crByPuzzleHashesOrder) { + return fmt.Errorf(`invalid order %q: must be "asc" or "desc"`, crByPuzzleHashesOrder) + } return nil }, Short: "Retrieves coin records by their puzzle hashes or addresses", @@ -53,6 +65,15 @@ var getCoinRecordsByPuzzleHashesCmd = &cobra.Command{ if crByPuzzleHashesEnd != -1 { jsonData["end_height"] = crByPuzzleHashesEnd } + if crByPuzzleHashesCursor != "" { + jsonData["cursor"] = crByPuzzleHashesCursor + } + if crByPuzzleHashesLimit > 0 { + jsonData["limit"] = crByPuzzleHashesLimit + } + if crByPuzzleHashesOrder != "" { + jsonData["order"] = crByPuzzleHashesOrder + } makeRequest("get_coin_records_by_puzzle_hashes", jsonData) }, } diff --git a/internal/cmd/coinset/get_fee_estimate.go b/internal/cmd/coinset/get_fee_estimate.go index 3c23ec4..3bb92d0 100644 --- a/internal/cmd/coinset/get_fee_estimate.go +++ b/internal/cmd/coinset/get_fee_estimate.go @@ -10,6 +10,18 @@ import ( "github.com/spf13/cobra" ) +var supportedSpendTypes = map[string]struct{}{ + "send_xch_transaction": {}, + "cat_spend": {}, + "take_offer": {}, + "cancel_offer": {}, + "nft_set_nft_did": {}, + "nft_transfer_nft": {}, + "create_new_pool_wallet": {}, + "pw_absorb_rewards": {}, + "create_new_did_wallet": {}, +} + func validateSpendBundle(data map[string]interface{}) error { aggregatedSig, hasAggSig := data["aggregated_signature"].(string) if !hasAggSig { @@ -65,11 +77,39 @@ func validateSpendBundle(data map[string]interface{}) error { 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) +func isValidSpendType(spendType string) bool { + _, ok := supportedSpendTypes[spendType] + return ok +} + +func parseTargetTimes(timesStr string) ([]int64, error) { + times := []int64{} + for _, t := range strings.Split(timesStr, ",") { + value, err := strconv.ParseInt(strings.TrimSpace(t), 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid target time: %s", t) + } + times = append(times, value) + } + return times, nil +} + +func loadSpendBundleFromFile(path string) (map[string]interface{}, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("unable to read file %s: %v", path, err) + } + + var spendBundle map[string]interface{} + if err := json.Unmarshal(data, &spendBundle); err != nil { + return nil, fmt.Errorf("invalid JSON in file %s: %v", path, err) + } + + if err := validateSpendBundle(spendBundle); err != nil { + return nil, fmt.Errorf("invalid spend bundle in file %s: %v", path, err) + } + + return spendBundle, nil } var getFeeEstimateCmd = &cobra.Command{ @@ -82,105 +122,124 @@ Examples: 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`, + coinset get_fee_estimate -c 20000000 + coinset get_fee_estimate --spend-type send_xch_transaction --times 60,300 + coinset get_fee_estimate --spend-type take_offer --spend-count 2`, } func init() { var requestData map[string]interface{} + 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") + getFeeEstimateCmd.Flags().String("spend-type", "", "Spend type for fee estimation") + getFeeEstimateCmd.Flags().Int64("spend-count", 1, "Number of spends represented by the request") + rootCmd.AddCommand(getFeeEstimateCmd) + getFeeEstimateCmd.Args = func(cmd *cobra.Command, args []string) error { dataFile, _ := cmd.Flags().GetString("file") cost, _ := cmd.Flags().GetInt64("cost") timesStr, _ := cmd.Flags().GetString("times") + spendType, _ := cmd.Flags().GetString("spend-type") + spendCount, _ := cmd.Flags().GetInt64("spend-count") - 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 spendCount < 1 { + return fmt.Errorf("spend-count must be >= 1") } + times, err := parseTargetTimes(timesStr) + if err != nil { + return err + } requestData = map[string]interface{}{ "target_times": times, } + if spendCount != 1 { + requestData["spend_count"] = spendCount + } - if dataFile != "" && cost != 0 { - return fmt.Errorf("cannot specify both -f and -c flags") + if cost < 0 { + return fmt.Errorf("cost must be >= 0") } - if dataFile != "" && len(args) > 0 { - return fmt.Errorf("cannot provide both -f flag and direct argument") + + specifiedModes := 0 + if dataFile != "" { + specifiedModes++ + } + if cost != 0 { + specifiedModes++ + } + if spendType != "" { + specifiedModes++ + } + if specifiedModes > 1 { + return fmt.Errorf("choose exactly one of --file, --cost, or --spend-type") + } + + if len(args) > 1 { + return fmt.Errorf("too many arguments provided") } - if cost != 0 && len(args) > 0 { - return fmt.Errorf("cannot provide both -c flag and direct argument") + + if spendType != "" { + if len(args) > 0 { + return fmt.Errorf("cannot provide positional argument with --spend-type") + } + if !isValidSpendType(spendType) { + return fmt.Errorf("invalid spend-type: %s", spendType) + } + requestData["spend_type"] = spendType + return nil } if dataFile != "" { - data, err := os.ReadFile(dataFile) + if len(args) > 0 { + return fmt.Errorf("cannot provide both --file and positional argument") + } + spendBundle, err := loadSpendBundleFromFile(dataFile) if err != nil { - return fmt.Errorf("unable to read file %s: %v", dataFile, err) + return err } + requestData["spend_bundle"] = spendBundle + return nil + } - var spendBundle map[string]interface{} - if err := json.Unmarshal(data, &spendBundle); err != nil { - return fmt.Errorf("invalid JSON in file: %v", err) - } + if cost != 0 { + requestData["cost"] = cost + return nil + } - if err := validateSpendBundle(spendBundle); err != nil { - return fmt.Errorf("invalid spend bundle in file: %v", err) - } + if len(args) != 1 { + return fmt.Errorf("must provide one of --file, --cost, --spend-type, or a positional cost/spend bundle") + } - requestData["spend_bundle"] = spendBundle - } else { - if cost != 0 { - requestData = map[string]interface{}{ - "cost": cost, - "target_times": times, - } - return nil + if parsedCost, err := strconv.ParseInt(args[0], 10, 64); err == nil { + if parsedCost < 0 { + return fmt.Errorf("cost must be >= 0") } + requestData["cost"] = parsedCost + 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") + 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 + return nil + } - 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") - } - } + if _, err := os.Stat(args[0]); err == nil { + spendBundle, err := loadSpendBundleFromFile(args[0]) + if err != nil { + return err } + requestData["spend_bundle"] = spendBundle + return nil } - return nil + 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") } getFeeEstimateCmd.Run = func(cmd *cobra.Command, args []string) { diff --git a/internal/cmd/coinset/get_fee_tiers_by_cost.go b/internal/cmd/coinset/get_fee_tiers_by_cost.go new file mode 100644 index 0000000..8b85fef --- /dev/null +++ b/internal/cmd/coinset/get_fee_tiers_by_cost.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "fmt" + "strconv" + + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(getFeeTiersByCostCmd) +} + +var getFeeTiersByCostCmd = &cobra.Command{ + Use: "get_fee_tiers_by_cost ", + Short: "Get wallet-friendly fee tiers for a cost", + Long: "Get economy, standard, and urgent fee tiers for a transaction cost", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + cost, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return fmt.Errorf("invalid cost specified: %s", args[0]) + } + if cost < 0 { + return fmt.Errorf("cost must be >= 0") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + cost, _ := strconv.ParseInt(args[0], 10, 64) + jsonData := map[string]interface{}{ + "cost": cost, + } + makeRequest("get_fee_tiers_by_cost", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_latest_nft_coin_by_nft_id.go b/internal/cmd/coinset/get_latest_nft_coin_by_nft_id.go new file mode 100644 index 0000000..d522558 --- /dev/null +++ b/internal/cmd/coinset/get_latest_nft_coin_by_nft_id.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(getLatestNftCoinByNftIDCmd) +} + +var getLatestNftCoinByNftIDCmd = &cobra.Command{ + Use: "get_latest_nft_coin_by_nft_id ", + Short: "Get latest NFT coin by NFT id", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + if args[0] == "" { + return fmt.Errorf("nft_id is required") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + makeRequest("get_latest_nft_coin_by_nft_id", map[string]interface{}{ + "nft_id": normalizeMaybeHex(args[0]), + }) + }, +} diff --git a/internal/cmd/coinset/get_mempool_item_by_tx_id.go b/internal/cmd/coinset/get_mempool_item_by_tx_id.go index 5e1236d..2a00bc0 100644 --- a/internal/cmd/coinset/get_mempool_item_by_tx_id.go +++ b/internal/cmd/coinset/get_mempool_item_by_tx_id.go @@ -1,31 +1,31 @@ package cmd import ( - "fmt" + "fmt" - "github.com/spf13/cobra" + "github.com/spf13/cobra" ) func init() { - rootCmd.AddCommand(getMempoolItemByTxId) + rootCmd.AddCommand(getMempoolItemByTxId) } var getMempoolItemByTxId = &cobra.Command{ - Use: "get_mempool_item_by_tx_id ", - Args: func(cmd *cobra.Command, args []string) error { - if err := cobra.ExactArgs(1)(cmd, args); err != nil { - return err - } - if isHex(args[0]) == true { - return nil - } - return fmt.Errorf("invalid hex value specified: %s", args[0]) - }, - Short: "Returns a mempool item by transaction ID", - Long: "Returns a mempool item by transaction ID", - Run: func(cmd *cobra.Command, args []string) { - jsonData := map[string]interface{}{} - jsonData["tx_id"] = formatHex(args[0]) - makeRequest("get_mempool_item_by_tx_id", jsonData) - }, -} \ No newline at end of file + Use: "get_mempool_item_by_tx_id ", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + if isHex(args[0]) == true { + return nil + } + return fmt.Errorf("invalid hex value specified: %s", args[0]) + }, + Short: "Returns a mempool item by transaction ID", + Long: "Returns a mempool item by transaction ID", + Run: func(cmd *cobra.Command, args []string) { + jsonData := map[string]interface{}{} + jsonData["tx_id"] = formatHex(args[0]) + makeRequest("get_mempool_item_by_tx_id", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_network_info.go b/internal/cmd/coinset/get_network_info.go index e222552..c704a92 100644 --- a/internal/cmd/coinset/get_network_info.go +++ b/internal/cmd/coinset/get_network_info.go @@ -1,18 +1,18 @@ package cmd import ( - "github.com/spf13/cobra" + "github.com/spf13/cobra" ) func init() { - rootCmd.AddCommand(getNetworkInfoCmd) + rootCmd.AddCommand(getNetworkInfoCmd) } var getNetworkInfoCmd = &cobra.Command{ - Use: "get_network_info", - Short: "Retrieves some information about the current network", - Long: `Retrieves some information about the current network`, - Run: func(cmd *cobra.Command, args []string) { - makeRequest("get_network_info", nil) - }, -} \ No newline at end of file + Use: "get_network_info", + Short: "Retrieves some information about the current network", + Long: `Retrieves some information about the current network`, + Run: func(cmd *cobra.Command, args []string) { + makeRequest("get_network_info", nil) + }, +} diff --git a/internal/cmd/coinset/get_nft_balance_by_p2.go b/internal/cmd/coinset/get_nft_balance_by_p2.go new file mode 100644 index 0000000..48fb5ec --- /dev/null +++ b/internal/cmd/coinset/get_nft_balance_by_p2.go @@ -0,0 +1,25 @@ +package cmd + +import "github.com/spf13/cobra" + +func init() { + rootCmd.AddCommand(getNftBalanceByP2Cmd) +} + +var getNftBalanceByP2Cmd = &cobra.Command{ + Use: "get_nft_balance_by_p2 ", + Short: "Get NFT balance by p2", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + _, err := normalizeP2Arg(args[0]) + return err + }, + Run: func(cmd *cobra.Command, args []string) { + p2, _ := normalizeP2Arg(args[0]) + makeRequest("get_nft_balance_by_p2", map[string]interface{}{ + "p2": p2, + }) + }, +} diff --git a/internal/cmd/coinset/get_nft_coins_by_nft_id.go b/internal/cmd/coinset/get_nft_coins_by_nft_id.go new file mode 100644 index 0000000..648e036 --- /dev/null +++ b/internal/cmd/coinset/get_nft_coins_by_nft_id.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var ( + getNftCoinsByNftIDIncludeSpentCoins bool + getNftCoinsByNftIDCursor string + getNftCoinsByNftIDLimit int + getNftCoinsByNftIDOrder string +) + +func init() { + getNftCoinsByNftIDCmd.Flags().BoolVar(&getNftCoinsByNftIDIncludeSpentCoins, "include-spent-coins", false, "Include spent coins") + getNftCoinsByNftIDCmd.Flags().StringVar(&getNftCoinsByNftIDCursor, "cursor", "", "Pagination cursor from a previous response") + getNftCoinsByNftIDCmd.Flags().IntVar(&getNftCoinsByNftIDLimit, "limit", 0, "Maximum number of records to return") + getNftCoinsByNftIDCmd.Flags().StringVar(&getNftCoinsByNftIDOrder, "order", "", `Result order: "asc" or "desc"`) + rootCmd.AddCommand(getNftCoinsByNftIDCmd) +} + +var getNftCoinsByNftIDCmd = &cobra.Command{ + Use: "get_nft_coins_by_nft_id ", + Short: "List NFT coins by NFT id", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + if args[0] == "" { + return fmt.Errorf("nft_id is required") + } + return applyPaginationOptions(map[string]interface{}{}, getNftCoinsByNftIDCursor, getNftCoinsByNftIDLimit, getNftCoinsByNftIDOrder) + }, + Run: func(cmd *cobra.Command, args []string) { + jsonData := map[string]interface{}{ + "nft_id": normalizeMaybeHex(args[0]), + } + if getNftCoinsByNftIDIncludeSpentCoins { + jsonData["include_spent_coins"] = true + } + _ = applyPaginationOptions(jsonData, getNftCoinsByNftIDCursor, getNftCoinsByNftIDLimit, getNftCoinsByNftIDOrder) + makeRequest("get_nft_coins_by_nft_id", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_nft_ids_by_p2.go b/internal/cmd/coinset/get_nft_ids_by_p2.go new file mode 100644 index 0000000..d652eb8 --- /dev/null +++ b/internal/cmd/coinset/get_nft_ids_by_p2.go @@ -0,0 +1,43 @@ +package cmd + +import "github.com/spf13/cobra" + +var ( + getNftIDsByP2IncludeSpentCoins bool + getNftIDsByP2Cursor string + getNftIDsByP2Limit int + getNftIDsByP2Order string +) + +func init() { + getNftIDsByP2Cmd.Flags().BoolVar(&getNftIDsByP2IncludeSpentCoins, "include-spent-coins", false, "Include spent coins") + getNftIDsByP2Cmd.Flags().StringVar(&getNftIDsByP2Cursor, "cursor", "", "Pagination cursor from a previous response") + getNftIDsByP2Cmd.Flags().IntVar(&getNftIDsByP2Limit, "limit", 0, "Maximum number of records to return") + getNftIDsByP2Cmd.Flags().StringVar(&getNftIDsByP2Order, "order", "", `Result order: "asc" or "desc"`) + rootCmd.AddCommand(getNftIDsByP2Cmd) +} + +var getNftIDsByP2Cmd = &cobra.Command{ + Use: "get_nft_ids_by_p2 ", + Short: "List NFT ids by p2", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + if _, err := normalizeP2Arg(args[0]); err != nil { + return err + } + return applyPaginationOptions(map[string]interface{}{}, getNftIDsByP2Cursor, getNftIDsByP2Limit, getNftIDsByP2Order) + }, + Run: func(cmd *cobra.Command, args []string) { + p2, _ := normalizeP2Arg(args[0]) + jsonData := map[string]interface{}{ + "p2": p2, + } + if getNftIDsByP2IncludeSpentCoins { + jsonData["include_spent_coins"] = true + } + _ = applyPaginationOptions(jsonData, getNftIDsByP2Cursor, getNftIDsByP2Limit, getNftIDsByP2Order) + makeRequest("get_nft_ids_by_p2", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_offer.go b/internal/cmd/coinset/get_offer.go new file mode 100644 index 0000000..3e6795d --- /dev/null +++ b/internal/cmd/coinset/get_offer.go @@ -0,0 +1,25 @@ +package cmd + +import "github.com/spf13/cobra" + +func init() { + rootCmd.AddCommand(getOfferCmd) +} + +var getOfferCmd = &cobra.Command{ + Use: "get_offer ", + Short: "Get offer summary/state by offer id", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + _, err := normalizeHexArg(args[0], "offer_id") + return err + }, + Run: func(cmd *cobra.Command, args []string) { + offerID, _ := normalizeHexArg(args[0], "offer_id") + makeRequest("get_offer", map[string]interface{}{ + "offer_id": offerID, + }) + }, +} diff --git a/internal/cmd/coinset/get_offers_by_cat_asset_id.go b/internal/cmd/coinset/get_offers_by_cat_asset_id.go new file mode 100644 index 0000000..c67eb86 --- /dev/null +++ b/internal/cmd/coinset/get_offers_by_cat_asset_id.go @@ -0,0 +1,58 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var ( + getOffersByCatAssetIDStatus string + getOffersByCatAssetIDFilter string + getOffersByCatAssetIDCursor string + getOffersByCatAssetIDLimit int + getOffersByCatAssetIDOrder string +) + +func init() { + getOffersByCatAssetIDCmd.Flags().StringVar(&getOffersByCatAssetIDStatus, "status", "", "Lifecycle status: open,pending,confirmed,cancel_pending,cancelled,expired") + getOffersByCatAssetIDCmd.Flags().StringVar(&getOffersByCatAssetIDFilter, "filter", "", "Offer side filter: all,requested,offered") + getOffersByCatAssetIDCmd.Flags().StringVar(&getOffersByCatAssetIDCursor, "cursor", "", "Pagination cursor from a previous response") + getOffersByCatAssetIDCmd.Flags().IntVar(&getOffersByCatAssetIDLimit, "limit", 0, "Maximum number of records to return") + getOffersByCatAssetIDCmd.Flags().StringVar(&getOffersByCatAssetIDOrder, "order", "", `Result order: "asc" or "desc"`) + rootCmd.AddCommand(getOffersByCatAssetIDCmd) +} + +var getOffersByCatAssetIDCmd = &cobra.Command{ + Use: "get_offers_by_cat_asset_id ", + Short: "List offers by CAT asset id", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + if _, err := normalizeHexArg(args[0], "asset_id"); err != nil { + return err + } + if getOffersByCatAssetIDStatus != "" && !isValidOfferStatus(getOffersByCatAssetIDStatus) { + return fmt.Errorf("invalid status: %s", getOffersByCatAssetIDStatus) + } + if getOffersByCatAssetIDFilter != "" && !isValidOfferFilter(getOffersByCatAssetIDFilter) { + return fmt.Errorf("invalid filter: %s", getOffersByCatAssetIDFilter) + } + return applyPaginationOptions(map[string]interface{}{}, getOffersByCatAssetIDCursor, getOffersByCatAssetIDLimit, getOffersByCatAssetIDOrder) + }, + Run: func(cmd *cobra.Command, args []string) { + assetID, _ := normalizeHexArg(args[0], "asset_id") + jsonData := map[string]interface{}{ + "asset_id": assetID, + } + if getOffersByCatAssetIDStatus != "" { + jsonData["status"] = getOffersByCatAssetIDStatus + } + if getOffersByCatAssetIDFilter != "" { + jsonData["filter"] = getOffersByCatAssetIDFilter + } + _ = applyPaginationOptions(jsonData, getOffersByCatAssetIDCursor, getOffersByCatAssetIDLimit, getOffersByCatAssetIDOrder) + makeRequest("get_offers_by_cat_asset_id", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_offers_by_nft_id.go b/internal/cmd/coinset/get_offers_by_nft_id.go new file mode 100644 index 0000000..248b56c --- /dev/null +++ b/internal/cmd/coinset/get_offers_by_nft_id.go @@ -0,0 +1,57 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var ( + getOffersByNftIDStatus string + getOffersByNftIDFilter string + getOffersByNftIDCursor string + getOffersByNftIDLimit int + getOffersByNftIDOrder string +) + +func init() { + getOffersByNftIDCmd.Flags().StringVar(&getOffersByNftIDStatus, "status", "", "Lifecycle status: open,pending,confirmed,cancel_pending,cancelled,expired") + getOffersByNftIDCmd.Flags().StringVar(&getOffersByNftIDFilter, "filter", "", "Offer side filter: all,requested,offered") + getOffersByNftIDCmd.Flags().StringVar(&getOffersByNftIDCursor, "cursor", "", "Pagination cursor from a previous response") + getOffersByNftIDCmd.Flags().IntVar(&getOffersByNftIDLimit, "limit", 0, "Maximum number of records to return") + getOffersByNftIDCmd.Flags().StringVar(&getOffersByNftIDOrder, "order", "", `Result order: "asc" or "desc"`) + rootCmd.AddCommand(getOffersByNftIDCmd) +} + +var getOffersByNftIDCmd = &cobra.Command{ + Use: "get_offers_by_nft_id ", + Short: "List offers by NFT id", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + if args[0] == "" { + return fmt.Errorf("nft_id is required") + } + if getOffersByNftIDStatus != "" && !isValidOfferStatus(getOffersByNftIDStatus) { + return fmt.Errorf("invalid status: %s", getOffersByNftIDStatus) + } + if getOffersByNftIDFilter != "" && !isValidOfferFilter(getOffersByNftIDFilter) { + return fmt.Errorf("invalid filter: %s", getOffersByNftIDFilter) + } + return applyPaginationOptions(map[string]interface{}{}, getOffersByNftIDCursor, getOffersByNftIDLimit, getOffersByNftIDOrder) + }, + Run: func(cmd *cobra.Command, args []string) { + jsonData := map[string]interface{}{ + "nft_id": normalizeMaybeHex(args[0]), + } + if getOffersByNftIDStatus != "" { + jsonData["status"] = getOffersByNftIDStatus + } + if getOffersByNftIDFilter != "" { + jsonData["filter"] = getOffersByNftIDFilter + } + _ = applyPaginationOptions(jsonData, getOffersByNftIDCursor, getOffersByNftIDLimit, getOffersByNftIDOrder) + makeRequest("get_offers_by_nft_id", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_offers_by_p2.go b/internal/cmd/coinset/get_offers_by_p2.go new file mode 100644 index 0000000..6c831cd --- /dev/null +++ b/internal/cmd/coinset/get_offers_by_p2.go @@ -0,0 +1,50 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var ( + getOffersByP2Status string + getOffersByP2Cursor string + getOffersByP2Limit int + getOffersByP2Order string +) + +func init() { + getOffersByP2Cmd.Flags().StringVar(&getOffersByP2Status, "status", "", "Lifecycle status: open,pending,confirmed,cancel_pending,cancelled,expired") + getOffersByP2Cmd.Flags().StringVar(&getOffersByP2Cursor, "cursor", "", "Pagination cursor from a previous response") + getOffersByP2Cmd.Flags().IntVar(&getOffersByP2Limit, "limit", 0, "Maximum number of records to return") + getOffersByP2Cmd.Flags().StringVar(&getOffersByP2Order, "order", "", `Result order: "asc" or "desc"`) + rootCmd.AddCommand(getOffersByP2Cmd) +} + +var getOffersByP2Cmd = &cobra.Command{ + Use: "get_offers_by_p2 ", + Short: "List offers by maker p2", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + if _, err := normalizeP2Arg(args[0]); err != nil { + return err + } + if getOffersByP2Status != "" && !isValidOfferStatus(getOffersByP2Status) { + return fmt.Errorf("invalid status: %s", getOffersByP2Status) + } + return applyPaginationOptions(map[string]interface{}{}, getOffersByP2Cursor, getOffersByP2Limit, getOffersByP2Order) + }, + Run: func(cmd *cobra.Command, args []string) { + p2, _ := normalizeP2Arg(args[0]) + jsonData := map[string]interface{}{ + "p2": p2, + } + if getOffersByP2Status != "" { + jsonData["status"] = getOffersByP2Status + } + _ = applyPaginationOptions(jsonData, getOffersByP2Cursor, getOffersByP2Limit, getOffersByP2Order) + makeRequest("get_offers_by_p2", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_pending_transactions_by_p2.go b/internal/cmd/coinset/get_pending_transactions_by_p2.go new file mode 100644 index 0000000..91be0c8 --- /dev/null +++ b/internal/cmd/coinset/get_pending_transactions_by_p2.go @@ -0,0 +1,38 @@ +package cmd + +import "github.com/spf13/cobra" + +var ( + getPendingTransactionsByP2Cursor string + getPendingTransactionsByP2Limit int + getPendingTransactionsByP2Order string +) + +func init() { + getPendingTransactionsByP2Cmd.Flags().StringVar(&getPendingTransactionsByP2Cursor, "cursor", "", "Pagination cursor from a previous response") + getPendingTransactionsByP2Cmd.Flags().IntVar(&getPendingTransactionsByP2Limit, "limit", 0, "Maximum number of records to return") + getPendingTransactionsByP2Cmd.Flags().StringVar(&getPendingTransactionsByP2Order, "order", "", `Result order: "asc" or "desc"`) + rootCmd.AddCommand(getPendingTransactionsByP2Cmd) +} + +var getPendingTransactionsByP2Cmd = &cobra.Command{ + Use: "get_pending_transactions_by_p2 ", + Short: "List pending transactions by p2", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + if _, err := normalizeP2Arg(args[0]); err != nil { + return err + } + return applyPaginationOptions(map[string]interface{}{}, getPendingTransactionsByP2Cursor, getPendingTransactionsByP2Limit, getPendingTransactionsByP2Order) + }, + Run: func(cmd *cobra.Command, args []string) { + p2, _ := normalizeP2Arg(args[0]) + jsonData := map[string]interface{}{ + "p2": p2, + } + _ = applyPaginationOptions(jsonData, getPendingTransactionsByP2Cursor, getPendingTransactionsByP2Limit, getPendingTransactionsByP2Order) + makeRequest("get_pending_transactions_by_p2", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_pending_transactions_by_puzzle_hash.go b/internal/cmd/coinset/get_pending_transactions_by_puzzle_hash.go new file mode 100644 index 0000000..05a4b1b --- /dev/null +++ b/internal/cmd/coinset/get_pending_transactions_by_puzzle_hash.go @@ -0,0 +1,38 @@ +package cmd + +import "github.com/spf13/cobra" + +var ( + getPendingTransactionsByPuzzleHashCursor string + getPendingTransactionsByPuzzleHashLimit int + getPendingTransactionsByPuzzleHashOrder string +) + +func init() { + getPendingTransactionsByPuzzleHashCmd.Flags().StringVar(&getPendingTransactionsByPuzzleHashCursor, "cursor", "", "Pagination cursor from a previous response") + getPendingTransactionsByPuzzleHashCmd.Flags().IntVar(&getPendingTransactionsByPuzzleHashLimit, "limit", 0, "Maximum number of records to return") + getPendingTransactionsByPuzzleHashCmd.Flags().StringVar(&getPendingTransactionsByPuzzleHashOrder, "order", "", `Result order: "asc" or "desc"`) + rootCmd.AddCommand(getPendingTransactionsByPuzzleHashCmd) +} + +var getPendingTransactionsByPuzzleHashCmd = &cobra.Command{ + Use: "get_pending_transactions_by_puzzle_hash ", + Short: "Back-compat alias for get_pending_transactions_by_p2", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + if _, err := normalizeP2Arg(args[0]); err != nil { + return err + } + return applyPaginationOptions(map[string]interface{}{}, getPendingTransactionsByPuzzleHashCursor, getPendingTransactionsByPuzzleHashLimit, getPendingTransactionsByPuzzleHashOrder) + }, + Run: func(cmd *cobra.Command, args []string) { + p2, _ := normalizeP2Arg(args[0]) + jsonData := map[string]interface{}{ + "p2": p2, + } + _ = applyPaginationOptions(jsonData, getPendingTransactionsByPuzzleHashCursor, getPendingTransactionsByPuzzleHashLimit, getPendingTransactionsByPuzzleHashOrder) + makeRequest("get_pending_transactions_by_puzzle_hash", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_raw_transaction_by_id.go b/internal/cmd/coinset/get_raw_transaction_by_id.go new file mode 100644 index 0000000..30020c8 --- /dev/null +++ b/internal/cmd/coinset/get_raw_transaction_by_id.go @@ -0,0 +1,25 @@ +package cmd + +import "github.com/spf13/cobra" + +func init() { + rootCmd.AddCommand(getRawTransactionByIDCmd) +} + +var getRawTransactionByIDCmd = &cobra.Command{ + Use: "get_raw_transaction_by_id ", + Short: "Get a raw mempool-style transaction item by id", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + _, err := normalizeHexArg(args[0], "tx_id") + return err + }, + Run: func(cmd *cobra.Command, args []string) { + txID, _ := normalizeHexArg(args[0], "tx_id") + makeRequest("get_raw_transaction_by_id", map[string]interface{}{ + "tx_id": txID, + }) + }, +} diff --git a/internal/cmd/coinset/get_singleton_info.go b/internal/cmd/coinset/get_singleton_info.go new file mode 100644 index 0000000..9f82dd2 --- /dev/null +++ b/internal/cmd/coinset/get_singleton_info.go @@ -0,0 +1,25 @@ +package cmd + +import "github.com/spf13/cobra" + +func init() { + rootCmd.AddCommand(getSingletonInfoCmd) +} + +var getSingletonInfoCmd = &cobra.Command{ + Use: "get_singleton_info ", + Short: "Get singleton info by launcher id", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + _, err := normalizeHexArg(args[0], "launcher_id") + return err + }, + Run: func(cmd *cobra.Command, args []string) { + launcherID, _ := normalizeHexArg(args[0], "launcher_id") + makeRequest("get_singleton_info", map[string]interface{}{ + "launcher_id": launcherID, + }) + }, +} diff --git a/internal/cmd/coinset/get_transaction.go b/internal/cmd/coinset/get_transaction.go new file mode 100644 index 0000000..64aa509 --- /dev/null +++ b/internal/cmd/coinset/get_transaction.go @@ -0,0 +1,25 @@ +package cmd + +import "github.com/spf13/cobra" + +func init() { + rootCmd.AddCommand(getTransactionCmd) +} + +var getTransactionCmd = &cobra.Command{ + Use: "get_transaction ", + Short: "Get a transaction by tx id", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + _, err := normalizeHexArg(args[0], "tx_id") + return err + }, + Run: func(cmd *cobra.Command, args []string) { + txID, _ := normalizeHexArg(args[0], "tx_id") + makeRequest("get_transaction", map[string]interface{}{ + "tx_id": txID, + }) + }, +} diff --git a/internal/cmd/coinset/get_transactions_by_cat_asset_id.go b/internal/cmd/coinset/get_transactions_by_cat_asset_id.go new file mode 100644 index 0000000..1411680 --- /dev/null +++ b/internal/cmd/coinset/get_transactions_by_cat_asset_id.go @@ -0,0 +1,38 @@ +package cmd + +import "github.com/spf13/cobra" + +var ( + getTransactionsByCatAssetIDCursor string + getTransactionsByCatAssetIDLimit int + getTransactionsByCatAssetIDOrder string +) + +func init() { + getTransactionsByCatAssetIDCmd.Flags().StringVar(&getTransactionsByCatAssetIDCursor, "cursor", "", "Pagination cursor from a previous response") + getTransactionsByCatAssetIDCmd.Flags().IntVar(&getTransactionsByCatAssetIDLimit, "limit", 0, "Maximum number of records to return") + getTransactionsByCatAssetIDCmd.Flags().StringVar(&getTransactionsByCatAssetIDOrder, "order", "", `Result order: "asc" or "desc"`) + rootCmd.AddCommand(getTransactionsByCatAssetIDCmd) +} + +var getTransactionsByCatAssetIDCmd = &cobra.Command{ + Use: "get_transactions_by_cat_asset_id ", + Short: "List transactions by CAT asset id", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + if _, err := normalizeHexArg(args[0], "asset_id"); err != nil { + return err + } + return applyPaginationOptions(map[string]interface{}{}, getTransactionsByCatAssetIDCursor, getTransactionsByCatAssetIDLimit, getTransactionsByCatAssetIDOrder) + }, + Run: func(cmd *cobra.Command, args []string) { + assetID, _ := normalizeHexArg(args[0], "asset_id") + jsonData := map[string]interface{}{ + "asset_id": assetID, + } + _ = applyPaginationOptions(jsonData, getTransactionsByCatAssetIDCursor, getTransactionsByCatAssetIDLimit, getTransactionsByCatAssetIDOrder) + makeRequest("get_transactions_by_cat_asset_id", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_transactions_by_coin_name.go b/internal/cmd/coinset/get_transactions_by_coin_name.go new file mode 100644 index 0000000..d4129c6 --- /dev/null +++ b/internal/cmd/coinset/get_transactions_by_coin_name.go @@ -0,0 +1,32 @@ +package cmd + +import "github.com/spf13/cobra" + +var getTransactionsByCoinNameIncludeTransactions bool + +func init() { + getTransactionsByCoinNameCmd.Flags().BoolVar(&getTransactionsByCoinNameIncludeTransactions, "include-transactions", false, "Include created/spent transaction payloads when available") + rootCmd.AddCommand(getTransactionsByCoinNameCmd) +} + +var getTransactionsByCoinNameCmd = &cobra.Command{ + Use: "get_transactions_by_coin_name ", + Short: "Get associated transactions for a coin", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + _, err := normalizeHexArg(args[0], "coin_name") + return err + }, + Run: func(cmd *cobra.Command, args []string) { + coinName, _ := normalizeHexArg(args[0], "coin_name") + jsonData := map[string]interface{}{ + "coin_name": coinName, + } + if getTransactionsByCoinNameIncludeTransactions { + jsonData["include_transactions"] = true + } + makeRequest("get_transactions_by_coin_name", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_transactions_by_nft_id.go b/internal/cmd/coinset/get_transactions_by_nft_id.go new file mode 100644 index 0000000..b4e1b65 --- /dev/null +++ b/internal/cmd/coinset/get_transactions_by_nft_id.go @@ -0,0 +1,41 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var ( + getTransactionsByNftIDCursor string + getTransactionsByNftIDLimit int + getTransactionsByNftIDOrder string +) + +func init() { + getTransactionsByNftIDCmd.Flags().StringVar(&getTransactionsByNftIDCursor, "cursor", "", "Pagination cursor from a previous response") + getTransactionsByNftIDCmd.Flags().IntVar(&getTransactionsByNftIDLimit, "limit", 0, "Maximum number of records to return") + getTransactionsByNftIDCmd.Flags().StringVar(&getTransactionsByNftIDOrder, "order", "", `Result order: "asc" or "desc"`) + rootCmd.AddCommand(getTransactionsByNftIDCmd) +} + +var getTransactionsByNftIDCmd = &cobra.Command{ + Use: "get_transactions_by_nft_id ", + Short: "List transactions by NFT id", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + if args[0] == "" { + return fmt.Errorf("nft_id is required") + } + return applyPaginationOptions(map[string]interface{}{}, getTransactionsByNftIDCursor, getTransactionsByNftIDLimit, getTransactionsByNftIDOrder) + }, + Run: func(cmd *cobra.Command, args []string) { + jsonData := map[string]interface{}{ + "nft_id": normalizeMaybeHex(args[0]), + } + _ = applyPaginationOptions(jsonData, getTransactionsByNftIDCursor, getTransactionsByNftIDLimit, getTransactionsByNftIDOrder) + makeRequest("get_transactions_by_nft_id", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_transactions_by_p2.go b/internal/cmd/coinset/get_transactions_by_p2.go new file mode 100644 index 0000000..f43c51f --- /dev/null +++ b/internal/cmd/coinset/get_transactions_by_p2.go @@ -0,0 +1,38 @@ +package cmd + +import "github.com/spf13/cobra" + +var ( + getTransactionsByP2Cursor string + getTransactionsByP2Limit int + getTransactionsByP2Order string +) + +func init() { + getTransactionsByP2Cmd.Flags().StringVar(&getTransactionsByP2Cursor, "cursor", "", "Pagination cursor from a previous response") + getTransactionsByP2Cmd.Flags().IntVar(&getTransactionsByP2Limit, "limit", 0, "Maximum number of records to return") + getTransactionsByP2Cmd.Flags().StringVar(&getTransactionsByP2Order, "order", "", `Result order: "asc" or "desc"`) + rootCmd.AddCommand(getTransactionsByP2Cmd) +} + +var getTransactionsByP2Cmd = &cobra.Command{ + Use: "get_transactions_by_p2 ", + Short: "List confirmed transactions by p2", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + if _, err := normalizeP2Arg(args[0]); err != nil { + return err + } + return applyPaginationOptions(map[string]interface{}{}, getTransactionsByP2Cursor, getTransactionsByP2Limit, getTransactionsByP2Order) + }, + Run: func(cmd *cobra.Command, args []string) { + p2, _ := normalizeP2Arg(args[0]) + jsonData := map[string]interface{}{ + "p2": p2, + } + _ = applyPaginationOptions(jsonData, getTransactionsByP2Cursor, getTransactionsByP2Limit, getTransactionsByP2Order) + makeRequest("get_transactions_by_p2", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_transactions_by_puzzle_hash.go b/internal/cmd/coinset/get_transactions_by_puzzle_hash.go new file mode 100644 index 0000000..b02e446 --- /dev/null +++ b/internal/cmd/coinset/get_transactions_by_puzzle_hash.go @@ -0,0 +1,38 @@ +package cmd + +import "github.com/spf13/cobra" + +var ( + getTransactionsByPuzzleHashCursor string + getTransactionsByPuzzleHashLimit int + getTransactionsByPuzzleHashOrder string +) + +func init() { + getTransactionsByPuzzleHashCmd.Flags().StringVar(&getTransactionsByPuzzleHashCursor, "cursor", "", "Pagination cursor from a previous response") + getTransactionsByPuzzleHashCmd.Flags().IntVar(&getTransactionsByPuzzleHashLimit, "limit", 0, "Maximum number of records to return") + getTransactionsByPuzzleHashCmd.Flags().StringVar(&getTransactionsByPuzzleHashOrder, "order", "", `Result order: "asc" or "desc"`) + rootCmd.AddCommand(getTransactionsByPuzzleHashCmd) +} + +var getTransactionsByPuzzleHashCmd = &cobra.Command{ + Use: "get_transactions_by_puzzle_hash ", + Short: "Back-compat alias for get_transactions_by_p2", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + if _, err := normalizeP2Arg(args[0]); err != nil { + return err + } + return applyPaginationOptions(map[string]interface{}{}, getTransactionsByPuzzleHashCursor, getTransactionsByPuzzleHashLimit, getTransactionsByPuzzleHashOrder) + }, + Run: func(cmd *cobra.Command, args []string) { + p2, _ := normalizeP2Arg(args[0]) + jsonData := map[string]interface{}{ + "p2": p2, + } + _ = applyPaginationOptions(jsonData, getTransactionsByPuzzleHashCursor, getTransactionsByPuzzleHashLimit, getTransactionsByPuzzleHashOrder) + makeRequest("get_transactions_by_puzzle_hash", jsonData) + }, +} diff --git a/internal/cmd/coinset/get_unfinished_block_headers.go b/internal/cmd/coinset/get_unfinished_block_headers.go index 1e39bce..f31b83f 100644 --- a/internal/cmd/coinset/get_unfinished_block_headers.go +++ b/internal/cmd/coinset/get_unfinished_block_headers.go @@ -1,18 +1,18 @@ package cmd import ( - "github.com/spf13/cobra" + "github.com/spf13/cobra" ) func init() { - rootCmd.AddCommand(getUnfinishedBlockHeadersCmd) + rootCmd.AddCommand(getUnfinishedBlockHeadersCmd) } var getUnfinishedBlockHeadersCmd = &cobra.Command{ - Use: "get_unfinished_block_headers", - Short: "Retrieves recent unfinished header blocks", - Long: `Retrieves recent unfinished header blocks`, - Run: func(cmd *cobra.Command, args []string) { - makeRequest("get_unfinished_block_headers", nil) - }, -} \ No newline at end of file + Use: "get_unfinished_block_headers", + Short: "Retrieves recent unfinished header blocks", + Long: `Retrieves recent unfinished header blocks`, + Run: func(cmd *cobra.Command, args []string) { + makeRequest("get_unfinished_block_headers", nil) + }, +} diff --git a/internal/cmd/coinset/get_xch_balance_by_p2.go b/internal/cmd/coinset/get_xch_balance_by_p2.go new file mode 100644 index 0000000..d7af0c8 --- /dev/null +++ b/internal/cmd/coinset/get_xch_balance_by_p2.go @@ -0,0 +1,25 @@ +package cmd + +import "github.com/spf13/cobra" + +func init() { + rootCmd.AddCommand(getXchBalanceByP2Cmd) +} + +var getXchBalanceByP2Cmd = &cobra.Command{ + Use: "get_xch_balance_by_p2 ", + Short: "Get XCH balance by p2", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + _, err := normalizeP2Arg(args[0]) + return err + }, + Run: func(cmd *cobra.Command, args []string) { + p2, _ := normalizeP2Arg(args[0]) + makeRequest("get_xch_balance_by_p2", map[string]interface{}{ + "p2": p2, + }) + }, +} diff --git a/internal/cmd/coinset/util.go b/internal/cmd/coinset/util.go index 20d0147..dc9b5d2 100644 --- a/internal/cmd/coinset/util.go +++ b/internal/cmd/coinset/util.go @@ -37,6 +37,10 @@ func formatHex(str string) string { return "0x" + str } +func isAscOrDesc(order string) bool { + return order == "asc" || order == "desc" +} + func convertAddressOrPuzzleHash(input string) (string, error) { if isAddress(input) { _, puzzleHashBytes, err := bech32m.DecodePuzzleHash(input) diff --git a/internal/cmd/coinset/version.go b/internal/cmd/coinset/version.go index b28ca7e..9a75153 100644 --- a/internal/cmd/coinset/version.go +++ b/internal/cmd/coinset/version.go @@ -1,20 +1,20 @@ package cmd import ( - "fmt" + "fmt" - "github.com/spf13/cobra" + "github.com/spf13/cobra" ) func init() { - rootCmd.AddCommand(versionCmd) + rootCmd.AddCommand(versionCmd) } var versionCmd = &cobra.Command{ - Use: "version", - Short: "Print the version number of coinset", - Long: `All software has versions. This is coinset's`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("Coinset %s\n", version) - }, -} \ No newline at end of file + Use: "version", + Short: "Print the version number of coinset", + Long: `All software has versions. This is coinset's`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("Coinset %s\n", version) + }, +} From ef3cb80fab1e2a4883bd04357554647817ea9276 Mon Sep 17 00:00:00 2001 From: Cameron Cooper Date: Thu, 26 Feb 2026 08:50:27 -0600 Subject: [PATCH 2/9] added pretty transaction printing --- README.md | 3 + internal/cmd/coinset/cats.go | 902 ++++++++++ .../cmd/coinset/get_block_transactions.go | 4 +- .../coinset/get_pending_transactions_by_p2.go | 4 +- ...get_pending_transactions_by_puzzle_hash.go | 4 +- internal/cmd/coinset/get_transaction.go | 7 +- .../get_transactions_by_cat_asset_id.go | 4 +- .../coinset/get_transactions_by_coin_name.go | 8 +- .../cmd/coinset/get_transactions_by_nft_id.go | 4 +- .../cmd/coinset/get_transactions_by_p2.go | 4 +- .../get_transactions_by_puzzle_hash.go | 4 +- internal/cmd/coinset/transaction_pretty.go | 1509 +++++++++++++++++ 12 files changed, 2446 insertions(+), 11 deletions(-) create mode 100644 internal/cmd/coinset/cats.go create mode 100644 internal/cmd/coinset/transaction_pretty.go diff --git a/README.md b/README.md index d7e3428..fa20811 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,9 @@ The following table shows all available CLI commands organized by functionality: | **Events & Real-time** | `events [type]` | Connect to WebSocket and display events (`peak`, `transaction`, `offer`, `balance`, `dashboard`) | | **Utility** | `version` | Display the version number of coinset | +Transaction summary commands support `-p, --pretty` to render expanded human-friendly output +(`TX` header + `Activity` event lines + detailed event sections) instead of JSON. + ### Smoke Test Checklist - `coinset get_coin_records_by_puzzle_hash --limit 5 --order desc` diff --git a/internal/cmd/coinset/cats.go b/internal/cmd/coinset/cats.go new file mode 100644 index 0000000..82ca5d7 --- /dev/null +++ b/internal/cmd/coinset/cats.go @@ -0,0 +1,902 @@ +// Code generated from docs/cats.json; DO NOT EDIT. +package cmd + +import "strings" + +type CatMetadata struct { + Code string + Name string +} + +var catLookupByID = map[string]CatMetadata{ + "00000000024e1fb9fc47c7ec72854c6a987c4cc99f6535a4caca6154220eeda5": {Code: "0000", Name: "0000... Zeros"}, + "001f4b4f03e1b5bdae1e4a757e9b3d7e5a62032332fb89887c5262bd736f0d68": {Code: "MECH", Name: "MECH"}, + "00535a3707d006d91221cb70eaa00f4b707bcd8ddf01b2a8bc4498875559832a": {Code: "XHAY", Name: "XHAY"}, + "009feb6bfa505c023a3d7e127e8ef1cf8631ff8e919c66ecf504588df17b3c28": {Code: "QGC", Name: "Quirky Giraffes Coin"}, + "01ac0cb37e80075dd028e2ceb433a79a5eb62649d5fbfd2e94650e33fa7d1cff": {Code: "GOAT", Name: "Goat Coin"}, + "01e88bf48c96294b6f29435bfcb7f907b47bcaf736d4f128b120d2775013cbf3": {Code: "TIBET-SHROOM-XCH", Name: "TibetSwap LP SHROOM-XCH"}, + "0246bb1c84fa6f64d6684f8ffaeb54b67796dad12dcb5a47d590d333f5a430a8": {Code: "TIBET-ALGOLD-XCH", Name: "TibetSwap LP ALGOLD-XCH"}, + "0290cc08748796ebe6fcf4e44b4833da10dda85068987bab7c8e32dfe87f9e88": {Code: "TIBET-CAT-XCH", Name: "TibetSwap LP CAT-XCH"}, + "0389988a8c640c8e0006d0ad3d3055ecfb460d4c78fd9a917af8f3057e4043b0": {Code: "KOAM", Name: "King of All Mojos"}, + "03c18a17fd92e879cdbaa7bb5a947976ea28ee2ee17847ea21517da5e6b2492c": {Code: "RAT", Name: "Revocable Asset Token"}, + "03cec543bfe0d76b40b83e7bfeaefb2f5741724bd8e496dd5af02fe1a36e47a5": {Code: "TIBET-THC-XCH", Name: "TibetSwap LP THC-XCH"}, + "048b1358f3b55a70c4db22114c2f52569c0398ba19e8212b8daf1cb25c90a641": {Code: "HonK", Name: "HonK"}, + "048b1495819237bc5ef3dddf3872f3c2143771066530ea847302cc235c9b219f": {Code: "GPC", Name: "Gigaparsec"}, + "04e28f2f24e1a8ba8290b30293976cdee933d1683220014316da6f8ee7753920": {Code: "XCC", Name: "Chiania Coin"}, + "052be4ab8ee02df7ee293571b6b148a1923d6c8c0a6d117f47d00714a0d3c61f": {Code: "UMBER", Name: "Umber Oak"}, + "054e8c7e593d0e7bab9ec83daa50a3b099e366097d445a0f125eb800c0370e44": {Code: "BGPL", Name: "Brick of GP Latinum"}, + "0570076f3c2cd8d6e65f9b56f96ab92079d3ce7ab7372c751e1c19fdb42b25d4": {Code: "GRAPE", Name: "APULIA GRAPE"}, + "05a6a22bd11c61a5dbfb0d1ab5ac4cfd23da775f594dabf7a04e901c9fcc728a": {Code: "BOTL2", Name: "Makers Mead Bottle 2"}, + "061cc6609d58b443abc4a34f0b77b3fb95215b37024128e39838335c21c226d4": {Code: "WINE", Name: "APULIA WINE"}, + "061cf8381b419c6ad6879e53b32d0d8c05855f2a43187fae3fcaf96def5a4247": {Code: "GPT", Name: "Genesis Pickle Token"}, + "07254f2fac2a4a9c0d7202e2a01b7bc7b6bbbd8bc4bf5bf870468ff1515fa12c": {Code: "ILLUM", Name: "Illuminite"}, + "0736e0a27b615a1c62ae655a577325ef8905cab958d6d74b1f3682c48615b377": {Code: "DZ", Name: "DZ"}, + "08410d6db9addd81b7c1342c104b27444c1491f51394b6eb6860fe7de3877532": {Code: "STC", Name: "Space Time Cats"}, + "086287bab372e0fcf2ce36b43e4eab3e0ad0c6fcf6e1a326612e5240a2a8abb5": {Code: "CAT", Name: "Chia Asset Token"}, + "08a45d7e7e26cc85fc5e469be7f4dcb9ddb3830c736a8997cbf1f98e990a41d1": {Code: "TIBET-TIBET-IYKYK-XCH-XCH", Name: "TibetSwap LP TIBET-IYKYK-XCH-XCH"}, + "08beac6f8ae466d12e3bb710b775626549e11998d2f79d89708acee497aef1ba": {Code: "FART", Name: "Fartbucks"}, + "08ea9e7a0f7a57fe617a03404f2912b1ba073ea4ebb7789a57ba1c2bf42d5486": {Code: "TIBET-TIBET-PEPE-XCH-XCH", Name: "TibetSwap LP TIBET-PEPE-XCH-XCH"}, + "0941dc178e75d3699fab42a002034cae455ba5dfc91a9a9f58b62c48c9cea754": {Code: "CHAD", Name: "Chad"}, + "0957ed359f099d5846c2c74c42e7c2eb9608b58c32e320e144840e3efab0b747": {Code: "MIRROR", Name: "MIRROR"}, + "099e11465ef07b31b69ecea19589f84502755c4f93d1f74a0e817881065b8f87": {Code: "CGLC", Name: "CatGurlCoin"}, + "0a039f753ba4387869fc001771518d802bff1123d2aad5a885cca57a78492241": {Code: "PLOT", Name: "Farmer Diamonds - Plot"}, + "0a0c42b14418562db48d12928940e2b9a9d6a76a36e58f727372b8c2f428bf56": {Code: "TIBET-WOW-XCH", Name: "TibetSwap LP WOW-XCH"}, + "0a7294d62a5c4cb78930f03d5d988f3b88496eee481f176c5264ccd149289689": {Code: "NBR", Name: "NEIGHBOR"}, + "0b0eb96bbba2e53d612ad2c7406b7107f3de71105bec00bc3a4457bf9a587b0d": {Code: "TIBET-PP-XCH", Name: "TibetSwap LP PP-XCH"}, + "0b3dc46f825f7d0ce0f47879e6d39312a95da956e61446317ce5fe5af564442e": {Code: "FCBT", Name: "FCB Fan Token"}, + "0b597916bbb1c223407accc5ea44753b6c726c3cef45cdd7e65b84347bd74029": {Code: "GGG", Name: "Grinning Goblins Gold"}, + "0b6e3a7edf997d69d15bab7627855d09b2dababdf92552b843002ca873b8f497": {Code: "TIBET-MRMT-XCH", Name: "TibetSwap LP MRMT-XCH"}, + "0bc56c4c5060a7e962cd642ca065206e5ad5b533a7ea56fc8a6ca2642975943f": {Code: "TIBET-\U0001F42C-XCH", Name: "TibetSwap LP \U0001F42C-XCH"}, + "0bf20840681f1b1d71e54cf49f0cd5269f4cc405b347409d5e2e325a226c8804": {Code: "TAIL", Name: "CoinTail"}, + "0bfbc7095597df453a6e8ab24b2d544f5f288d7e4613e2e0a79c11171572dbea": {Code: "GDN", Name: "Garden"}, + "0c5007ec7a9a4d7c971b92f5bc264c07e7cf4d0cb8ee5e11e5377e1d9caf586f": {Code: "WYRD", Name: "Wyrdwood"}, + "0d2151e3d1e9d9f9bca37eb322e4045925e2fad5af1d7ff7dd98eb3f6c963d7e": {Code: "TIBET-TIBET-TIBET-TIBET-TIBET-USA-XCH-XCH-XCH-XCH-XCH", Name: "TibetSwap LP TIBET-TIBET-TIBET-TIBET-USA-XCH-XCH-XCH-XCH-XCH"}, + "0d3cdcf5e7a1a8c240e55f43e709d9ac516f53daad470150f78f742919a68d72": {Code: "TIBET-\U0001F4A8-XCH", Name: "TibetSwap LP \U0001F4A8-XCH"}, + "0d56d7f0ebc0a7a02f6538f76395da85bf9da946904824ddfd94a7ad24e47884": {Code: "TIBET-NAME-XCH", Name: "TibetSwap LP NAME-XCH"}, + "0d595bef07becc2878f59f6e88168a11cb27a29c3ac6404b60a5d06708391ed7": {Code: "TIBET-TAU-XCH", Name: "TibetSwap LP TAU-XCH"}, + "0e01b1418f5b8e1c8a636ad625793803088ca1ec62a3e8d883abce05eb0101a5": {Code: "BUG", Name: "Billybug Bounty"}, + "0e3db26887c5d8659baba5e42c1c47c99580112e42a343c42357d4fa5e07a222": {Code: "MB", Name: "MarmotBox"}, + "0ed71c399419b16df76ae7cde9fa257f1dbf845bef462b7f9ea6de8d181cdf97": {Code: "CIC", Name: "City Illusion Coin"}, + "0f3acd842a1723d67f382a2e59875f2c1a45f86b6a3d5b7fb1682f1b8410b123": {Code: "TIBET-DEGEN-XCH", Name: "TibetSwap LP DEGEN-XCH"}, + "0f5867e44ff5ae301e924c8b654a1005d4a1a961c1fd1d0c0e0b328cacd8ca51": {Code: "WLBF42", Name: "WizardLittleBearFriends42HoffCohen"}, + "0f7c3c1a00998c535179ff0c7831cf0d44c18a5d28e5878182d68df266ba9542": {Code: "GLDO", Name: "Gold Ore"}, + "0f8676bd61b83d8cc8543cf75a54bb6963ea7c9465eb2da4b84901778ad37d3f": {Code: "XPCD", Name: "XCHPool Discount Coupon"}, + "0fe22b51e5a2e641a6153f7d4e56245d2865a16cb13ec207e7504128fa954204": {Code: "TIBET-boden-XCH", Name: "TibetSwap LP boden-XCH"}, + "0fe711bf57d0222603a8d3e13df54b29382373ab2b05261c69667360a5738d64": {Code: "NOT1", Name: "Sappho Lyric"}, + "1065c0b2dd987e634ab3e48c1883e629bf7c475315c892e39138360606ebb00c": {Code: "TVS", Name: "TVS"}, + "109ccae6e0ff923c080e31bebc61c2663d79d0e6b47735d32811d0c3e42a5283": {Code: "WOOD", Name: "OfferWars WOOD"}, + "10ef2a107d283606f6734f69b9bce8e18ef8d8dc4ba08ac18d02c4b138aecaf4": {Code: "$MMC", Name: "Mini Motions Coin"}, + "1129f39816dc92bd225cd7eba883f07bb35ecdc639ecfc07bf4db7e58c8cfbb1": {Code: "TIBET-XEG-XCH", Name: "TibetSwap LP XEG-XCH"}, + "118abe6c8398f4ebac0eab0f8221244276f17335ad8df85ce31798ba75933fd3": {Code: "TIBET-NIOC-XCH", Name: "TibetSwap LP NIOC-XCH"}, + "1231b8487b7501767b0a737dd849f80fa592dfc00a90c26c13003bf111ff7e16": {Code: "TIBET-WHUP-XCH", Name: "TibetSwap LP WHUP-XCH"}, + "125ef688c3200d4a82248c31f6dcfe2dc45d549b779c0b3b1ef35568fea840b6": {Code: "AGIC", Name: "A Golden Caesar"}, + "12cc4ab2ce7ea00fc154456614265a624535e64dccb26fad7d2243ce9a83e973": {Code: "MPLOT", Name: "Multiplots"}, + "1397080ca3d0b477b2f6a5ed06750a63b6eea11931e948ee074ad12307fe782e": {Code: "HEAD", Name: "CoinHead"}, + "13f1e28d66064a17fe5b7155e17927c491493b9d42717debaaf9e219bf58fc9a": {Code: "BALLZ", Name: "Ballzcoin"}, + "14142d7922b2487d1a145db198f7cc232a99a2798ec7da90279af45270e53465": {Code: "GOLD", Name: "GOLD"}, + "14b40962dfef81d954ac0d92b51ec21ce7acd8c62dd9fef9303aa51c615cb495": {Code: "LLBT", Name: "LLC Burn Token"}, + "14feb393ffb015b7becf2c1eea218e4fbfef0bb932366b793d7c5193a8197358": {Code: "GM \u2615", Name: "GM"}, + "1502c633aca69108171cba44e473cae15761f5fa7df8eb19d012a6362c4ba72e": {Code: "SPRITE", Name: "Wasteland Sprites"}, + "15ad8f8e45be4bfc2397107e1df30d685819eab05f545367ffc694b9cb2e400a": {Code: "TIBET-{WC}-XCH", Name: "TibetSwap LP {WC}-XCH"}, + "15d397bb558152652ce94f5933c77100e5d425a7f7bf4c5a3a18d2752f6ea3b7": {Code: "RDx", Name: "RODx"}, + "15e436bc1d59221bd5331ce6cfdfd5bae6aa827347bb7304284c62972a3f4c97": {Code: "OUTR", Name: "Outer Fields | OUTR Token"}, + "1653a1df583f3ae6a822ab214b74d2a08fb4309025d54f2db140e5e6bc06e9da": {Code: "MANA", Name: "\U0001F9D9\u200D\u2642\uFE0F"}, + "167558c9bac114cbf7b58f1aa4cad43153e3b06ce292199918268b6f3f46e5bf": {Code: "Bark!", Name: "Bark!"}, + "1698907709fcf295b7771a27a5dff24fa9d817a8f70889a413ede07cd9eb8507": {Code: "MGKB", Name: "Magickal Billys"}, + "173ea1f42bea14966e9b08d24587aa80e14c7b6bc36bf98ce1e1fc0e977cd519": {Code: "TIBET-CBDC-XCH", Name: "TibetSwap LP CBDC-XCH"}, + "179e003d01aea5b348cbcf3fecbb0fe8682e37d7d1aeea8903844823cb32caa1": {Code: "IRONO", Name: "Iron Ore"}, + "18396fd18e52cf5ecad4a3509ea2482c55b58a614c540bb95d1ecb3e85a3d723": {Code: "TIBET-SPACE-XCH", Name: "TibetSwap LP SPACE-XCH"}, + "18b3cee443eadc1bd72ca0486ade9113a066c008348bec6cd8a5008f7e4d434b": {Code: "TIBET-ZIG-XCH", Name: "TibetSwap LP ZIG-XCH"}, + "18cf38a8d4ebabc814b58e84163928ad39db6d3371d528f0a7a265eaac933b1e": {Code: "DOGX", Name: "Dogmatix"}, + "1959758d650caff790f6e8b430aed7cd3854a93875c293dbc7cc9ee4841d3f50": {Code: "XL20", Name: "Chialing Twenties"}, + "1966f233c00315da03762d9d006e5758107087efad7c4d391ec89d022f81d3d9": {Code: "ARCDS", Name: "Arcane Dust"}, + "19823a33c11d6707ed6ea9481f8104947a4d8b817dc78366a328402177959e62": {Code: "TIBET-CHILLGUY-XCH", Name: "TibetSwap LP CHILLGUY-XCH"}, + "1a2652d2ac20635348a24b575f96d716941f8e52214a7df69c857599740b4d90": {Code: "TIBET-XSTONE-XCH", Name: "TibetSwap LP XSTONE-XCH"}, + "1a3c66a4cd6aed6b5bf37342f4b59d5d61d50737d4fc8916dd314817810b8db9": {Code: "OFAS", Name: "Outer Fields | Starter Pack"}, + "1a8bd94ce5b26ef7fdedec6a21748a747fc8d1f05972efd248b63298c3e5be96": {Code: "XPC", Name: "Pool Coin"}, + "1aa6ddf6a56c30b5a1712905e35e470eb81331e78558cdebe05f7d0febdd6502": {Code: "TIBET-TIBET-\U0001F37A-XCH-XCH", Name: "TibetSwap LP TIBET-\U0001F37A-XCH-XCH"}, + "1ad673d21799c9a224014ca71f9fe07cbc836fa23fa97b3be275d46d0b8bd9da": {Code: "NeckCoin", Name: "NeckCoin"}, + "1af35863d6b110eb70150b10f6bb1f72863a79934d6851cdbf3315bb0a258c20": {Code: "CHM", Name: "ChakMoon"}, + "1b894664dbdf9eae44d9ede9c8071402ffed19b6fd672bf36e3085df42cdf2f1": {Code: "HODL", Name: "HODL Coin"}, + "1b9b4f70b6da701b445d7a0de1a45a0038abfcd2e448503a32d3de1b182d41c3": {Code: "XTECH", Name: "XTECH"}, + "1bb207c40ec1c4099499b77aa3c657e48eec03f1ea157f500c3f5d46df65c77a": {Code: "TIBET-XMETAL-XCH", Name: "TibetSwap LP XMETAL-XCH"}, + "1bc3bdfc30f47f01ca4ca55df8d135ed2b66ad24e8ddd2c114d40d47e5a5d02b": {Code: "CAT6OR", Name: "CATMOS Scissors"}, + "1c009be94c996222ef28d4f7a2c1e38f8699542ee14ef9d26a853ff0a9540a6d": {Code: "TAU", Name: "TAU Token"}, + "1c0bc24766f94b98ba88c9752d2b488ded14c97b85665eac42cf65feada468d6": {Code: "TIBET-CTK-XCH", Name: "TibetSwap LP CTK-XCH"}, + "1c5d566419728c54790835f8043abf02395043bc2055c23f97b2218d2c9c01c3": {Code: "HRTST", Name: "Heartstone"}, + "1c7e47075275b3f5f43a9d1a4948db94039348f95ffce17c9d8beb28566ce54c": {Code: "TIBET-RUG-XCH", Name: "TibetSwap LP RUG-XCH"}, + "1c88979bf0db25ea7a33e400c5a8222fcd3a5a6f02b429304238b06ed869e4ea": {Code: "SUNGR", Name: "Sungrove"}, + "1d0d63d3f3384605cb22866a48b7a855938572c5ef02fdbe2f14210ee4c03e0e": {Code: "OLUT", Name: "Kaljaviikko"}, + "1d3c0c66804da57f0ef64dd61048be2c52a2fae38b6eb129b1d9e7686b0f8dde": {Code: "BOO", Name: "Ghost Token"}, + "1dadf5b097d2e1257d375759aaa15298ed3e0f70b6de275d9c70dc2f7eca14a8": {Code: "MIO", Name: "Marmots.io"}, + "1db8e4e1d9c7493b880df398444f8c6d4bd976e6de3c1f74181dd071a9e2d498": {Code: "CF3", Name: "Cassandre Fairies Club"}, + "1e6774d988a7d383fab822053b304358a98bc3dd7adcc2656e07dbfec92e6f86": {Code: "TIBET-NWO-XCH", Name: "TibetSwap LP NWO-XCH"}, + "1e8cbe903032fd32fbf5ced44ba0f9576b4903160d8b67e02c25f5ea5b8fde86": {Code: "TIBET-MRBL-XCH", Name: "TibetSwap LP MRBL-XCH"}, + "1ea65224cb4d464424c297a6a8a68768f6f2512b9130c2397cd9f9e27e23a354": {Code: "TIBET-OXC-XCH", Name: "TibetSwap LP OXC-XCH"}, + "1fa352483aad7d9fa6574525866054da9b9739bf97fe71d9129ccfd24bdc08b7": {Code: "TIBET-BEER-XCH", Name: "TibetSwap LP BEER-XCH"}, + "2053459a8f479b11cd57c54bdce4818b10b3a4ccea2267e5ab1ff7ae281f5649": {Code: "DSC", Name: "Discount Token"}, + "206a46714efaddda2b09a90720f3a1ad7f6d9dac3902939d0ab974f98f89e8e6": {Code: "AIG", Name: "AI Agent"}, + "209fd79450d51856eaa980af5d14bbc0d4312e692817f6c014820733d5902ba7": {Code: "DIAS", Name: "Farmer Diamonds"}, + "2135d12899cffd0085c49e1c1eb5296e35565bf66cfda1ef2df8d6a7ea08f4aa": {Code: "SGPL", Name: "Slip of GP Latinum"}, + "216475ebcf59f28e0fb2f99f9a742a59f42e7906ccaff353e535aa83af3c83bb": {Code: "MELT", Name: "MELT"}, + "21ab5244a2732e01ebc0491e1cee8e0d9dc203dd63ac73ca6e523d6138e12f18": {Code: "CAPES", Name: "APES IN CHIA"}, + "22945c6c10f2c1be9f4b2484652768498cf1e4153c015a7bf981913a6b21430a": {Code: "BGCI", Name: "Board Game Crypto Intro"}, + "239006fa6efac7002675e41aba4df685efd6b1443190ea46a3e8a67e724f222c": {Code: "MITH", Name: "Mithril Ore"}, + "2444e71c79fcb69339cc4768445b9a14e85805bfcc09b0b05b7824e4706bc908": {Code: "DEVIL", Name: "DEVIL"}, + "24681286bb98d901647a863b59c6c932408ebdc5c02ccd885b01d652adc1710e": {Code: "\U0001F99E", Name: "Lobsters"}, + "254a6ba2033ce9369ddebca5617ab3beb6db01c3dee744b8946286ca93d41552": {Code: "TIBET-\U0001F30E-XCH", Name: "TibetSwap LP \U0001F30E-XCH"}, + "258a2aaf40fbbe73d968df4a478092160e9e6a72ff20090dc6f50871a83e4f39": {Code: "TIBET-TIBET-LOVE-XCH-XCH", Name: "TibetSwap LP TIBET-LOVE-XCH-XCH"}, + "2655201e2fdc84fc08ef19e4d6b971110d750a9dadac1359d4ee4d44ddf7c51f": {Code: "CLN", Name: "Chia Lotto Nft"}, + "26579e01e312311ab70ffd7eb526ceb3cdaf5122f8cf7efb7ce999556406f72e": {Code: "TIBET-MAGE-XCH", Name: "TibetSwap LP MAGE-XCH"}, + "265bd83e40da62ba95330c737be25245255c9b5a0c3102daa35c60ba01acfe06": {Code: "SKULLY", Name: "Skully"}, + "265df5ddcd033ee3aa4c9fd29b99056175382d73a5618237ca9e08ace4b67313": {Code: "TIBET-ETHSC-XCH", Name: "TibetSwap LP ETHSC-XCH"}, + "26c29cff746d8d0c01550cebc305b8f9fa4f236dc70fdcdb71dcfbc39e2b71d0": {Code: "BCRED", Name: "BCAP RED"}, + "26cce5b7583e4ae0d8963afebe4dbd587841e22766b80aac8dac0e25486f110a": {Code: "\U0001F514", Name: "$BELL"}, + "26f25ea7538617f038d644dcdfaa7c055561ec6c1bbf32ad7db6e457217f9f25": {Code: "TIBET-LOVE-XCH", Name: "TibetSwap LP LOVE-XCH"}, + "271b8779e8454aeb4b0e0dd2b09900e1976161f58b88ad6682529870b312b81d": {Code: "COW", Name: "\U0001F42E"}, + "287b633167f822353444492c96030db71ee78e6ac33897ea4c572623aa4c94d2": {Code: "XIP", Name: "Fools Gold"}, + "28a7dde2dd666897c87b8dfc840ac22b51991e51b098cea92e8e0704e92611ff": {Code: "TIBET-CPZ-XCH", Name: "TibetSwap LP CPZ-XCH"}, + "29320e8cfbc13684e9f5471b65e7d99684b369c91bbadb11b0a56c20f76917c5": {Code: "ZOMB", Name: "Zomb"}, + "2964aead53ded9f630682014520b6c2b674a963be8be1e8d252e8a432b7d6be0": {Code: "TIBET-ALWEAP-XCH", Name: "TibetSwap LP ALWEAP-XCH"}, + "2b0b9542278bd983d292d4b335460f3ddffd911cff98276fe488bd43f5d3ef7a": {Code: "XCOW", Name: "XCOW"}, + "2b5cb14992f19cc37508854b14defb7a16fead8ab502925e9bbbdd1ae4d53b56": {Code: "DGM", Name: "Digital Mine"}, + "2b9f2ab9e2e7d87d2d9cc5724feb0f46fbc418d6c9c6f23434b1fc5e40921ac1": {Code: "TIBET-LFG-XCH", Name: "TibetSwap LP LFG-XCH"}, + "2c6ff33a981d89330d882bcfeee148a14b0de8b0308ef71b90269ac9c1adeebc": {Code: "TIBET-SPACES-XCH", Name: "TibetSwap LP SPACES-XCH"}, + "2ccbaed18bc14fcd3abe862c229ae4730e9c29cac46a5c0996e25b3d435327be": {Code: "NWO", Name: "New World Orange"}, + "2d289e44f91f9280f9bb78a78c7e46dfb2b38e59e002097a5e3f4db8053b0b70": {Code: "TIBET-BYC-XCH", Name: "TibetSwap LP BYC-XCH"}, + "2d38e7c2d98d3f5fb9cc404a8aad52dab8dc6b066ad190f1c61857ff03d17512": {Code: "NIO25", Name: "NioStake25"}, + "2e28b8334c17ce291a29159be092dd7723d59534eab41a0a0b974e6db1183e5a": {Code: "SPRIGF", Name: "Spriggan Founder"}, + "2e5181759cfee4399530b66d524d9b20a75415d0e1b6f056c2cebd7ec46cae34": {Code: "TIBET-TTT2-XCH", Name: "TibetSwap LP TTT2-XCH"}, + "2ed236fe3fa979eaad87d89d10f9156539526f8b6c60d6f341f9de5b1f7c19f5": {Code: "SMOON", Name: "Soon to moon"}, + "2ed7adfeeb594cd115ab0d50d25b06c7ff5aeefded58af336aa910a63846e714": {Code: "TIBET-INCL1-XCH", Name: "TibetSwap LP INCL1-XCH"}, + "2f3a28ba03734b65797df5d5ff0e253606e7fea2e2ee341c39e9a405bce68aa8": {Code: "GOT", Name: "GOT Token"}, + "2f68cb8319eb0b9846bf13a288275f999d3159f434ad7da01c3e17042884b747": {Code: "TIBET-B$b-XCH", Name: "TibetSwap LP B$b-XCH"}, + "2fba9ce94b1b27c94a003d782eafde654ca991651ef97acac5c956777dff54d7": {Code: "TIBET-INGDP-XCH", Name: "TibetSwap LP INGDP-XCH"}, + "3057c3eca4087abbc15d75b29b56e523a9f85cf4476802e137c645cf7d5d9f5f": {Code: "WESO", Name: "Weso"}, + "30abf6a6b8faf714201fb6a7167463b429e4a795308668976f70e94697f87619": {Code: "FDAO", Name: "Four DAO"}, + "3121da00096a0a9a5e6cd2b4b4e9bf0d5dcf9d97333ca9814b281b2c8189b9b3": {Code: "TIBET-MARS\U0001FA90-XCH", Name: "TibetSwap LP MARS\U0001FA90-XCH"}, + "312d58cf56cb2ae8d106ec8ca2876c5cb50d19958db4838ba858e1752d5e2885": {Code: "SLOW", Name: "\U0001F629"}, + "318424dd2529fe52c52cad579685aa35782d459f2aefe2587d8c41a1ade85116": {Code: "TIBET-ALTOOL-XCH", Name: "TibetSwap LP ALTOOL-XCH"}, + "319ad6d73f87f62f446765b03c55af860e2c3dcac9105a78532038f4056a9160": {Code: "BEARD", Name: "YELLOWBEARD"}, + "31a5a5f1947979ebe64b5c742e368c0b79c442d3d331db6abad97986ab86200a": {Code: "TIBET-$DUMB-XCH", Name: "TibetSwap LP $DUMB-XCH"}, + "31ffd54c5b38bb33352dc0be0ebf9cf4f29a6329156dd4811d6f4254f85c8200": {Code: "wEURC.b", Name: "Base warp.green EURC"}, + "320b869bc8d293cca8784187312da1a61cf43b9cf0724b47d8e027dcca1dd501": {Code: "DEGEN", Name: "ChiFi Degens Redemption Token"}, + "32443416798c5243d23f184cbc5128cf74c1495712a530442a6847ed2134d42b": {Code: "TIBET-TIBET-USA-XCH-XCH", Name: "TibetSwap LP TIBET-USA-XCH-XCH"}, + "326d17b08162c32c5042496b00e7cdb760d84cad53a5aafd47acb28ab507158d": {Code: "TIBET-$WLBF42-XCH", Name: "TibetSwap LP $WLBF42-XCH"}, + "326f4f1a13465a65c6f5399dfaaa82be83446b3fafb25db54a99b8b80e55d19d": {Code: "TIBET-$MMM-XCH", Name: "TibetSwap LP $MMM-XCH"}, + "32f08162c1cd03cb8b4ffae586bc13303dac01d495ff51f63a079af3acfb4d3c": {Code: "TIBET-SLOW-XCH", Name: "TibetSwap LP SLOW-XCH"}, + "32f9989569a6d457c5d2e8c72957709c4e1d7adb186f18b0963c93a9eac5c402": {Code: "RUN", Name: "Run NFT 2.0"}, + "33228fd8debd2e1383a3ad316e65c97d46bead0e2bedd7274695686de7799abd": {Code: "GRNST", Name: "Granite Stone"}, + "339e9512bb050ecc2e0548cbe720692981a9132902b667588c68b8b5f00f0107": {Code: "USA", Name: "USA"}, + "33b24e48d33ff65bdf5eb4e90780cb5510e9c132e57636d31a2b75f44255ac2e": {Code: "TIBET-GWT-XCH", Name: "TibetSwap LP GWT-XCH"}, + "3421ebd1b4bc6617973f165744b49c956ab7a58782062744cc2e451bea894fe5": {Code: "XFRUT", Name: "XFRUIT"}, + "3490208d3c2048b3f6efbc46cf65de7732ed5a1dd8dd5fcfe02a829a61273230": {Code: "MANT", Name: "MAN Fan Token"}, + "3495e78ad2a7b18a518192cf324dd135fef7c9e4e10a077d37c722a352482357": {Code: "TIBET-TIBET-\U0001FA84\u26A1\uFE0F-XCH-XCH", Name: "TibetSwap LP TIBET-\U0001FA84\u26A1\uFE0F-XCH-XCH"}, + "34b14863f51635a20b0f4ef2b1d85280e11552498f79bd74facdca1865a58935": {Code: "\U0001F4A8", Name: "AirCoin"}, + "34f64cf041a182635d467ecb24348993317fae4371ecadba35397431308c6254": {Code: "FTK", Name: "Find The Key"}, + "3555671ab090827d983a61fe0b448a66916d122528cb6e5220dba2a115e178b4": {Code: "RAS", Name: "Rumble Ape Squad"}, + "358bfe761cc7a5d4810d74b48cc9509e3bea58ea01ca7631670fd794183e0446": {Code: "TIBET-THW-XCH", Name: "TibetSwap LP THW-XCH"}, + "35abeeabdc079ed3ecb8eb8ac3c424f593a047078635c18b96b97576d3ca963b": {Code: "wLOVE", Name: "Love"}, + "35cd47116c1ec3371cca2f3e3291157d474d72476df04258da1f7284d2330aca": {Code: "SHROOM", Name: "Secure High Return Officially Optimized Money"}, + "35dc1493baac0efeda494cb8599c3815ab0537eb7743b81425dcde8e3bde199e": {Code: "HP", Name: "HippopotamusArt"}, + "36029c0e2f17c2021c5f078a85917bbb10a006cd9431da74d26344723fc3679e": {Code: "TWV", Name: "TWV"}, + "36705e41d5ee1962c1407259a2cd8563dace4b9d7b42d43004955ef0b2e150e7": {Code: "\U0001F41A", Name: "Shells"}, + "367a314417bd8f938dbf3203219b31b934ddc33a8edb4b06d7685709750b2659": {Code: "TIBET-GEN-XCH", Name: "TibetSwap LP GEN-XCH"}, + "367c6b0d8df5d86e5a06da7e57dc1f339a684516435c6bc86f69af1ec1adddb6": {Code: "XPIG", Name: "XPIG"}, + "36dcbca61c10913b454187cec3b98023562f225617af4d4e32c09e946ebe2a44": {Code: "FLXR", Name: "Felix Red"}, + "36e3d311420e92ecda11c711395b9a2f78d0168a735fefc59617c95cc9165836": {Code: "TIBET-ARA-XCH", Name: "TibetSwap LP ARA-XCH"}, + "36e61f3bac9b6240ce12164acec40b7321147eb780d5c9db420def9a67713d20": {Code: "TIBET-CMA-XCH", Name: "TibetSwap LP CMA-XCH"}, + "370b11ffa9ed2934fbc4137be95053d4a97b7b65e850af3edc0b58f088a0059c": {Code: "COOKIES", Name: "\U0001F36A"}, + "37266aaf38765f142a981bf18bbe8577b3e0c1659445a6aa739cce497482321c": {Code: "$HOLES", Name: "HOLE"}, + "37b231bbdc0002a4fbbb65de0007a9cf1645a292888711968a8abb9a3e40596e": {Code: "G4M", Name: "go4me"}, + "37da66520b0076c979216ada11e8394955c65dba0f7b64f9f1aa371e026dadc7": {Code: "FOME", Name: "FractionalOwner.me"}, + "3832f78f7f98ef11fce6af1047691facd2cd1d4c878df5d3e76d5b506859c6ae": {Code: "TIBET-MINT-XCH", Name: "TibetSwap LP MINT-XCH"}, + "383fc8ce5ca6d5ee68e74007da856fab938f76cd866c9f879b9381be461a6d31": {Code: "JULIET", Name: "Juliet"}, + "3857a99b531acdebca632c48111d811bafde58b093658efbf1a9d8e09521926d": {Code: "HELP", Name: "Help Ukraine NFT 2.0"}, + "3877f1b913ec5d8c3a176049b25f57c7113f7d4a449d5337a9c2c7cb5844eaaf": {Code: "TIBET-TIBET-TIBET-USA-XCH-XCH-XCH", Name: "TibetSwap LP TIBET-TIBET-USA-XCH-XCH-XCH"}, + "38a5071bab76410b155d073c9780fe38555d24ddec1a2b64e8f3392864e11978": {Code: "WOJAK", Name: "WOJAK"}, + "38e3d82b22b52258232d77a4d9a62a526200cb2c721d31e8738c93a0abec03c3": {Code: "CHNN", Name: "Chianon"}, + "39527b8859d3acfb2dcad3956106207d880c2020c3deb0960ab88c337f6dc14a": {Code: "NUTS", Name: "Nuts"}, + "39c5d2bc5f4452e6e9f50d903b82d244dafbbc5fda447cebbedb860dfdc6a3cb": {Code: "IGNI", Name: "Ignium"}, + "39e6c0bbf4118372c6705d65346f33cd920a0c6d362f776681e1fd530d7dc9be": {Code: "TIBET-\u2728\u2764\uFE0F\u200D\U0001F525\U0001F9D9\u200D\u2642\uFE0F-XCH", Name: "TibetSwap LP \u2728\u2764\uFE0F\u200D\U0001F525\U0001F9D9\u200D\u2642\uFE0F-XCH"}, + "3a80cd9e78201cc103647b4cae86616b7b85908a1db2839c8d543531fc660b7d": {Code: "BBUX", Name: "Billybux"}, + "3a90944e478d6bcc3d45bb5ce8b9f1edbdf1d2fb6f1106466f650a5bc22632f4": {Code: "TIBET-CH21-XCH", Name: "TibetSwap LP CH21-XCH"}, + "3a911861c478619fb14735606282f9fb4128ae20e2a767ae351b0d5531a26b2f": {Code: "TIBET-What's-XCH", Name: "TibetSwap LP What's-XCH"}, + "3aa444277876543e8e643697010d25341b256c400ed7bbd1cafe58cabd2b0038": {Code: "BEER", Name: "BEER coin"}, + "3b19b64418682ad60c6278d85f8076a108c9fb4ca385d401c6d41ba2bfa9612e": {Code: "JOCK", Name: "JOCK"}, + "3b27617d821428052f00199e7a06b1ab666d58a59901540455fe71d36451a00c": {Code: "JAR", Name: "Hairy Bear Honey Jars"}, + "3b2861c9f22ef4e40bf7a4fea24b2d92b49e28c26e262867c250538461a6d4f9": {Code: "TIBET-DXTS-XCH", Name: "TibetSwap LP DXTS-XCH"}, + "3b66e769792c541f742b54fe9d66127a5bb0db8d5dc391421bb3fee522565777": {Code: "HDSCR", Name: "Hide Scraps"}, + "3c129dc1a2f21e649717ec9986643162dcef2b83f4b43185d2923a90344e267b": {Code: "TIBET-XHAY-XCH", Name: "TibetSwap LP XHAY-XCH"}, + "3c6da5ae38438db6299426402dc23f9436ee085270ed76f8fc7f9e6b4136dca2": {Code: "\U0001F408", Name: "PUSSY"}, + "3c75c138fe2fc94c0bae3467e7ae5a457758cb058925424ec2eb327f6b231a18": {Code: "ALWEAP", Name: "Abandoned Land - Weapon"}, + "3c9ff6027993982cdc2787e5c4ea59ff327f53fef0502b33e4185b7bc7179c4c": {Code: "ZF", Name: "ZESTYFLUFF"}, + "3d28ba2b0f8f1c6bd71db0fbe3bfcb8177eb56d9003f3b14f2060f40aec42470": {Code: "TIBET-DUMB-XCH", Name: "TibetSwap LP DUMB-XCH"}, + "3d597f5dd9d9249be84b515dcb22442a773afa2aa210c27624ce3bc612e9c3d3": {Code: "MOTO", Name: "$moto"}, + "3de44f01c3ae41d5917396edfd4c1ad9a3ed064745a9d8295581cd696c254cad": {Code: "TIBET-\U0001F420-XCH", Name: "TibetSwap LP \U0001F420-XCH"}, + "3df660e538e935331a39a1c986ebda7111af06647ace0e9c345daba2b92ddf05": {Code: "TIBET-NINJA-XCH", Name: "TibetSwap LP NINJA-XCH"}, + "3edb0b2199b913135f722eb5b9d590a2a9ad86885380830c0a84d53dea034804": {Code: "MRBL", Name: "MarbleCoin"}, + "3ee6ab1c8fcf69c8a0c68a03717d40bbc012e2b898edaa733ea8e0bf1146e9d5": {Code: "HDT", Name: "Hema DAO"}, + "3f30f8b50ae20a48e344ebb18e6cbb72bbed394f581fa9b1f973da5ae538383f": {Code: "TIBET-CNS-XCH", Name: "TibetSwap LP CNS-XCH"}, + "3f346cc3122ff1634b812f43bd122fcf0d532496df16f1edacc1d0b338a55c88": {Code: "GOP", Name: "OMGopo"}, + "3f8fc05ff6fd799fc49d7616a7d8440435796d95b8f5a80d83a9935130c43e8c": {Code: "SWLT", Name: "SeasonsWhiteListToken"}, + "3ffa2b25c2b646a0ffc4eb345402ae158eb78c7e1d855f76a6538a33bc61b7ef": {Code: "TIBET-KUT-XCH", Name: "TibetSwap LP KUT-XCH"}, + "407002a9b1c3917b1d3d0cc79f622c22a9b91fa56990cbee56af6384fca2fd46": {Code: "CECT", Name: "Counter-Economics Capital Token"}, + "4145063308ede4f10e8e67f160a15c9c92994b93e80399827e51ee1d761ca927": {Code: "SILVER", Name: "Silver Ore"}, + "4163cb4b370da9001750b00395702914806dc4c43c3bf9fdbd412fdeef42615d": {Code: "TIBET-ASON-XCH", Name: "TibetSwap LP ASON-XCH"}, + "41d04f9a89f12ae3b12f26f5019a6c26ed78f50f79e48e22a5063ce561bc052e": {Code: "TIBET-STNK-XCH", Name: "TibetSwap LP STNK-XCH"}, + "425881d0271ff45f2b905f21d007f543d8f25ef4ff2c3e81f22a7acb73fcdbc5": {Code: "CATMSZ", Name: "CATMOS ZERO"}, + "4275bd94b6913028adc4b183288a873c7cabf65c163e6db7d5686e4ccdc0bb35": {Code: "XSEED", Name: "XSEED"}, + "429793b1d0e25c115da34bc7a23809d4b4981e8b99d2c2282b186e743be43844": {Code: "CMA", Name: "Chia Meme Academy"}, + "42c6e84ae069d15113f90fe9617e539a891adbfdbc0639495afe6686036dd34a": {Code: "CHIEF", Name: "CHIEF"}, + "42c78cd7e88dad70795d0e7302b915f9c78193b2aba073fd6fe4623d4aa0603b": {Code: "TIBET-\U0001F422-XCH", Name: "TibetSwap LP \U0001F422-XCH"}, + "42d298d75fb6400c7ea18f9b6a5a006580c3f43e47a625583cec4aafb9148a10": {Code: "$DUMB", Name: "$DUMB"}, + "440879faa061603be94b340f4041aac35081e5f4d7a7671c9b5fd0b1d914bd7d": {Code: "TIBET-G$b-XCH", Name: "TibetSwap LP G$b-XCH"}, + "4411b68bdb2d1e13a50f66b69ccbc4c4df0c8c074d092da610b5f136875eb470": {Code: "METAL", Name: "OfferWars METAL"}, + "443005e9430528b29586bc0164f2b4159b588c22f99bb029abe7e685de985035": {Code: "POR5", Name: "Poor Man's Chia Farm 500"}, + "44479960c96118f7f83d247822a0c9916198ff7256130a7c451cb6549daf9820": {Code: "WEB", Name: "Web3.0"}, + "444bddca5815027bf4118da0501a619aab689bb707b895f3e576e94f2c55195c": {Code: "PUMP", Name: "PUMP"}, + "446f5c3532929f71fa82f1c65f7e93170dcfbf8d59baf82a81b6f8e8f85e8a5c": {Code: "ALGOLD", Name: "Abandoned Land - Gold"}, + "4497669e1fc1e97ee553ab4b239a126922ebabfc630061fbb77677dd5da62d46": {Code: "EDSC", Name: "Elderscale"}, + "44a7a9e4666c82465e3429a4600748848e122e55a3de0fbb0bb33caf42b4188a": {Code: "BISSE", Name: "Erikoiskalja"}, + "44bdda948cb314f0e5c80c60cff39e96450747c51fe8364b195b892ed3fd0712": {Code: "TIBET-\U0001F346-XCH", Name: "TibetSwap LP \U0001F346-XCH"}, + "44c92ff85fdfbb7c1349265108b1e5899938b84941c94a4be57405d3b9bee9a6": {Code: "TIBET-YOINK-XCH", Name: "TibetSwap LP YOINK-XCH"}, + "450d102874843616d9016e534397f29d7f91a57a50d27e96cee06fb72904917c": {Code: "TIBET-CGLC-XCH", Name: "TibetSwap LP CGLC-XCH"}, + "450d89e992449b10ff510dde612488046ec9560e4b5559a48d343a6b75a4b427": {Code: "STAKE", Name: "STAKE 2.0"}, + "4512de10cbe18be75694aa97d8a47e6e52f3a7421e6888741ec4a2740e9ade19": {Code: "TIBET-ALWORK-XCH", Name: "TibetSwap LP ALWORK-XCH"}, + "456308b3e1ff54c98327742dc886f0060ec55e1169f469598d46b23fb952a4c8": {Code: "ICE", Name: "ICE"}, + "46161485996570b82faebe7504a369c23fc98cbe748110ff51275e3710d16835": {Code: "TIBET-YEBIGA-XCH", Name: "TibetSwap LP YEBIGA-XCH"}, + "46664f9b76f388065d6c523d09797d4568b6c4cb91f3685560a795af4dfaf87c": {Code: "HOOVEC", Name: "HOOVE Coin classic"}, + "46d05ace386e1deb1bbf1886a26e65e55bdd0c9fb508b2fd5bf98c0fa17c446b": {Code: "TIBET-COW-XCH", Name: "TibetSwap LP COW-XCH"}, + "46ef568a4ec5f5656adb630bb90ceabbd73557fbf654fcd1144d70f0642287aa": {Code: "TIBET-\U0001F48E-XCH", Name: "TibetSwap LP \U0001F48E-XCH"}, + "46efa82f52027a741714066db2293bffdc88dae793658432d1604c644d334379": {Code: "TIBET-GOLD-XCH", Name: "TibetSwap LP GOLD-XCH"}, + "46fcff7c26a9a8ebb3fd201c8c473e84de0628c50b1277ac9fddb79fc92df4ef": {Code: "TIBET-DEVIL-XCH", Name: "TibetSwap LP DEVIL-XCH"}, + "47254e1bd88d6f08a0d93d571998c058784eac231606f6cd87d150a10dd5d1fb": {Code: "RUG", Name: "It's a rug"}, + "4752c19d391044af2ec88f6997ee60539ec2ac28ae044d85c6c3121ef125c475": {Code: "TIBET-DXT-XCH", Name: "TibetSwap LP DXT-XCH"}, + "477285dc4f1f2f5ec42ed63ed1ce127cc56646614540fca86b8481f5b5084072": {Code: "CHET", Name: "CHE Fan Token"}, + "478b66e6f3c033a12ad43e354329a2f54c187503a4025abdb3b3824f92ca0819": {Code: "TRZ", Name: "Torogoz"}, + "478e7d8f8fe01b7f671cd4d8149cac1d502e598866e22b8c39ea6e7e40859e58": {Code: "B$b", Name: "Bran$baby"}, + "4793260e9118425a32db6aabc8c8e11b1eeb536ca41ead21a85fa84403354b9a": {Code: "TIBET-WARP-XCH", Name: "TibetSwap LP WARP-XCH"}, + "480f52ae9bceb6add706ffaaa48d2c541233fa71a342909b2f902baa69b936d2": {Code: "\U0001F4AF", Name: "\U0001F4AF"}, + "482b49902d310c53065c3531d398d41808f1390590d566815d67040f6a32d124": {Code: "CTK", Name: "Catkchi"}, + "482c11cbb96f98ca61423ad17827747a10025a265358f1ff9f7c5bbd06f5426b": {Code: "DPZT", Name: "Depozit Token"}, + "48752d01d52f5f49f5709f92a2630b83ef766d43175be89c198ffc7d3b6018fe": {Code: "TIBET-\U0001F525-XCH", Name: "TibetSwap LP \U0001F525-XCH"}, + "487924868a90bb2b149dd61d9d3aefa613f046bfe6d5b0c1b282eb6066a4851a": {Code: "SPELL", Name: "Spell"}, + "487d889d0000cb6d7c6c5caf2c30775ef409d3a5b4f5a544e8a6de1c511fae03": {Code: "QTLO", Name: "Quatloo"}, + "48d3f1627c3576a73af4d969b235300af741ac9fbc4e172c8bea9fdf94d3390e": {Code: "WAT", Name: "WATxCH"}, + "494ce7c4ca5781246bd3f4331801703c4476e2828ba2e164242b080b798ce8c6": {Code: "TIBET-CNUT-XCH", Name: "TibetSwap LP CNUT-XCH"}, + "4965fb881379c5d30694e4a6df586295fbee66cb04af352f34c7ab2c19300412": {Code: "DBL", Name: "Did BallDog learn to create CAT?"}, + "497f43630802f7f3482c36221d114d6f13dd660a228f2b4ed93f9c881983aed4": {Code: "TIBET-XIP-XCH", Name: "TibetSwap LP XIP-XCH"}, + "49e041a0d3735343e3cfa47a885f65812862695bcd0a304c7b59c701306af103": {Code: "TIBET-DACH-XCH", Name: "TibetSwap LP DACH-XCH"}, + "49f84392fa4dd3b5b9ab4dda821550c3a06ad0681ec074636a44cf9821a63e71": {Code: "ANGEL", Name: "ANGEL"}, + "49fd76384a4fced922d99d28a646dd0439ab753ad15fcfea8ef50213804112e6": {Code: "SCAC", Name: "Solar CAT"}, + "4a168910b533e6bb9ddf82a776f8d6248308abd3d56b6f4423a3e1de88f466e7": {Code: "ECO.181.2022", Name: "Agricultural Reforestation Project 2022"}, + "4ac791e72592ec2ed05a2ae871efb30e277ca10ce63ce2f07ef94c7a413095e7": {Code: "L33T", Name: "L33T Coin"}, + "4af2d229d02d233ac8e53c66b9808593774694344729e732f0370c04064707a1": {Code: "TIBET-PUNK-XCH", Name: "TibetSwap LP PUNK-XCH"}, + "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a": {Code: "()", Name: "()"}, + "4c4380af7d15c896d9e6266f322ac494c398803032eef56f2ab65877956d007f": {Code: "NAME", Name: "Namesdao NAME"}, + "4c9c99becbb2b4de99e2faf7e1d79789f0b8d91e7986b18f5de73b5957a0c65b": {Code: "XEGG", Name: "XEGG"}, + "4cb15a8ecc85068fb1f98c09a5e489d1ad61b2af79690ce00f9fc4803c8b597f": {Code: "wmilliETH", Name: "Ethereum warp.green milliETH"}, + "4cc4c6d12d75e1e5eb403527336587782a0387ce47142bdb3802e06b872d5b24": {Code: "XSTONE", Name: "XSTONE"}, + "4d114914b01b1411897fe3ccd4a1c2e69377c3ace7fd0c57095100bfbb729e33": {Code: "TIBET-DPLY5-XCH", Name: "TibetSwap LP DPLY5-XCH"}, + "4d25c455307adbd056b128dc60cf6b36a2c5e437837f018999b9a317f79662c8": {Code: "TIBET-MANA-XCH", Name: "TibetSwap LP MANA-XCH"}, + "4d38ad9998bd93d3a424a788884f2386fbe076b9edcd36580ae02844388d23af": {Code: "ZOMBIE", Name: "ZOMBIE"}, + "4e241c644c1e01918599d6cd93665fa3c6fc356202f536d3bb85cb172502fb2a": {Code: "FRSTB", Name: "Frostbark"}, + "4e34fb4247865a1a644c992a51340aa32f397f094cf1abd9663803ed4e172dfc": {Code: "FLXG", Name: "Felix Gold"}, + "4e47b5051e02e20bcb42f4fdd0c0f036462df06c54c4fd539a35f411128facfc": {Code: "BBW", Name: "Body Of Bram Wafer"}, + "4ea9037096cb1eb0fbed37bc301cdfc80c41e25d787b2afdec078392ca980988": {Code: "TIBET-wUSDC-XCH", Name: "TibetSwap LP wUSDC-XCH"}, + "4ec438355a6b7506cb85e7d330e758d7b30d0bcf44021517f094ed878f74afe0": {Code: "VAL", Name: "Valentine"}, + "4f50e6f88fd5765a64ed49b20bf0e81a37b1550ac92c19fbe84e41562f23e60a": {Code: "XST", Name: "Xtock Token"}, + "4f80f2c16145208330b16ada79f1ef09eea72b456d75fc39364271626acb6272": {Code: "\U0001F9AD", Name: "Seals"}, + "4f90e7099dbdfbbaf632c31466b2a1b77dccb134f17e7c3f3556567f501f74a4": {Code: "\U0001F9A0", Name: "Microbes"}, + "509deafe3cd8bbfbb9ccce1d930e3d7b57b40c964fa33379b18d628175eb7a8f": {Code: "CH21", Name: "Chia Holiday 2021"}, + "50ff267b6710475da8a4744879401cb111b20242803a8a6c69a0fed70c2feb50": {Code: "OMEGA", Name: "Omega Molecule"}, + "51ef89a35fa316c0f2747696b2483c9203da3155b603ea3f194640ada374907f": {Code: "DCH", Name: "farmingDCH"}, + "5264772cacc3258fee98f60963cea56c46bd4dfc83c94cf75137bcb5e86f895f": {Code: "TIBET-GANG-XCH", Name: "TibetSwap LP GANG-XCH"}, + "5283bafe633f7701c662fa216eee1c257725eb122dcf8e661ad62e2bee7609d9": {Code: "\U0001F422", Name: "Turtles"}, + "52decef5bfacacb99a5f4f4df8872e83204dfa8f8e446520bbdafbbdbf171f84": {Code: "TIBET-BCAP-XCH", Name: "TibetSwap LP BCAP-XCH"}, + "532775c90d1b454232b51c5caf35601371cb4e8df9042133d603e01f05bb25aa": {Code: "TIBET-DIG-XCH", Name: "TibetSwap LP DIG-XCH"}, + "5364c7e938cb8ed78630008ee844d9c6688dd0729fb7c72e138b277c3af19a06": {Code: "TIBET-HonK-XCH", Name: "TibetSwap LP HonK-XCH"}, + "5391cc933ee1b2d56483b90f4db82f78ef26d7b8f1aeedb14af11eb28e919d1b": {Code: "GCC", Name: "Gaerax Custom Characters"}, + "53bdf5c043029093ca0b8a3653987e215e9518e18b73606c69ee9399dfecec35": {Code: "WIZ", Name: "Wizard"}, + "53dae588ec454844bf6aa48045baa5629e5d5b87f5035119f03cad33b4477bba": {Code: "SPACES", Name: "\U0001F680"}, + "540c7e9bc1dd360d2f06f2269299bbbd3ba7692de766423cdff9b870edc44a05": {Code: "TIBET-BDOG-XCH", Name: "TibetSwap LP BDOG-XCH"}, + "5442e655253cf91adf96bda0a60f8b956c73234ab643275434112365cc261afb": {Code: "TIBET-DPLY3-XCH", Name: "TibetSwap LP DPLY3-XCH"}, + "545d961ea53b05dae7e3779ae06dc48aac050a8f80b8e9b1eb4629cb9d425f8e": {Code: "TIBET-ALORE-XCH", Name: "TibetSwap LP ALORE-XCH"}, + "54c151d078f56fe5c6a5e36d8765e43440b3dfba0054d31fc18a0bbca16ae313": {Code: "BTCM1", Name: "Bitcoin Merch Batch 1 Coin"}, + "54f4bf70c2219965982b9bcea4184285415353e9844167625ce08161e07f3068": {Code: "TIBET-MPNY-XCH", Name: "TibetSwap LP MPNY-XCH"}, + "55a944c8cdf92d08df244af84fd334e7d2730264b88a406ce6f03f977e6d76b6": {Code: "Pep3D", Name: "PEPE NFT 3D claim"}, + "563c883b801c4e6c736994d09ec69c94c9c6482612b4655b76097282b8d2fae6": {Code: "WARP", Name: "Warp"}, + "566530baf6b7d44a3990841595c4370f2dae1d477b00fb8b0492ff0903b279f2": {Code: "TIBET-BIKER-XCH", Name: "TibetSwap LP BIKER-XCH"}, + "56de7231a70a27e3137b4362214d0037d1817ff075be604be63238382b1935d1": {Code: "TIBET-MOJO-XCH", Name: "TibetSwap LP MOJO-XCH"}, + "57138565da3cd7346097bb27ab05e4da3fd588188a6d4ae255fbb84262aae56d": {Code: "CGT", Name: "Collector's Guild Token"}, + "573ff667253b16221e32d371d49c64ee6358a7fa9ca3e4330ee7929f562a75dc": {Code: "TIBET-DPLY6-XCH", Name: "TibetSwap LP DPLY6-XCH"}, + "579cf1f31744b34b18cf4b917e74c890871c9a6aac4308ed269254610f46c2f8": {Code: "TIBET-\U0001F355-XCH", Name: "TibetSwap LP \U0001F355-XCH"}, + "57b6e11cc866845b9ded1d25fbd3c19cd934997b506635c4fe82087e111e791c": {Code: "TIBET-LSD-XCH", Name: "TibetSwap LP LSD-XCH"}, + "582e5e9684210d2ce64e6dec63d526e8f8df0492a8eef591b9afc6bbf96971ff": {Code: "TL", Name: "Timelord"}, + "58895ce446f35c806bd15b2b32698f1172fc24783cb07cccdcd4ca2f83320ba3": {Code: "TIBET-CAPES-XCH", Name: "TibetSwap LP CAPES-XCH"}, + "588b9f46b82c98546b466549c77ac68f8e5855a03c0441f64c6f1db09cdf24a1": {Code: "TIBET-OUTR-XCH", Name: "TibetSwap LP OUTR-XCH"}, + "58d525df69cd72ec149266eae04ef7f57a6a9cce64bd42742242e31c8d6ea1c5": {Code: "DATG", Name: "DAT GOLD"}, + "593c68c3f0c2061ea384ff30d9f3bef15039c6d5fbd46b555c73b2e9b490fca5": {Code: "ALTOOL", Name: "Abandoned Land - Tool"}, + "59441bb99a2765cca75fcf435a017cfe942d3131f7f68abd344ed3d8b273d0e2": {Code: "TIBET-MWIF-XCH", Name: "TibetSwap LP MWIF-XCH"}, + "595c14a3748291de5d9be9b21d18311ba597432932b0ff28db3e8e378899b152": {Code: "TIBET-TIBET-TIBET-TIBET-USA-XCH-XCH-XCH-XCH", Name: "TibetSwap LP TIBET-TIBET-TIBET-USA-XCH-XCH-XCH-XCH"}, + "59e109b217edc2c21919d83937d68bb7f157e73effdb7405d4cc5c0b7c08f919": {Code: "TIBET-ICE-XCH", Name: "TibetSwap LP ICE-XCH"}, + "5a1ddb8379d9547e68a71e870103f9a5cfb049dbf83eec309e4df9e166a01b98": {Code: "SQRT1", Name: "SquirrelRat NFT"}, + "5a23534befab31f90f63034dea2f5ea8263d94f8294de423bf11f2789ea4e5da": {Code: "TIBET-MAX-XCH", Name: "TibetSwap LP MAX-XCH"}, + "5a4743ac88b052f387c1b5f969938c0d122f248616c769bb075dfecbe88ea471": {Code: "SDT", Name: "Stardust"}, + "5a7c011e1bea52a87fc72952dfafad0d82fcde0448a2a4fa7fa0bd53d9a4e1c9": {Code: "YOTT", Name: "Year of the Tiger"}, + "5a91c044c2e3f992530a2334c9c8bc3448db304e17546a8ac9bcba01af062f45": {Code: "KPC", Name: "Kiloparsec"}, + "5af8db0b15e0de99ad1eff02486bb1998602053c56dfb22dc04e0f5e17ccec8d": {Code: "ECO.29.2021", Name: "Smallholder Reforestation Project 2021"}, + "5b0b3a7022db6efc4a104d44ca7847845bbaac30d810e60733c792b61b9706b7": {Code: "PSGT", Name: "PSG Fan Token"}, + "5b171a315a20e902b15e258617a66d320e5a0a7334304eb6a2cfee6571ff1453": {Code: "BET", Name: "Bet"}, + "5b8d2b7e4758169f492c9e5f07d89703681c5ede8bd2283cf618bb96b53ae244": {Code: "RBLST", Name: "Rubble Stone"}, + "5ba3334d0fdf14cf0e64b49e7d0b19b34e049a083bf40f84a70c8e39cddd867d": {Code: "XCS", Name: "Cesium"}, + "5bbd55812a557f2525683dc960ce277cecfbb5abffed8be8babcabc43e4da19b": {Code: "CELDS", Name: "Celestial Dust"}, + "5bc69ae002eb3cf9a5acbd21af320d49292962755446595417c9d63922fbacab": {Code: "TIBET-TWV-XCH", Name: "TibetSwap LP TWV-XCH"}, + "5c8eab03c9f7e58881539363c15afedbfe884e3b3d57fa96d2607780d7b0b419": {Code: "XCHIN", Name: "XCHICKEN"}, + "5cc3d3797aea0f6f80fb1b095c4a56e042d8846c30ccced06837539c54f3aa23": {Code: "TIBET-AIG-XCH", Name: "TibetSwap LP AIG-XCH"}, + "5d3a24241258965791e3e4fed17dd0d119940dbe3b3310f48a4a041c9801efd1": {Code: "TIBET-TIBET-HOA-XCH-XCH", Name: "TibetSwap LP TIBET-HOA-XCH-XCH"}, + "5de2b461a7257e1451bf1c331cbfc55945c39e02b2e540f05e596963c410003d": {Code: "TTT2", Name: "Tiny Tardigrade Token"}, + "5e8b3e84f235ee9173c6d651441b1674313c2792e7708473aae31edee318f87b": {Code: "TIBET-\U0001F4AF-XCH", Name: "TibetSwap LP \U0001F4AF-XCH"}, + "5ea2c18c6714b2d55bc21c6148975c0bd9939de17b5068633c5e7f5446df5eaa": {Code: "THORN", Name: "Thornwood"}, + "5ec1527c4f8e1f10bd4b3dda299b6b43098921777ae87fa1f5a390a79f0b6791": {Code: "TIBET-OMG-XCH", Name: "TibetSwap LP OMG-XCH"}, + "5fd27391e6385e5d4bdc5b7b2df67b2e8698e337d49b94302a3551deda565e58": {Code: "POTT", Name: "Proof Of Treasure Coin"}, + "607fb74ee321225d42da8896c95764ff37103300fee3c0607edc5efa5d5245ba": {Code: "TIBET-NGDPLT-XCH", Name: "TibetSwap LP NGDPLT-XCH"}, + "609f36bfa050bd1d387a8c5acb236bb65563c991533ab29c93b5d4cbfa17be44": {Code: "TIBET-KBX-XCH", Name: "TibetSwap LP KBX-XCH"}, + "6188396ebb38365be3f7aad1d5c0310d1b6ce66180d8847e5da096e2ce33ba1a": {Code: "GYATT", Name: "GYATT"}, + "61ab1f8ad006196c1166b92ef3f298812d5b9d32c1b78a80410b2e81673970c8": {Code: "\U0001F525", Name: "FireCoin"}, + "61e29ca2626f2b2d927398520b58f390be1e88946add8ee6f27c567cff1b9a3f": {Code: "TIBET-R34-XCH", Name: "TibetSwap LP R34-XCH"}, + "623c352de013e561656678f57058f7728aab631aa3aaab685fbe7918cea9b001": {Code: "TIBET-IYKYK-XCH", Name: "TibetSwap LP IYKYK-XCH"}, + "623ee5ec5a082747154cfe60a4bffc3d0491472940c4a81d676e40f7c9d9a0dd": {Code: "TIBET-FBX-XCH", Name: "TibetSwap LP FBX-XCH"}, + "62ddc5ed7bb626baa27703c29b8523f9032292559463dade21d0f5265c8cdd37": {Code: "CKC", Name: "CAT King Cole"}, + "634f9f0de1a6c39a2189948b8e61b6852fbf774f73b0e36e143e841c49a0798c": {Code: "wUSDT", Name: "Ethereum warp.green USDT"}, + "636414896636bfe995c053b7a9db1832b26cad56d5f6a7a8d64e9d2b3d2f5da7": {Code: "TIBET-TIBET-CNUT-XCH-XCH", Name: "TibetSwap LP TIBET-CNUT-XCH-XCH"}, + "6431a89ee450e0844a3f2a243725a0707b9fbd3f0fef349f485d2de7ecaf3ea9": {Code: "LISA", Name: "The Lisa"}, + "64d1e7e794464053858763f2082eb595ed4416976bed72ca679a553839ff189c": {Code: "STDG", Name: "StellaCoin"}, + "650fad74542be77115ad1bcc3b3e59001eacb86fbfb01aa016319dc932748eae": {Code: "TIBET-BTC-XCH", Name: "TibetSwap LP BTC-XCH"}, + "652829318a1cecc6c640ccb0aba8947833a80ca2274708f90ab44b14b8d74e32": {Code: "BIKER", Name: "BIKER"}, + "6609ae6dd66a800dd7d4dfba54e1ed614362a08381dd2d60f95b779e32710f83": {Code: "XMETAL", Name: "XMETAL"}, + "660bb2ab6c885276fd6bfdd7c54c038b4cc08b324f20137635bd482fa7c414e2": {Code: "XCHINA", Name: "Chia in China"}, + "66bce98d19045fe686d133c55bc9731abed7a71a311161d79de017700fa20f0f": {Code: "TIBET-C2R-XCH", Name: "TibetSwap LP C2R-XCH"}, + "66fdffbccd18b2c91b9c2a442be203b6e732b71e225ac6ec3595bda753d780e5": {Code: "GLD", Name: "OfferWars GOLD"}, + "6733f1b8d4ff22e5634accb40c0981d8c3cc14506ab4a935d3980be0ec2f0d5c": {Code: "SILVER\U0001F4AB", Name: "Silver"}, + "6784015939685cacd6bc01b16309a1cfb33977be7811bb0545326a074d97f47e": {Code: "YEBIGA", Name: "YEBI GA"}, + "685201000b8b930f4ec2d2ef5ce8c7d52fe771b0e0e551cade823128b24f8441": {Code: "NGHTW", Name: "Nightwillow"}, + "6878179772274846a7d8298597250ed2b6386549365c27577a45fa1a8f996361": {Code: "DUMB", Name: "DUMB"}, + "688c28ca0bfdd917162ecba4abeb9a370115573412dcbc1d2e5a1cf93be4857f": {Code: "TIBET-DMT-XCH", Name: "TibetSwap LP DMT-XCH"}, + "68e2d5c01ef319196af1325b342dba68321ce54b50d252a058ceb103911de9a7": {Code: "TIBET-XCHIN-XCH", Name: "TibetSwap LP XCHIN-XCH"}, + "69035ab8661905af95760b6099267fc51b1e5f6b2ed5afd46dfafafb7f0bf651": {Code: "CBT", Name: "Chibear Token"}, + "69326954fe16117cd6250e929748b2a1ab916347598bc8180749279cfae21ddb": {Code: "$CHIA", Name: "VersaceFerrariVegasApartmentPatek9000Inu"}, + "69776ba48bada66d8be4a4dd97290e73197fe20f1cfb294429be5304bd3527ed": {Code: "BYTE", Name: "XCHDOG"}, + "699ed0a27560ce47130c906f3ce8ffaf9b7009637446d04c21220bef615622bc": {Code: "FAITH", Name: "Faith"}, + "6aa8a630612d4cba2ee806b98d68c29eaa6f2c74fb63465e9c84febfd3c79693": {Code: "HARDL", Name: "Hardened Leather"}, + "6ab843d28de9f679e1340aafdcfa3fb56d4b05a9bba64af98db1ebed52769ccd": {Code: "TIBET-CROAK-XCH", Name: "TibetSwap LP CROAK-XCH"}, + "6ae636f1eb6f00b72ff015f64bb86af4f46343697ce576173359b329c89f01a8": {Code: "PUNK", Name: "PUNK"}, + "6beb251f216d8e5a8e6fb0466d76f0c3085d94cd1e5d84aa5b778a9fa7406598": {Code: "CRX", Name: "ChiRex Token"}, + "6c0af0c08407cb16da6f7021b6e6c1ab49dddc641645a2c28d178e896ec5379e": {Code: "KINGBEAR", Name: "KINGBEAR"}, + "6c7b32f8f725ca4138ea1a6c3330184d3fd8a8c319946cedd4df18c848565284": {Code: "KUT", Name: "KUT Token"}, + "6d113dc5217699020c5e10aa138ec28fe3e15d894e9876b1647d9ad6bf6b1d6a": {Code: "TIBET-BYC-XCH", Name: "TibetSwap LP BYC-XCH"}, + "6d3d2c9709d8e89d4707fa4f5f3e69269bd0b825a22da4fa45ed517da95136e0": {Code: "LLAT", Name: "LLC Asset Token"}, + "6d6ba069de0ef0be7ba31c78643eabae871997aaed685adb2a3fdeb1954184c2": {Code: "$MMM", Name: "Mini Millionaire Marmot Coin"}, + "6d95dae356e32a71db5ddcb42224754a02524c615c5fc35f568c2af04774e589": {Code: "USDSC", Name: "Stably USDS Classic \u26A0\uFE0F"}, + "6daab7bb79093ef60f3b83dc3d9def25e91eae55916da8bf95268eda9ee871ea": {Code: "ALWOOD", Name: "Abandoned Land - Wood"}, + "6dd9ddbdc0f7ff9510177e49ff2407593faf5618eb4381feafd451a00965c6d1": {Code: "TIBET-BEATS-XCH", Name: "TibetSwap LP BEATS-XCH"}, + "6e1815ee33e943676ee437a42b7d239c0d0826902480e4c3781fee4b327e1b6b": {Code: "BSH", Name: "Brother Shiwan"}, + "6e55e6dde6bf3fd9e474f2c45cb63e30af64f74285b7ef0b92bbf3503aa14e42": {Code: "BTF", Name: "BTF v1"}, + "6f84749d908f4e0dd260a9b98c36bf88525759c288f5fff245e0d05ca8cfaac0": {Code: "TIBET-XSEED-XCH", Name: "TibetSwap LP XSEED-XCH"}, + "70010d83542594dd44314efbae75d82b3d9ae7d946921ed981a6cd08f0549e50": {Code: "\u2764\uFE0F", Name: "LOVE"}, + "7051e62d3dcd418cc89f4be15ed6e68c102640b90dbbe829419dc3139b9b73a5": {Code: "TIBET-MELT-XCH", Name: "TibetSwap LP MELT-XCH"}, + "70b29bc28622933d9bf50b2f7c1a88023358029ce459adf1dfc1e381a182f8df": {Code: "TIBET-CPC-XCH", Name: "TibetSwap LP CPC-XCH"}, + "70fa530ab1f8ed49a5cf93bfbe44bb842d4fbd837a84c1eee377bcfb8f438672": {Code: "TT22M", Name: "2022 Mojos"}, + "7108b478ac51f79b6ebf8ce40fa695e6eb6bef654a657d2694f1183deb78cc02": {Code: "GWT", Name: "Green Wallet Token"}, + "7119038b7d7fc1e5f042fc82d44363b12f3c460229d6e361290bbe1e78115e67": {Code: "TIBET-BUNCH-XCH", Name: "TibetSwap LP BUNCH-XCH"}, + "715108d993c0b296d7754a8361222f1ace66f115cd0a2b943550471365d1670b": {Code: "TIBET-MOG-XCH", Name: "TibetSwap LP MOG-XCH"}, + "71765f6ad4747e8fc7349411ff887c5d5abcc17a537f723fc3a296062b043996": {Code: "TIBET-MIO-XCH", Name: "TibetSwap LP MIO-XCH"}, + "71bacd4db2e7f46cc54a0a52c7584b3e376a1c09e1323a7a420e1713dbdd0743": {Code: "TIBET-GRAPE-XCH", Name: "TibetSwap LP GRAPE-XCH"}, + "721be932b8a7313c3a0d0ee0df3820cef124cdf0154ef0cd701288445de2d238": {Code: "TIBET-INCL4-XCH", Name: "TibetSwap LP INCL4-XCH"}, + "72761d19301a98b36cfc9ff09e82e703c9b35ad753ff216208fd7492356c995c": {Code: "TIBET-LC-XCH", Name: "TibetSwap LP LC-XCH"}, + "730a00819224b961ae0fb9a639a93e5d668a4d66dca40d6b3671b694169764ea": {Code: "TY", Name: "THANK YOU"}, + "731e60929c0afe235213ea2f9961ba02f518c0f84f6d1892eb55714851ac2fbc": {Code: "TIBET-HOCK$-XCH", Name: "TibetSwap LP HOCK$-XCH"}, + "7322c53521b3a35eba6220a52571526d840e2c91fcde494c7fd1480aa76c6571": {Code: "TIBET-\U0001F41A-XCH", Name: "TibetSwap LP \U0001F41A-XCH"}, + "745abada28e244928f91736ed8051e16930a5b6fdc6a89ec85d91133c50c9615": {Code: "\U0001F420", Name: "Fish"}, + "74d0141c75654ba350a57a28f84a887258c272487e855aef078ac0f794d9f571": {Code: "TIBET-DAT-XCH", Name: "TibetSwap LP DAT-XCH"}, + "75602bcb965c2e6fd513b23a740f8a9eabd3dfc8a84fff8a20b49a149d58c33e": {Code: "TIBET-TICA-XCH", Name: "TibetSwap LP TICA-XCH"}, + "75832d4f278b27f9e0f2d52d21e653b00ae9aa143db53f585e3a08f987c73e99": {Code: "TIBET-wmilliETH.b-XCH", Name: "TibetSwap LP wmilliETH.b-XCH"}, + "759f4e056d21c4b6cce0f955dbdfdd12c9826e5f4b3b6e68a574c78c1b2d9076": {Code: "BTCS", Name: "Stably BTC \u26A0\uFE0F"}, + "75bff94f0327043b919aa2fddfa4ff079c7b45e90bc76a30eddd562e5d0d520d": {Code: "WYRM", Name: "Wyrmshard"}, + "75e781078f75674eb006ff34cced56afb98483bddbc357f2d5486decb27ed4a7": {Code: "TIBET-FRDM-XCH", Name: "TibetSwap LP FRDM-XCH"}, + "769a1598eb4dbe931f7a9e98ebc8935130769b81131353d1f531ad79f5ecb6be": {Code: "XWOOL", Name: "XWOOL"}, + "769ba7250524b17ce00ec265587872f0c5bca943110aee5a149c02b199219e85": {Code: "TIBET-FISH-XCH", Name: "TibetSwap LP FISH-XCH"}, + "76f8a24339230dcf8f83d79a6d7823929671f9dcc564583ae17f5aafaeceb68c": {Code: "{WC}", Name: "WhalesCoin {WC}\U0001F340"}, + "771fb6f5ec9a5a51243d45e2378bed7f1733221ec5cadeec8624a24f226ec1af": {Code: "CNUT", Name: "Chia Nuts"}, + "772a41a72c91471fcfdb6d74ddc1bd47b25de4536be770a09fe29ec063f14a33": {Code: "BRS", Name: "Brave Seedling Coin"}, + "77b5178d32f66932dc9e48157f029b8a96bfed577ee877d2b22cd73d6799e424": {Code: "TIBET-wUSDC.b-XCH", Name: "TibetSwap LP wUSDC.b-XCH"}, + "7852eddc5b8a909046a8a2cbf5aac99ffb44b57940f39bd9db176dcad181d71d": {Code: "BCGRN", Name: "BCAP GREEN"}, + "786005b4f225b6d80c921a8d01b3ae001386156468bf937fae70e60fd21fb4c6": {Code: "ASTRB", Name: "Astralbark"}, + "78b0c79c2844b5516ec5a5611eb359478a6da15c34c66a17a26ffd2e98a09c18": {Code: "WITCHER", Name: "The Witcher"}, + "78cf127de2ce653ed234d0857f1fbc09ec81c92b50cb32c85afe15d3707578d3": {Code: "TIBET-wEURC.b-XCH", Name: "TibetSwap LP wEURC.b-XCH"}, + "791b0bd5c1ca39038469c3f7346881889e81185a8e02aea84f2a8d61ee1d9e6b": {Code: "BEATS", Name: "BEATS"}, + "79344c6df333dc75da4568495dc61a557a00552a0839012345b53e170e24010a": {Code: "POA", Name: "ProofOfAnything"}, + "794095e659d9529d36dd22425ad0e6aa983efb37ee8b8f10dfedc18941029efd": {Code: "TIBET-TIBET-\U0001F355-XCH-XCH", Name: "TibetSwap LP TIBET-\U0001F355-XCH-XCH"}, + "79ca93817d91ea055c380393e1e3485b796f33afa5df8b7e2e754d16d527d6c5": {Code: "GRC", Name: "GymRatCrypto"}, + "79f6313fdb6ba66347a5bcad4af6878ac07bf5fafedeb384c3b350d913c8b6b6": {Code: "NIOC", Name: "NioCoin"}, + "7a0b3df66ba4eddbcdbb8071b9eba266cd3cb8a38d24f5b4e211e9a8bae0767f": {Code: "D210", Name: "DUBAI Token"}, + "7a99007d5db1e757a4bab4339f24f76b6619b2a2769a65936f4918e9bee17c60": {Code: "SHD", Name: "Shard"}, + "7ac3e932a216e518b81bc519e683e006295a96e77bbd5d5ed03a0fcbfccc9448": {Code: "TIBET-\U0001F9AD-XCH", Name: "TibetSwap LP \U0001F9AD-XCH"}, + "7b123d847de71db929e863b66c886517fe46fc1260f4fae78fc35afe296aec02": {Code: "SNAIL", Name: "Slowlana"}, + "7b502f7638c2dd95849546f585ef6013c52ff0b70ccd1c6ffc13b965fc88d89a": {Code: "TIBET-PAW-XCH", Name: "TibetSwap LP PAW-XCH"}, + "7b8e3efb6292a9737aa29374a611b18103c7d3ada28a5e36db6434c2100cf671": {Code: "TIBET-XSHEP-XCH", Name: "TibetSwap LP XSHEP-XCH"}, + "7b90239c646bc7660e95f46f30e44d6e706a2c9a1b4ce3c8cc676e99168950aa": {Code: "TIBET-\U0001F980-XCH", Name: "TibetSwap LP \U0001F980-XCH"}, + "7c1766edb4c48c6a692715def4343af1ff8d68ab7db7acf93f8e4658329294d7": {Code: "Plank", Name: "Plank"}, + "7c76993c26b9b3f37e79cf5ae8f30bbbeab85648bd12f43cd110067a4a490080": {Code: "1BTC", Name: "2XCH~=1BTC"}, + "7ccf260808c1c3b8d87e02ea2f8df9cf71d793b62c6cb42bd654aa289410f9b5": {Code: "TIBET-HOA-XCH", Name: "TibetSwap LP HOA-XCH"}, + "7d176bff0ac2e15fe6e5f57352e7c3a467b476a798dd38f30e39af7ef6cc52b4": {Code: "ALORE", Name: "Abandoned Land - Ore"}, + "7d4de8d69be7a3f661a04284ec96e7ca1c33bf0f5eeacf8740e8d37ff4d698a8": {Code: "CHUMP", Name: "ChumpCoin"}, + "7d9e10895f967c7fb67eb9b6f866b636d73a336eb50b4097260cad0be70e5b97": {Code: "BRAM", Name: "Bram Coin"}, + "7df4ee8b1248811068debe40b94d2f78d0d1ecd5e3abe1783db1ba3f1f412f7f": {Code: "STONE", Name: "OfferWars STONE"}, + "7e0353e36d52e8e7b3a985e493b229f10014ac86f87d4ce15f11c1a2bf963d4c": {Code: "ASON", Name: "ASON Token"}, + "7e3f8040eed9cde63b88544bb44adfe3d5be3a863e1ffe3cd5147a3e39ccf876": {Code: "TIBET-\U0001F9A0-XCH", Name: "TibetSwap LP \U0001F9A0-XCH"}, + "7e882e3eb390635ab660c452747c9a352f991ac066b071d02f889974047e8499": {Code: "TIBET-\U0001F36A-XCH", Name: "TibetSwap LP \U0001F36A-XCH"}, + "7eba2dfb8b0be8a91bcee8416f70d5d2f15466de5f8604622fee5dc37a8da237": {Code: "KEK", Name: "feels good man"}, + "7f147901d20472fab470543c5eae51309ee54d4aecfa186f7366e2676141e139": {Code: "BEAR", Name: "BEAR"}, + "7f83b8c04aa42aac4c00c6cec630a7753763a5dbb3f22619994316ae96c736d4": {Code: "TIBET-CHNN-XCH", Name: "TibetSwap LP CHNN-XCH"}, + "8093fcd312eceb63fcac9306d83e257a43f8a6bed628ba0c9dacd853cfd37413": {Code: "TIBET-CHAD-XCH", Name: "TibetSwap LP CHAD-XCH"}, + "80f3eea06bdb757c219e164eb66a305daf84e97d2954f9e3ea4fc50080af2e46": {Code: "KEY", Name: "Friends Key"}, + "819740b6134ecfbbd75f59fbf394040c9acc5ad415ea3bbb2edb9c36a8b5a31e": {Code: "TIBET-TY-XCH", Name: "TibetSwap LP TY-XCH"}, + "822d7ccfea53bb54350f76ea985979d131871e7dcf3edd6f9ee60e8610e5653e": {Code: "TIBET-PUMP-XCH", Name: "TibetSwap LP PUMP-XCH"}, + "824c71e37ac660006e03f7884561e7a124d930460ae1506a9c234c06ebc6aa1d": {Code: "AIR", Name: "AIR"}, + "825f1038141bf7861889c0e3ce4c4e08594b8b5df9083974dbcd49432ab5e6f1": {Code: "TIBET-HERB-XCH", Name: "TibetSwap LP HERB-XCH"}, + "82bd24df4a320d3324dc3aa259b2934ac887b32e00f09dac3eebba22128eb84f": {Code: "DUCK", Name: "\U0001F986"}, + "83314b078d6247e4974faf6d63b8c37a1dd83cc667c74e5c58240223c4f5e4cc": {Code: "KALI", Name: "Kali"}, + "8380f9620eae0e10f7cee74cb812790c3eb600e4abcff46b380ae7c95bf31092": {Code: "TIBET-SNAIL-XCH", Name: "TibetSwap LP SNAIL-XCH"}, + "83bb69d63eac958b1cfd4b4f10a286c9926ebdfc4306c49a0205c4eb455ed395": {Code: "DUNK", Name: "Dunktober"}, + "83c4c586ed0943dbe301ca31d7d50f60415e49ed76db01ad2e0a99981a99d59b": {Code: "TIBET-0000-XCH", Name: "TibetSwap LP 0000-XCH"}, + "83c8fd2b3d0f80a7bca4b0021b3bee13b6b44f82303be8cb8dd61252e342704e": {Code: "TIBET-BANANA-XCH", Name: "TibetSwap LP BANANA-XCH"}, + "83e0e951a4f5d137502fec716c16856638b8c1c8c1d6dff0a4005b82686c2c09": {Code: "TIBET-DID-XCH", Name: "TibetSwap LP DID-XCH"}, + "83ef7c5620a25e05949ba096f55e269b2b8fd0d675a2698f1e8d9ce15fb9d467": {Code: "TIBET-wLOVE-XCH", Name: "TibetSwap LP wLOVE-XCH"}, + "8456727ec39de3baafa7fd1432feada91d5796d12e1a241f836ea343b92da54a": {Code: "TIBET-ZEN-XCH", Name: "TibetSwap LP ZEN-XCH"}, + "84d31c80c619070ba45ce4dc5cc0bed2ae4341a0da1d69504e28243e6ccbef37": {Code: "PP", Name: "PP"}, + "84e4b6e52378b3325d5173cc9c142d51fb8159a5bd25c00a21564a2cc889d8df": {Code: "ZEN", Name: "ZEN"}, + "84e8664b6273cc4d5f30f0240b605cfdbaa9bae13deb3aad3170c3f5120247f0": {Code: "AK-47", Name: "Automatic Kalashnikov 47"}, + "8577f62d87eb5b41392ec3eeffc45b68f49030cf964ec04aec1d7bbb5f0063da": {Code: "XTC", Name: "eXchange Token Coin"}, + "85dc4ba6b903f440c8915b5edbe04dbbf0ec8864a9abbaa1bdeeaaa625a1f4b9": {Code: "CBDC", Name: "Chiabatta Backed Dangerous Cash"}, + "86290de455ae081dfe44c099743cb8eae18c705f461922280d138c24ac8c3110": {Code: "CATMS", Name: "Catmosphere"}, + "87492d7499e9f5a884c9c7a5db21c05a5644b7f53208a1bd8916dce056b9ec16": {Code: "CBDS", Name: "Cash Basic Defence Self"}, + "87b84c43896d43e765132572c22e6988aae59649b57d54a252d2c759f1eb8eec": {Code: "IRON", Name: "IRON"}, + "88970c9500b70e3a4cc2a7bb9a50dbffae190ea730be475d43b5001a431d6c2d": {Code: "TIBET-G4M-XCH", Name: "TibetSwap LP G4M-XCH"}, + "88c30463dbc67745c3eb85f0a8b3df1bf4b7ae117926d7dc77042e04ad367077": {Code: "CCC", Name: "CCC"}, + "88f6ef9b9c7e02eff129a7ceeb3475e32d7c802bd384b3943682e2f7f03e57b9": {Code: "TIBET-MFM-XCH", Name: "TibetSwap LP MFM-XCH"}, + "895593f0b575975cb15e092e5a06249286a6f8263e0edbf9f4f5bf8ce1709eac": {Code: "TIBET-tremp-XCH", Name: "TibetSwap LP tremp-XCH"}, + "895d30090a17116d282af37cedec585613d4ee0faec3eaa7e19eb3f2bcd236e1": {Code: "TICA", Name: "Taichia DAO"}, + "899dd11ccee41f7662997e328981ace4eed2ee90bfe51f7404c7d827b8dcee39": {Code: "CROAK", Name: "CROAK"}, + "89ac0d910e71ba15ebfb31e8b191654039fe4e65c370a30f63e7a60554ca7668": {Code: "MARS\U0001FA90", Name: "Mars\U0001FA90"}, + "89f3f718cd4077f1685f338974ec7ee7850444d4e2fb9b5c4dcf02e40e12663f": {Code: "NIOT", Name: "NioMint"}, + "8a0208f53687ca9dfc5ea48dd14396461707848baeae6230b4977b05654ea8a0": {Code: "300", Name: "The 300 Spartans"}, + "8a5bfe4e3fd0d42f0a42456d649c933230419ee38eb347c3e55e6e7aa572b1b6": {Code: "\U0001F433", Name: "Whales"}, + "8b82932596d543ee08fe3945a9a436879d7f5051bcfed08e28f4048cb4cc5a07": {Code: "XEG", Name: "XEG Green Token"}, + "8baae48bf827d4fc7412565b1055c2ffb3daaa90374d5c1c04b05d435e4ff765": {Code: "TIBET-SNAP-XCH", Name: "TibetSwap LP SNAP-XCH"}, + "8c0b6466b559d27ddbeebbdf3e031861ff613b438070284ef4f914116d96ba44": {Code: "FRDM", Name: "freedom"}, + "8c4025a01025990526a56a84c3faf65d32ffb678fae9e6bcc0d737ebd3a20616": {Code: "TIBET-WITCHER-XCH", Name: "TibetSwap LP WITCHER-XCH"}, + "8c77de1427156b98fb15cce77d908f79bd69f6b4b8e3a60d8e051dac481b5365": {Code: "STR", Name: "StarBits"}, + "8c7becf84cdb78965fced1a911ebf2d489580e94c1db842674846fe1fb2e7551": {Code: "TIBET-GOT-XCH", Name: "TibetSwap LP GOT-XCH"}, + "8cf1c0d1a196d307dc63d0bc1a8be1d996847c0f81756f8b6e6ae2bae0ecc50a": {Code: "MPC", Name: "Megaparsec"}, + "8d3554b6463be55c6db0824aa31a0e3f4b222a6937e308a51530483d449816ae": {Code: "SPKDS", Name: "Sparkling Dust"}, + "8da21294d1ef0a371d17f3fbdbcf54e09d5e3bc0553ada9d2a11334c46d6ffe0": {Code: "TIBET-DZ-XCH", Name: "TibetSwap LP DZ-XCH"}, + "8dddca755fa6f13f4acab5bc59c1a7656fecb9d7cbb68701855401ee0cfe390e": {Code: "BLOOM", Name: "Chellyz Bloom Token"}, + "8df7d606644abc6b0f23d8e12e10b4e57b6824f26135e2b00b88afa7706e03d2": {Code: "TIBET-ENIGMA-XCH", Name: "TibetSwap LP ENIGMA-XCH"}, + "8e2cc7adf5e2096fb76dbb15ec399539bdd5affa86a8690037708efae5dac68e": {Code: "TIBET-BEAR-XCH", Name: "TibetSwap LP BEAR-XCH"}, + "8e3d0fde796c57138d46c20d1cb04ea59bc4030456788914b5e5c127258be7f5": {Code: "TIBET-WAR-XCH", Name: "TibetSwap LP WAR-XCH"}, + "8e97db7777b127a6a13b7ecb33206fd99d82fb59cb4fc456df93633ce00da4ec": {Code: "VALG", Name: "Valentine Gold"}, + "8eaba7f33f848a30ed376a5f5fd20a49d5adf93a0bb894ae857c5a0ff52195ce": {Code: "\U0001FABC", Name: "Jellyfish"}, + "8ebf855de6eb146db5602f0456d2f0cbe750d57f821b6f91a8592ee9f1d4cf31": {Code: "MRMT", Name: "Marmot Coin"}, + "8f27f8b6aa31e07f4de43e12eea854ab450d56afdd65be4328928f5bd2fa80cb": {Code: "XIPM", Name: "Mystery Coin"}, + "8f282ef9b7486ed68a4e5e5cf40627a2b6f6cdfd69a18898f73dec52e416c9b5": {Code: "TIBET-CIC-XCH", Name: "TibetSwap LP CIC-XCH"}, + "8f93ddb54dcc52f8c03a786d18756d6f1bd1689eb89b509b57d57c4a8af89323": {Code: "TIBET-CHIEF-XCH", Name: "TibetSwap LP CHIEF-XCH"}, + "8fac6c37fa3bf95bdb2e473df9839fa5f50580c7bad8b6ea13ad201b695b9398": {Code: "MINUTES", Name: "Minutes"}, + "8fdf5148a7c865bda3f84b8970c4cfe324e5159c96c3eed06ec87933ca27f989": {Code: "BGT", Name: "Baby Groot Token"}, + "9033476927bafdf46a8e422ce0a7e0876ef10c12c79e94c087cbd96c94874219": {Code: "TIBET-\U0001FA84\u26A1\uFE0F-XCH", Name: "TibetSwap LP \U0001FA84\u26A1\uFE0F-XCH"}, + "90ee27c55480fe039e10b8f7bd8ff9bae64b68ad6d1a89c810e82ce23476eb34": {Code: "TIN", Name: "Tin Ore"}, + "91154f2515698723f5f2f1a41f6ebd0a0b0ce87d3737a163f696d33655d91c8a": {Code: "ACID", Name: "Acidium"}, + "91a48b56d7d4e11e9419b1e2c06859f3f6d9ad4a8408c820a10b2da173abb59d": {Code: "TIC", Name: "Terran Imperial Credit"}, + "91b049f4d96ed30215c2548b0933fd382eaebe248ad3b25026e78017f6f63649": {Code: "XLNG", Name: "Chialing"}, + "91caa456661a58e1f953e8850138ed3419a00e286e2b8f36edd77a52de60e9d9": {Code: "TIBET-DWB-XCH", Name: "TibetSwap LP DWB-XCH"}, + "91ffe356c7b3fc89862ec5382cd632a6605bc9b66dc16ec5e3468125727fcfd7": {Code: "LIVT", Name: "LIV Fan Token"}, + "92ee34a9728f12e889e79da5ccfa13267ca106e85e57ebf3f27232992c80c2e0": {Code: "XPA", Name: "XChangeNFT ProofofAddress"}, + "92f9457654ca2808440587c3347ac923f8e41c05764b45da3454b210ce4268b7": {Code: "OEN", Name: "OENOMAUS"}, + "93f61c25eece661130a4a94bf9ab31e2894e02bb21d57f5ff36a7554fbfee179": {Code: "PAW", Name: "\U0001F43E"}, + "940613a8af071d5ad8feb65e1eef5aaa3813e5091988734c75689cb2dd1c272b": {Code: "XIVI", Name: "XIVI"}, + "94621bf6d74f26866812a69b9bc7cb2b25507b9f5717ebd768c1ec0ac63796aa": {Code: "FELIX", Name: "Felix"}, + "946ebdd5828503a5239bbefdee10e3b933c92b48f84a0b4ba5c69a7007c402d5": {Code: "RADIQ", Name: "RADICOLA IQ CAPS"}, + "948b43068a09895cdfdf3a3eae857eec450a34677fe69fc5ce39df36b3a40fec": {Code: "CO8", Name: "Crew of Eight"}, + "94a45df93c1a480fc5b0ee1a8992cf9e0932c033d96ac84b22df255ee41e4b97": {Code: "TIBET-\u00C6-XCH", Name: "TibetSwap LP \u00C6-XCH"}, + "94cc0fc3b49fa8bcec178b494dfd776ee321979839a9332d7ee87056d9f76cff": {Code: "TIBET-BTCSC-XCH", Name: "TibetSwap LP BTCSC-XCH"}, + "950ecbc2f34449c0e083bfc22b087ef3a794642acfb760d1fd00612ba1c9955f": {Code: "MED", Name: "MED - Games Token"}, + "953615110fa1f1a2680a5106d5c47aec35e9c0f543034c994686862e16091f99": {Code: "UFO", Name: "Cute Aliens"}, + "95c44cc193d89b0c31b4b95c12be95192953c991b436b2f658644839be91f683": {Code: "COMFD", Name: "Common Provisions"}, + "95ecb053f9c55055a3431026400575bd87b287035321b539f9e7b8ae751a5916": {Code: "TIBET-\u2615-XCH", Name: "TibetSwap LP \u2615-XCH"}, + "96a7cb7f9872f84d6471dd3a2442f05deaf59df46197b877cd8afc32e872267d": {Code: "TIBET-whoren-XCH", Name: "TibetSwap LP whoren-XCH"}, + "96a914615cbf6adb9ba7b120b7577545826147a3b72517429d74418ef6c72776": {Code: "TIBET-CC-XCH", Name: "TibetSwap LP CC-XCH"}, + "96b1c5fef756484b81490c2ebe97b0ac15b5871dd6900c12ca97769633196f3d": {Code: "BCBLU", Name: "BCAP BLUE"}, + "96de47df2c31441efa46d049d2763c750561f0cfa1db3c8bfcb4c72db94a06f6": {Code: "HNTR8", Name: "Hunter of 8"}, + "96f48bf42952aa8364194f2d3d007f4289df2b1cbc7e90085d347c98e1246b66": {Code: "MYTFT", Name: "Mythic Feast"}, + "9720fcb8333984c72f914fc5090509ae9f7b1ff72eff2ed6825d944d7a571066": {Code: "ECO.49.2022", Name: "Antioquia and Caldas Reforestation 2022"}, + "97357cdd797c101739a0e0415809fb2f25958e59117ebcb1c21ee0c7c5cc4ef0": {Code: "TANG", Name: "Tanginium"}, + "97bd6d179cedfff84f4a0e82982765dfeab0eec7c1cef7ead0b5f23360de7a63": {Code: "NGDPLT", Name: "US NGDP Coin"}, + "9841225e2372fb4a32791dd96ca231b25e4f7d14e9b59d217b8881a82f858806": {Code: "XMEAT", Name: "XMEAT"}, + "9870e1db85db9b03fc64272ad5b402b92c61a3ed3a72f046666da4355ebdb8f9": {Code: "TIBET-ANGEL-XCH", Name: "TibetSwap LP ANGEL-XCH"}, + "9871568c71fdab47f634673f10a539f47eb074e32920e7275c0b223a9d8d176f": {Code: "TIBET-TANG-XCH", Name: "TibetSwap LP TANG-XCH"}, + "98be421bf2afd03bbaa71d7217216837790a9ea7a710f0b684cc9a4cc9f6f153": {Code: "TRPC", Name: "Transporter Credits"}, + "9995f30b83e7d3f3ea0eec7e450dbcade83f76516da79daa9a84a9daafec2329": {Code: "ALWORK", Name: "Abandoned Land - Work"}, + "99d34b73d2d1909f1c9ac105ba064cc9864c440a8e5f8bedf6924b8b797713e3": {Code: "MFM", Name: "Mega Fat Marmot"}, + "99fc775b37ca4fe4665b424afc2e2f8337244dbd3109adf140918ae183ebe277": {Code: "whoren", Name: "delizabath whoren"}, + "9a272827e361f2171ba80332940fbe9d76c0932c41ef9925578bd30a8f587f8b": {Code: "\u00A2ni", Name: "\u00A2at Network Index"}, + "9a2aca2b130287259e3855a108365f527a905c591ea6f37b4d31b6b705ce1e9d": {Code: "TIBET-MINUTES-XCH", Name: "TibetSwap LP MINUTES-XCH"}, + "9a5909a155459ef7af9c83bdc2e5176c84e03060c075a19ce56d4fe3d27b3a6b": {Code: "TIBET-BALLZ-XCH", Name: "TibetSwap LP BALLZ-XCH"}, + "9adcd7a519a70471960a3e5914d7c14735925683b0e7440ffd058bd0c41ea916": {Code: "PFP", Name: "PFP"}, + "9b585fc0851e74393de98332f486604e02eefd58d6a95647f8551a692e535b76": {Code: "TIBET-USA-XCH", Name: "TibetSwap LP USA-XCH"}, + "9b958904ebff33ab7dad482323985dfaa347fce56ce3d19ab986964c7b13fe6d": {Code: "MAX", Name: "WhereOmak?"}, + "9bb8ffd2f1675fc23c9daf581e613bf791b06102f2d2f7950f7d776894a04a85": {Code: "TIBET-GYATT-XCH", Name: "TibetSwap LP GYATT-XCH"}, + "9c09a4ca57065cadce8f3194f985056773360968a1e90a70dbe37f3630deff1e": {Code: "CMTG", Name: "CMT Gold"}, + "9c341110ea40779a26de81616fbf95dec3dec5e3ee5cc4479aea99dfa8ccecae": {Code: "TIBET-MECH-XCH", Name: "TibetSwap LP MECH-XCH"}, + "9cccbe22b036a1d75d31a828b20fe1906ecb5c3358e4a0358c5a2359aedd01f6": {Code: "TIBET-MIRROR-XCH", Name: "TibetSwap LP MIRROR-XCH"}, + "9cdfb5fa1fe006386ac149f889a6ad029b397656a6d5660ec695606d101ae249": {Code: "\U0001F346", Name: "WANG"}, + "9d9264c542c2a3108c7b8f74cad82b60dcbb6e50b328e9cdbaa7acb468a5707f": {Code: "ECO.29.2022", Name: "Smallholder Reforestation Project 2022"}, + "9da21774b07828ecf8e55081e647e1f68207f4c9888c7a18a4cb69e862c98f37": {Code: "XCM", Name: "ChakMoon"}, + "9db189e76bafe22487cae5ce122eadb0b6217f169f9c5db07a4eeaec27a39a6a": {Code: "BWF", Name: "Be W Free"}, + "9dd6c418280702dd5b5aa99396abb8ead1d5bbdff9141051c28633d991c353f7": {Code: "TIBET-LKY8-XCH", Name: "TibetSwap LP LKY8-XCH"}, + "9df8fc00cdd27b4951badb2662acc047a52c86617b2a1616a402770cc1c3c3eb": {Code: "SAO", Name: "Shares DAO Governance"}, + "9e04d998a90bca05bdbc301548c83104d79256f1b00bb2542104e6c6c37f2421": {Code: "BBT", Name: "Big Brain Token"}, + "9f0ad77b6e99f53eb3d19039823ec7ef075bf34f61da53d08a1c0ef5c960905b": {Code: "TIBET-LNR-XCH", Name: "TibetSwap LP LNR-XCH"}, + "9f65e250eae02e8a64f1d89952431f369fd667386c19acce95dc217878b2e134": {Code: "TCHST", Name: "Treasure Chest"}, + "9f81bc1e1fbdc4027a08ef19923f3835c87311b70e897968c72fd20b2cc266b9": {Code: "SBF", Name: "Spaceballs: The Flamethrower"}, + "9fada1bb28ab17a051b87a9b8392e08058a85b80f418651badecef7a92ef8824": {Code: "TIBET-ALEXP-XCH", Name: "TibetSwap LP ALEXP-XCH"}, + "a05880b37d4a1e5152e6398f4d66e91757924aeab13435143e747a1e6edd00dd": {Code: "TIBET-INCL2-XCH", Name: "TibetSwap LP INCL2-XCH"}, + "a06791271e9b3a8d262aeb1f688aa10adbd60b33e1df24fa057c1172dd0e8214": {Code: "RMDT", Name: "RMD Fan Token"}, + "a09af8b0d12b27772c64f89cf0d1db95186dca5b1871babc5108ff44f36305e6": {Code: "\u2728\u2764\uFE0F\u200D\U0001F525\U0001F9D9\u200D\u2642\uFE0F", Name: "Caster"}, + "a1001eb23ea19fd18a6294d949aeee78e622b7bc3f92e29596a268ec0c38f9c0": {Code: "GENINC", Name: "Genesis, Inc."}, + "a1b036efdc6767f612dc2c8f9a1107c7e879f7d02881c67085ba37f79ef23536": {Code: "DPR", Name: "Dred Pirate Roberts"}, + "a1cb22807f3a273355ebc1e9b886f8f8ca8a7f8d7716d5e40e3ddd498d6c6ae5": {Code: "LFG", Name: "Let's Farmer Go"}, + "a1f6b48d2ba5b8b405c4c5ed8ec5a1787238efdb81270ed21cda21508ec2f149": {Code: "FISH", Name: "\U0001F41F"}, + "a20150452b619a45cf2a7d3a1cb39587af6e9a50f2ae1d3e558751b59d619179": {Code: "TIBET-KEK-XCH", Name: "TibetSwap LP KEK-XCH"}, + "a28ab4ac91b5cb23f1156f22a248e51b509be8f2fa76eba663a9544eddde09d7": {Code: "TIBET-$TVL-XCH", Name: "TibetSwap LP $TVL-XCH"}, + "a2fa900d1afc3d8bba427cefb8e6cad09c1eb8796223b35341c7ebc81929183a": {Code: "TIBET-TL-XCH", Name: "TibetSwap LP TL-XCH"}, + "a30b9db819b73cfe6de386e231f8322462ca21e7e74966f242db68ef8ebd7fe3": {Code: "TIBET-CHUMP-XCH", Name: "TibetSwap LP CHUMP-XCH"}, + "a30ce6074671db2926e00a077ba1471782fb36eb9e2c8072082fb239474b96a7": {Code: "INCL1", Name: "Inception Liquidity L1"}, + "a32cc6a45b5ade539d96b68d6323691d4437d9a951b8cce8211ae07b904af0e7": {Code: "OKR1", Name: "Okra Folk"}, + "a36680b5ac48be3bb816ff99a0e64b278061c193242d6d60622c70b0c6fe80b7": {Code: "TIBET-PEPE-XCH", Name: "TibetSwap LP PEPE-XCH"}, + "a3e3820f0c80723c1a504c8acb56f4c560938b6eb85584c36de09a83fb6c2397": {Code: "CISAG", Name: "XCH CISAG"}, + "a3f472b7fcc11cb5881510fe5cae232ca0e662e79d3cb2e4a39b7bb908e35e02": {Code: "TIBET-ZOMBIE-XCH", Name: "TibetSwap LP ZOMBIE-XCH"}, + "a406d3a9de984d03c9591c10d917593b434d5263cabe2b42f6b367df16832f81": {Code: "DIG", Name: "DIG Network"}, + "a48574f6ea4245c5e4a208576cc6b10b14a86d2b46618894fdf210c9d5bec950": {Code: "RPRT", Name: "Replicator Rations"}, + "a4ad44551f7ba3f75851a4533e0e98d8d49aea7bf643180a6879245ce7fbd28c": {Code: "WHUP", Name: "WHUP Token"}, + "a628c1c2c6fcb74d53746157e438e108eab5c0bb3e5c80ff9b1910b3e4832913": {Code: "SBX", Name: "Spacebucks"}, + "a6344df973735fc6eee9b3181909e1c441a5fed1016d078581c2421467898266": {Code: "OMG", Name: "OMG"}, + "a66ce97b58a748b3bb2a8224713620cca0ca00cb87e75837b1f04e3a543aaa40": {Code: "BANANA", Name: "BANANA"}, + "a7b739313f893e26a34677fe3de43def80552faaf3b123f15864580d64920582": {Code: "TIBET-IND-XCH", Name: "TibetSwap LP IND-XCH"}, + "a7c8135b134e1a477563480febefd2e2091afe15d24ab567045eb8fa024ef597": {Code: "DXT", Name: "DeltaXtrade"}, + "a82b3a2d2b9bb4d9fbdb4bd3bfce4bf56a1d1859a366e56fecf95790aed49094": {Code: "CF1", Name: "Cassandre Fairies Club"}, + "a84a40fec6d65fc97ea42e46ff7b2ea66ad66ab49b02d1673539d91d54fabae2": {Code: "\U0001F333", Name: "tREE31000"}, + "a8838e15919621dfc8eee85640995231bbf90a025341f3aff6247e0584693777": {Code: "DC", Name: "DEERCODE"}, + "a8859921de8eadb477972e11821dd15420acff79edccbc297181846722d2c9e1": {Code: "TIBET-GRC-XCH", Name: "TibetSwap LP GRC-XCH"}, + "a8c46dc5d1bca99944ef6b2261998ed7bfc66459ec695f86e341283013b692a2": {Code: "TAP", Name: "TapChia"}, + "a8d60e4861f4472d88a82bcdf565fbed633dac508be60722b0be3f5808c73b32": {Code: "ECTC", Name: "ECTS Cash"}, + "a8e913a16952f68a4e8a6b4ec65686ec7456dc5c9045b6c7e0dd9a4656f72ccd": {Code: "boden", Name: "jeo boden"}, + "a92a8d0896ebf1c776b101d2ca8c8c6d562c49fb2848a8df4c31864dd1efdaab": {Code: "TIBET-\U0001F37A-XCH", Name: "TibetSwap LP \U0001F37A-XCH"}, + "a96b37350c890bfb16cafc3acd10e249a3424e7ca42510c8f92e0a9838665dd3": {Code: "TIBET-XCOW-XCH", Name: "TibetSwap LP XCOW-XCH"}, + "aa15228b460f978dba6342bf95ad69a137cdc5e9826ddc6d05ca084ffc51238f": {Code: "EVG03", Name: "Batch 3 Token"}, + "aa3038a70653b03929fa9af938124e04954a1e10ec1d924f968e94dd6b215987": {Code: "BRICK", Name: "BRICKs"}, + "aa5a61d556dac32483a18564d45a8a1a34d7851ea098ad1f5be517e428edd7e9": {Code: "EVG06", Name: "Batch 6 Token"}, + "aa6e21b39fe65c47ffbc22c8a5bbaa1ccd52687f988264c3294b7cdc554f4979": {Code: "TIBET-GFY-XCH", Name: "TibetSwap LP GFY-XCH"}, + "aa700cd7908217b1f8af79d6dc49cea1ae4a62bd989e872ef73670f2e7599e51": {Code: "TIBET-WESO-XCH", Name: "TibetSwap LP WESO-XCH"}, + "aa937ec8694be546e967642d7a2191d7bf01cc98f736f908a5a74fd39de92b8e": {Code: "AZZ", Name: "AZZ Token"}, + "ab1634e50e891771672bd3094c1c57a1a2187b2a8c7a1545f3d4b06b5608f4e8": {Code: "FMX", Name: "FarmX"}, + "ab38323fff407e0f29ac86db86442d1a480ca0bcfd92f572fa4ced68e27bb098": {Code: "R34", Name: "Rule34"}, + "ab40b37740f65194415c7c413264a1e5d483d4a9c34b8168d3b9cd1ebdb717c0": {Code: "TIBET-BET-XCH", Name: "TibetSwap LP BET-XCH"}, + "ab485f45382ad2e15ff56e2240dc30f209cd9fa69adcd6af2038a177e47fd92b": {Code: "TIBET-SHD-XCH", Name: "TibetSwap LP SHD-XCH"}, + "ab558b1b841365a24d1ff2264c55982e55664a8b6e45bc107446b7e667bb463b": {Code: "\U0001F331", Name: "SPROUT"}, + "abd03d855cb2c11bcb67427ad63480eec4c36091bd291018041cc1dce173a000": {Code: "CRCH", Name: "CrunchCoin"}, + "abdeaa3009745478d1aa4ce462b574d1e433b1a12942dc83789fbcaf36144a5e": {Code: "TIBET-$CHIA-XCH", Name: "TibetSwap LP $CHIA-XCH"}, + "abdf87384471882b33e41bbb0b09c0d46f51b34ae1eaae713d9afe6828f441a4": {Code: "TIBET-POTT-XCH", Name: "TibetSwap LP POTT-XCH"}, + "ac403f64f21cf68acf2ee9291328f3008fc0c80c4b29115141dcef26646fba17": {Code: "boss", Name: "boss"}, + "aca38d438bbe5279832f8d6e7bad54301d9c58b8a4225007614a3ef2008d6e66": {Code: "TIBET-XCS-XCH", Name: "TibetSwap LP XCS-XCH"}, + "ad0406a6f9aac4b4682cec430f728d6002401330c326cb0a99ab496c32924093": {Code: "PS", Name: "PawnShop"}, + "ad737980cfd6a3ef70bb2e0867d23ed72a954de643d40b51d075cb413bf292cb": {Code: "TIBET-NeckCoin-XCH", Name: "TibetSwap LP NeckCoin-XCH"}, + "ad84b5ed5f9a040a12cf75d9ad4c592dbe2866d621b006c12280f14651fe1be5": {Code: "YOINK", Name: "YOINK"}, + "ae1536f56760e471ad85ead45f00d680ff9cca73b8cc3407be778f1c0c606eac": {Code: "BYC", Name: "Bytecash"}, + "ae158989c0837aff20afd03a21143eb2ca51c09570dec90d7844e5d76a88b339": {Code: "TIBET-MMW-XCH", Name: "TibetSwap LP MMW-XCH"}, + "ae53cf713749d347cb50a9bc92dae69348bb52e02a7c3cfd189a09ab434b8250": {Code: "TIBET-CNAR-XCH", Name: "TibetSwap LP CNAR-XCH"}, + "af23c8a35984e0fb5c21bb9dc773a5d185ad6ec568782c4e3c3bbff04bde33a0": {Code: "MHC", Name: "Moon Howlers Coin"}, + "af305e93a1d7e4f13aebc20a5e8aacdf38a98261d4e6ec07922b9aff5a2c7509": {Code: "MBT", Name: "MEMEBET"}, + "af7c58b91d286376b6f3602fa18211e4a0b6abc7edf54f3fa3806730ca5f674b": {Code: "CATPPR", Name: "CATMOS Paper"}, + "afc15038e0bc7226c4ebd37f7dd642f5d6a8f3f27075b70877273e126ec6939f": {Code: "EVG04", Name: "Batch 4 Token"}, + "afefb47c852838c9f66d5c116f11f1b655371377f6671d952f8253ce828461d0": {Code: "PIX", Name: "Pixel"}, + "b023202719015e0065a5be9d61cbd06ea33eb27bc0f3b745980030fc57c5c396": {Code: "ETHS", Name: "Stably ETH \u26A0\uFE0F"}, + "b0495abe70851d43d8444f785daa4fb2aaa8dae6312d596ee318d2b5834cc987": {Code: "DWB", Name: "DWB"}, + "b0eb1fbe92edae487377b75d38ac0778b7fe67af868c061245230ff13cf8c106": {Code: "TIBET-STDG-XCH", Name: "TibetSwap LP STDG-XCH"}, + "b0eee4ddb9e5c4dd868fb834ae987d504f0faa46becc79db6968ffe248422d35": {Code: "XWOOD", Name: "XWOOD"}, + "b121d01fe25c0f6097c269d0598c6cad99360d1674bb79bc29f8c4f7b9009951": {Code: "DACH", Name: "Dachshund"}, + "b22b1a805a3b846e2347d88269076a1be2af97534a1ef1d2e9df405f70c28990": {Code: "BRAM\u2655PUNK\u262E", Name: "Bram CyberPUNK"}, + "b29039adfe6697d5542085368b6a1fe81198c5943e4c6aa948c0875c112783b7": {Code: "\u26FD", Name: "GAS"}, + "b2aaaa9e3470d8816583f0948a53e862f3bdf7b53931a0a2a458316e74ec30f5": {Code: "DID", Name: "SPLENDID"}, + "b30027a8bf56260cf40366e0d1e2755e15d8508cb4f6a0f186206a011ca22cc9": {Code: "TIBET-DPLY-XCH", Name: "TibetSwap LP DPLY-XCH"}, + "b31aaec8f44a46c72980527d53fe7c560a9c07e17480dc32cd4c2d4428d3eeb6": {Code: "TIBET-\U0001F333-XCH", Name: "TibetSwap LP \U0001F333-XCH"}, + "b36f1344b3c1e229ce5f5313d96045e9c5bcde5516a270bf13c130bb1777fa4c": {Code: "TIBET-MHC-XCH", Name: "TibetSwap LP MHC-XCH"}, + "b3816771b634b015280eea37f854ceb6ae905c8b82f5d277e181d7c4f2635c19": {Code: "\U0001F4A7", Name: "WaterCoin"}, + "b3867458a6af107794a09b6083e3f0f05fbece0adae328f4df9ade53a60ca1b3": {Code: "C1MT", Name: "CAT1 Memorial Token"}, + "b44efa9efaf5a7be6ad27802ab301aa6a86b13aaefbe0c82aaf9202c4f61ad47": {Code: "TIBET-BRAM\u2655PUNK\u262E-XCH", Name: "TibetSwap LP BRAM\u2655PUNK\u262E-XCH"}, + "b4c8025254a1a6ffff1a05c9cb9d0521dd42f1efcb9e091ddd6b47d29150c8c3": {Code: "MPNY", Name: "MoneyPenny"}, + "b504550212dc2d4cdc07ecb8f047a226174968b7df7557c2304a048e5a3d6daa": {Code: "COPPER", Name: "Copper Ore"}, + "b51efc0f20039fe1ff47dbd6ce73267ecb3e929c639c060f5dfa7be13ec3084d": {Code: "TIBET-BWF-XCH", Name: "TibetSwap LP BWF-XCH"}, + "b5e6abc3f398a4f90ef30e6f3c0469b391a33c5f9ca2a632bb452104b2b9271d": {Code: "TIBET-DPLY2-XCH", Name: "TibetSwap LP DPLY2-XCH"}, + "b6483f4884a7625e42616a6969fdf1f92238245c7a1e67cc47964b36c6e4b811": {Code: "DRLEA", Name: "Draconic Leather"}, + "b6f058290720ce1d84384e7b5ae5165c3b355cb2bd1b5ac0772f1956d726cd29": {Code: "TIBET-GOAT-XCH", Name: "TibetSwap LP GOAT-XCH"}, + "b735204374700907ac3e8af6a21eb407ed4d92a43514626abbd4e1d1afafe0ab": {Code: "TIBET-\U0001F99E-XCH", Name: "TibetSwap LP \U0001F99E-XCH"}, + "b77428bed2f5a543961763677bc140193d4097eaa6f369170f6ac4ab2550c8a4": {Code: "TIBET-BGT-XCH", Name: "TibetSwap LP BGT-XCH"}, + "b7c0b9c3a59898a8a9e9be1e9d77d63be558bc57060257a2a667c8ca7ab687a8": {Code: "HONEY", Name: "Hairy Bear Honey"}, + "b837eb5fe827ed234e5508c58354f79e1776aaaeca665a62b02c76b049975e30": {Code: "TIBET-333-XCH", Name: "TibetSwap LP 333-XCH"}, + "b866c8075501879ec421062f065467092d20cbbd8cb4b0d3fccba220a16f0656": {Code: "HERO", Name: "Hero 2.0"}, + "b8edcc6a7cf3738a3806fdbadb1bbcfc2540ec37f6732ab3a6a4bbcd2dbec105": {Code: "MZ", Name: "Monkeyzoo Token"}, + "b9aef34a93278955a976bb71b3916ab67f50797cd9a4cb6c5c3dbbc7b14af665": {Code: "BONUS", Name: "Big fan NFT token 2.0"}, + "ba0505e9955be82a9a9239e657d4e9397e7124d7e7960fef66212e861a35ffbf": {Code: "\U0001F30E", Name: "EarthCoin"}, + "ba2b5d01f8379116ae6fdd547e39e3124bbf3ff2ecfb11c57c97077e74f201d4": {Code: "\U0001F42C", Name: "Dolphins"}, + "ba36df73e5f4ddd5c298bba7030678ebc1bbc5d4431663cffd0747099f81d8bb": {Code: "CNS", Name: "Chia Name Service"}, + "ba9750db89ef16feb3eabadbfb199509e3e1290851188e5625d0956ca1354b85": {Code: "TIBET-BEPE-XCH", Name: "TibetSwap LP BEPE-XCH"}, + "baa5da2d0726bb184673c9f5ec68a92499e5f164bab82461f5267dc1d65a3f23": {Code: "TIBET-HP-XCH", Name: "TibetSwap LP HP-XCH"}, + "bae62a38ea2e4d6fb165c6583ea57a2b28bedf73625e56073cdc53f7da1939e8": {Code: "FDB", Name: "Flying Dutchman Bux"}, + "bb1de510a86d52a950b56158d1125cda66638221d1335d7d1dd2fcc2ad5cdb2b": {Code: "INGDP", Name: "Inverse NGDP Coin"}, + "bbb51b246fbec1da1305be31dcf17151ccd0b8231a1ec306d7ce9f5b8c742b9e": {Code: "wUSDC", Name: "Ethereum warp.green USDC"}, + "bbbedf8bd9c7b1c2ef8fb211f28eccb84c498809d453a1df6ee494da872be281": {Code: "DMT", Name: "DAT MEME TOKEN"}, + "bc70aaacf4d807677b2c5933792eb626d22c4aed1f0ef3f2df7d5b35a6720230": {Code: "KBX", Name: "K Bucks"}, + "bcee35d7043f02480107a70203e8b483b8404b2eec08efd30ce612bd23343373": {Code: "42", Name: "42"}, + "bd8331db67ba6e1343b79d6b8ad7ca9bc403bd9148bcfac9ee6a5617b9f4907f": {Code: "VLT", Name: "Valuto"}, + "bd960d5a150ed1b4f2242fc5761097b017b530d19dc2943f5e1907a6be1a0d09": {Code: "CF2", Name: "Cassandre Fairies Club"}, + "be666a56d3b62f32ee83cc0cc03b2cb8f714751e0f37186a1be32c75d26c23e0": {Code: "DATAW", Name: "Data Wheel Token"}, + "be6b283c7010616c7d2ed18cd7950d42a35d932a47bed38727e2411f25cee4dd": {Code: "TIBET-USDSC-XCH", Name: "TibetSwap LP USDSC-XCH"}, + "bf13e218f980c513ef6445aff25800997628599398f3a37c00308645cf1a3615": {Code: "TIBET-42-XCH", Name: "TibetSwap LP 42-XCH"}, + "bf96d9d1a1973d41047e74d43117799d5b63b275c8f26921e6228ec84b553da9": {Code: "STNK", Name: "STONKS"}, + "bfcfab5d13381531d45a0aa73ff846e13e7b7f90f682e939f87c38a7b57bace6": {Code: "XSHEP", Name: "XSHEEP"}, + "c012fb7c993e11075f7234066ac851b5434d7305ceda871629136dfaac4d54fe": {Code: "CATRCK", Name: "CATMOS Rock"}, + "c03ac995d15d316a8ccdc9e3d7a0effea55ad1d26e82b71eecf73a049c3d9e61": {Code: "MINT", Name: "\U0001F340"}, + "c03eb501e5e9eef952f000c419eeb746fcd9ad23140309e011988375e68ed9ed": {Code: "BNNCN", Name: "BananaCoin"}, + "c06ee27f9bd92f751987e9d43b1709ef664e6aee0df63f040d00605826b54a67": {Code: "XFUEL", Name: "XFUEL"}, + "c09a612af3332491f8c44bb6a9dcbcdf1b20e214515bcd556be26ec078ab523f": {Code: "$TVL", Name: "ThiccValueLocked"}, + "c0ae895b3a9ee28403efc1754e4e7e4f539a71082bb0237762ef0b75fa9df7b0": {Code: "TIBET-AZZ-XCH", Name: "TibetSwap LP AZZ-XCH"}, + "c1a01edd117f0a2bddffcbac6ee044eb1023034c119a3e31cec5d1ea45a3ec39": {Code: "TIBET-STRRO-XCH", Name: "TibetSwap LP STRRO-XCH"}, + "c22914961cd7dc043276c129acb1414cd8f26b32144497dbe071155a5687f430": {Code: "EVG02", Name: "Batch 2 Token"}, + "c2295c8490df7aeae2a60ec9dfc70ea92d39fd0fc1bc8fbe1cdf6907b2523183": {Code: "TIBET-KALI-XCH", Name: "TibetSwap LP KALI-XCH"}, + "c291810ab8ce7e504f05dd8043456a61e375a915651f389e3d1a492a1f0bb0a2": {Code: "TIBET-$HOLES-XCH", Name: "TibetSwap LP $HOLES-XCH"}, + "c2cd1406e2a3700b016f978b56040efff44071d23bda4614c1978f3f5bfc4e7b": {Code: "ENIGMA", Name: "ENIGMA"}, + "c3abdd9c4932b34600cd33c998ec813e5b32c6125db301637c0d7f52189fc292": {Code: "COMIC", Name: "Comic Book"}, + "c3ff7a26e3b4f17a0450804177f6c06ab6e309f4d4b4439fc782fac00b4bbd74": {Code: "TIBET-wmilliETH-XCH", Name: "TibetSwap LP wmilliETH-XCH"}, + "c40c46a1b7238c4a7b68ce3f5967281759349aa340e948e2009ef146628a8a73": {Code: "HG", Name: "Hugo"}, + "c4258ed0e051d553322b66ae51a14d06ec39b06d6437bb6821d4fe22d4930f32": {Code: "LC", Name: "Little Caesar"}, + "c4eda4d9f48a5f6b929a32311b3234b6e4216e932c0eb394a6a5f219b1199132": {Code: "TIBET-MED-XCH", Name: "TibetSwap LP MED-XCH"}, + "c4f87b1666b8974907e3a8f3cdfcfc32b0dd820324d7c3302405373e999681e0": {Code: "MMT", Name: "Marvelous Marmot Ticket"}, + "c5741ee5b3dd911f86c52952797c6a6f4c6024f7c0027f1c65ec89af01d8ed30": {Code: "NKDE", Name: "Naked Emperor"}, + "c62e20ed883b79d852cba3da7bbff06542101f8b7888c3aa47ca52439fb55a79": {Code: "TIBET-BBT-XCH", Name: "TibetSwap LP BBT-XCH"}, + "c64a132765edae47493c401d53b5e17971bbec5cf92e4054785f7709a4e2f118": {Code: "TIBET-\u00A2ni-XCH", Name: "TibetSwap LP \u00A2ni-XCH"}, + "c65dceecf197da46fa92edd71d22b4bbbe27e52b86bdb3fc67e635aa75a7484e": {Code: "BOBO", Name: "bobo"}, + "c695c50f19e13b0fa1b07a1513a1d1cc39fb64da31eb1ed513addc1636fcb64e": {Code: "ZIG", Name: "Ziggy"}, + "c6be668127b983686af02ceb26486ead5144c958c9f450b273c4549f209a4660": {Code: "BTC", Name: "BitTorrentChia"}, + "c7047a88e2f9700e4a322d69abf029208f182f7cc08a9e246121289c6f8f6391": {Code: "TIBET-PFP-XCH", Name: "TibetSwap LP PFP-XCH"}, + "c81ce4f5875ec8a898ed325a2ea472b9cb06fd5093d99926685a8cbd124a2a78": {Code: "FLXB", Name: "Felix Blue"}, + "c85c0d96a746965f16f41a2ef83283d4a526ad2387198d977e03568e2a3a4ce0": {Code: "SNAP", Name: "Seedsnapp"}, + "c8d663305435ad2ebaf8a13872d780e826cfb189a892ba1fcd8b3d5cab52e34d": {Code: "TIBET-\U0001F36A-XCH", Name: "TibetSwap LP \U0001F36A-XCH"}, + "c9923b86c696f35a116f2376c812a58ca3c835e68ffc128a5a51a0021f0bee81": {Code: "TIBET-\U0001F954-XCH", Name: "TibetSwap LP \U0001F954-XCH"}, + "c9bc89cd1366db4f831f7876dd8cdc2f5c589b1dc7ef0bd448f69b650be7ad67": {Code: "ARA", Name: "from Africa"}, + "ca17ac895f0b560b1bd0db6f794afb98c29a2cfe5c421a7370ea01b664e0416b": {Code: "HEJI", Name: "Hedgie Token"}, + "ca7e396f76b0e7acee31bdce6ac70ca714fc9366f48f471e081c97443f4e456e": {Code: "TIBET-XCHC-XCH", Name: "TibetSwap LP XCHC-XCH"}, + "ca861009baf2c825482504f359c9fc399a0a0af5ad678398eeab92551821d203": {Code: "OFAB", Name: "Outer Fields | Booster"}, + "cad26bfe4f41d76b6f2de417d91e4e4090c6e7ceafe519a818c1f1033a6d6ef1": {Code: "ROCKY", Name: "ROCKY STABLE TOKEN"}, + "cb21f3625d7b4e8f843dda59b4b7866862f16c376077134cc8735e2b3a29c011": {Code: "WAR", Name: "WAR"}, + "cbc75ea1c0840e55fa948d7bcf256f368ea8047c7eb9390fd790eebfcfac0cf4": {Code: "CHILLGUY", Name: "CHILL GUY"}, + "ccbb3643aef0282727e9a3f3d6d6a7e1c7ebdf4a2b7f18372b19558e18277121": {Code: "GG", Name: "Goblin Gold"}, + "ccda69ff6c44d687994efdbee30689be51d2347f739287ab4bb7b52344f8bf1d": {Code: "BEPE", Name: "BEPE"}, + "cd4d292b5969aa607667de786766b45d0ebd47a9e20d028fa6cf52ab67d6649d": {Code: "\U0001F990", Name: "Shrimps"}, + "cd87e746371697552d2d70e88b7b25a1be761417b434e33d11071b1a26055384": {Code: "TIBET-VAL-XCH", Name: "TibetSwap LP VAL-XCH"}, + "cda8a68918dc62497cf37d3115a7e13b6c8691ea5849ec12203b1836278a74be": {Code: "TIBET-RAT-XCH", Name: "TibetSwap LP RAT-XCH"}, + "cdd97d5bb1aa1e069590d29a61278fd0e659dcf35fbc11e8f3f70ed4659a640c": {Code: "GIFT", Name: "GIFT Token"}, + "ce1a5e11a731bba3e7b4cd35c3eb060d18c0e4381f81f743eabe7b8d72818728": {Code: "TIBET-ZOMB-XCH", Name: "TibetSwap LP ZOMB-XCH"}, + "ce3be095a5d9f22cf286f94ef7a95cfa4afd83267c0d76f9f0e19fc0171d15f1": {Code: "BIGD", Name: "Proof of Big DID"}, + "ce3e3a641461be16571944ac597a179fa0ed75fbd0ed48ec1e45cdc8eb0943d7": {Code: "XMM8", Name: "Million Minus 8"}, + "ce80d0a2d478cbc090cbe82460315d0eabfae610a15035ea055d93d7a9dbabaa": {Code: "TIBET-CHEEZE-XCH", Name: "TibetSwap LP CHEEZE-XCH"}, + "ce9dfce94a5a65c0207bf3d57b407db559e9cd34f2050f8bd61291b42bead361": {Code: "ALFOOD", Name: "Abandoned Land - Food"}, + "cf03376948a71102deb621b7e22aa41a63ec4607053e691c7f347c8093589af4": {Code: "O2", Name: "Oxygen"}, + "cf62db954959f58f6db511e928d8c23cd7561676023bbbdbd790bfc7b7626b0f": {Code: "LBSS", Name: "LAMBOSS COIN"}, + "cf988b566e3fd52e7dac329e29063e681c477772518173aab78f28f7ec012365": {Code: "TIBET-TRZ-XCH", Name: "TibetSwap LP TRZ-XCH"}, + "cfc6764d1c7fba23efbc79124b3f174682a1c854a803479496901ca35bcead6b": {Code: "GMF", Name: "Good Morning Fuckers"}, + "d00472f8a4f38efdc41e2f594a9bd6543a1c32a58c98210fbc921817046efd2e": {Code: "TWC", Name: "TWC"}, + "d060556eb72208cfa0f78394cbe431dabfbba95752dcbca34cbb7d8848c12b09": {Code: "WOW", Name: "WOWATCH"}, + "d0b6098e494db061bf0c28a5c7d94de6c06de23dddd705b29d7857c420b123a2": {Code: "ROCKS", Name: "ROCKS"}, + "d11680ca2eae38854d3940a3ad59d1018bc762b8acbccdcaa4496e2887ca227b": {Code: "SBC", Name: "Spaceballs: The Coloring Book"}, + "d12fbf63bb015fa0e988509b971ad4c9da7cc5fc30f2499d3aab38c3fadc531c": {Code: "DAT", Name: "DAT GOVERNANCE TOKEN"}, + "d1adf97f603cdec4998a63eb8ffdd19480a60e20751c8ec8386283b1d86bf3f9": {Code: "MOG", Name: "MOG"}, + "d1b161790df06e2339f9e1b7c550919731a0bc009fccbc87898ae12c494df652": {Code: "TIBET-ALFOOD-XCH", Name: "TibetSwap LP ALFOOD-XCH"}, + "d1f935cf4c42742fe60e9b10ad21d8f4805e888e1145b0a95e34a4b50b0770b8": {Code: "IND", Name: "Indigo"}, + "d2039f75557be47a2679bd870949d834c943208e2a33cc798fc8bb6d6b95af48": {Code: "OKRA", Name: "OKRA"}, + "d29bf3015b40da2d61dd4e706d320df0fbff57abf07709e100437d2fb31e0e4e": {Code: "IYKYK", Name: "Word of Mouff"}, + "d332339b36c8ddd329a5e471a279c4f689395723c8d73ae81537fae5937184fd": {Code: "BUNCH", Name: "APULIA GRAPES"}, + "d3647f946788b0c29b07d8fbb23d99f1accf1cf6f8f0b2840c85105439d30e63": {Code: "THW", Name: "THW"}, + "d36ed081c23d30e9bacd77a0cfdca5a6b40624115c83d306c6444a32cbcab3b9": {Code: "WAR3", Name: "Web War 3"}, + "d3c8d7cb8a60dfc2c335e7c9b9ffa4ced8a85ca9fb11a86a86fafa125eeffd01": {Code: "What's", Name: "?"}, + "d43b348a62185b6e31743d56a5ebf58bd8171f5ab879479bb0fe419b9501c565": {Code: "TIBET-TIBET-BEPE-XCH-XCH", Name: "TibetSwap LP TIBET-BEPE-XCH-XCH"}, + "d44a36523c9163452968d7ce1d20b29a23085a4896aa0065a7fab979fe186cdc": {Code: "TIBET-XIPM-XCH", Name: "TibetSwap LP XIPM-XCH"}, + "d4f1f6d29e6392c477b84dc65a4379eaba570d29f7930895d89148ab5ed4b343": {Code: "SPACE", Name: "SPACE"}, + "d5cfbfadf873554d5184ebcb9dee86e69f8b11f44a0cc4e417871c4f7fb17b9a": {Code: "UFCG", Name: "UFC Genesis"}, + "d5d153d346f835cd2284306e783e193ac598dc56309a7b76b90a76cbe3c2ad56": {Code: "G$b", Name: "Gene$baby"}, + "d67bc228bf7a0b910af43b12102637bb0364bd8601daf2028ae2565a5348769a": {Code: "TIBET-BNNCN-XCH", Name: "TibetSwap LP BNNCN-XCH"}, + "d67bf92b0da177775466cb5c5b282befda772e965022612cd0be12aaeee16c76": {Code: "TIBET-DPLY6-XCH", Name: "TibetSwap LP DPLY6-XCH"}, + "d6e7f6439bbd7b5481999540b6fc8edf32d7aeb9b7d531ad59aa189fce26ae4f": {Code: "CHIANKA", Name: "Chianka"}, + "d6faa42c5a554c9323f0b1de604f021d966c5ffec251adf97907f312279c1d9f": {Code: "TIBET-\U0001F408-XCH", Name: "TibetSwap LP \U0001F408-XCH"}, + "d728cba6b5c5d5aeea91019803c6156722a91d79d3e945c74fc0d29a98379c02": {Code: "HOCK$", Name: "Hockey sticks"}, + "d73a503b68c8fb52bd917eef1056de0a92269f4b9eb553db56b435e690584b1d": {Code: "MAGE", Name: "Magecoin"}, + "d76af742cbe799c03793c547ab535c1b776a41e19cf56adcf41d74a244a417fc": {Code: "DXTS", Name: "DELTAXTRADES"}, + "d773501c5a28b57d465d6b36fdd7562766460950f745ce88b3a611101d6227ce": {Code: "USPP", Name: "Unidentified Space Phenom"}, + "d78fe90f1eea43a5be7b103f7dcb479e25ccdc50f09c5ef219214c8f27fa3983": {Code: "TIBET-FDB-XCH", Name: "TibetSwap LP FDB-XCH"}, + "d7f5081c76d58c3ccf42566bf15b960784f1a686468c58eedd3fa764ef0ada45": {Code: "NIO5", Name: "NioStake5"}, + "d810d81a2562900641ae21798289ec5b5b3c7341fc0029c3d6ef4a486abde97c": {Code: "STRAIN", Name: "The Strain"}, + "d8e69370dd1282969eb20ff4bcc6854f59653e1a043828aaec5005aad1b9290d": {Code: "TIBET-BLOOM-XCH", Name: "TibetSwap LP BLOOM-XCH"}, + "d8f60c8f89cb78a817e2077d1443f9c962904939da42b9a02fc06b69e44ce0db": {Code: "GS", Name: "Goblin Silver"}, + "d92268a1df8aa91aa410fcfaaa57583562bdfe9bacb458453f3518c4a3256124": {Code: "CHEEZE", Name: "CheeZe for SpaceRatZNFT"}, + "d93a7f55dfb95139ab5b4d0bed200eafcc85ce4901c60fb3f04397e9daa70f37": {Code: "OXC", Name: "Oxygen Cash"}, + "d96a2cee1aec7d05b493292adfd6475896747334a9cb4c2da5e71de7566717ec": {Code: "TIBET-\U0001F4A7-XCH", Name: "TibetSwap LP \U0001F4A7-XCH"}, + "d9af1d253eb6bd7dca97389d9c985ed66900bea563afc2e0d2ddb66afc61796a": {Code: "%", Name: "1%"}, + "d9fc39c18b9f302ea4b3746535df4eaf613339910f5cf9e1de71af0b6b82222e": {Code: "\U0001F980", Name: "Crabs"}, + "da13224bb2805034b192fd79b29f925087d1beef87fbfa3515978d0812542dc8": {Code: "TIBET-LBSS-XCH", Name: "TibetSwap LP LBSS-XCH"}, + "daf1390497969e647289b9f596248c0b1f6d91a7ece190a8954f28cd694d5f77": {Code: "STAR", Name: "Star Coin"}, + "db043fe4cd0f9f178e46774492c0969c7110ac355fcc580dd710c7fdf97881df": {Code: "CNI", Name: "CNI Stock"}, + "db1a9020d48d9d4ad22631b66ab4b9ebd3637ef7758ad38881348c5d24c38f20": {Code: "DBX", Name: "dexie bucks"}, + "db45c586aa57f72023e1755f9ed84ae5628de27cdcf7f1b0a4a5238bf603f120": {Code: "TIBET-TIBET-\U0001F331-XCH-XCH", Name: "TibetSwap LP TIBET-\U0001F331-XCH-XCH"}, + "db55c0aafaa830a0ba4cbbd1d07211b291f925fe4a41580c6da087145c0dcb31": {Code: "TIBET-MZ-XCH", Name: "TibetSwap LP MZ-XCH"}, + "db7454c93c26b84a147bb011b2252a7f66a9e24549b507604c842ed8089bf106": {Code: "TIBET-MBX-XCH", Name: "TibetSwap LP MBX-XCH"}, + "dbc10b12861b0ba902bb14a8feeb75df6bf014a0ef14716d6242b9ada3a34de3": {Code: "MOJO", Name: "DBC Art Coin"}, + "dbce1ddf7a9294a3f96ff3bd27d3221d5b494f09a1353a1229d98ab19f4b839c": {Code: "TIBET-ROCKY-XCH", Name: "TibetSwap LP ROCKY-XCH"}, + "dbec36ff11f1141598f5f9eabe3073cd2ddf13091f75b2f8079999e34166a16b": {Code: "TIBET-WTF2-XCH", Name: "TibetSwap LP WTF2-XCH"}, + "dc5c2bde27d1f73f655b360eceab68150b28c335d81b4fc1a1069ee6441ef886": {Code: "GENE", Name: "Gene"}, + "dc77ba1f5599fa63c126b8e512573eacd3496b3e4f6712d1252b119f1e6bec2d": {Code: "FHW", Name: "FHW"}, + "dc80c49bebc4981ec4b548b5ee3f1299efd93ce2f6924653926a906aab95a7cc": {Code: "TIBET-SBC-XCH", Name: "TibetSwap LP SBC-XCH"}, + "dd06261ba264dd38c2b601ec06694a3fe1ab0fbc83683d3700e1ecc457c962bb": {Code: "TIBET-INCL3-XCH", Name: "TibetSwap LP INCL3-XCH"}, + "dd1fc964af97877cc992cccac1a50f5173043e559c0c0998256874902c11d94c": {Code: "NORM!", Name: "NORM! coin"}, + "dd21263fd013000774dcb76db32a4a3b57e3c4760c42769bf286589f23d34efb": {Code: "tremp", Name: "doland tremp"}, + "dd37f678dda586fad9b1daeae1f7c5c137ffa6d947e1ed5c7b4f3c430da80638": {Code: "\U0001F355", Name: "PIZZA \U0001F355"}, + "de1bf7ecfb1a64b01c84bfec3578b7eaf7dd68e006fdbd4785c732743c468507": {Code: "TIBET-JOCK-XCH", Name: "TibetSwap LP JOCK-XCH"}, + "de6b7e0dec3b3b68779f9331973270f52a4172cd84f47f7742432ab85ca33458": {Code: "\U0001F954", Name: "POTATO"}, + "de85d93ed91d70a0327a793cfd8c155090a3c126ac78bde0e64909a890084b5e": {Code: "TIBET-KING-XCH", Name: "TibetSwap LP KING-XCH"}, + "dee835fbd92ea63fe8f192a6f94b80ae5bad7443284d1347c73143cd25a94293": {Code: "CNAR", Name: "CNAR Token"}, + "dee920435cdaffc0a71e229ea2120e5544c2247945de3594a11f0e307ab1dcde": {Code: "TIBET-%-XCH", Name: "TibetSwap LP %-XCH"}, + "df33004689a08608af3c8dfee1c6b8c80f3c0a60688e80547fe6d378e3547914": {Code: "TIBET-OEN-XCH", Name: "TibetSwap LP OEN-XCH"}, + "df5178d61b8ccdf5539456fbe3f0cb86884bbe55502dbd457b868e2891a09fdd": {Code: "TIBET-XFUEL-XCH", Name: "TibetSwap LP XFUEL-XCH"}, + "dfb730b31aad9f0b00dd813bcf838579c29ecd9ba76c85ac53940335366c03ac": {Code: "VALP", Name: "Valentine Platinum"}, + "e0005928763a7253a9c443d76837bdfab312382fc47cab85dad00be23ae4e82f": {Code: "MBX", Name: "Moonbucks"}, + "e00ccfb0cf39f1e8814f0a50c940afae1e3c787c29527d5455c61bb49be1c8bb": {Code: "TIBET-WESO-XCH", Name: "TibetSwap LP WESO-XCH"}, + "e014d6ad9e56181aace636b348721aee6af8a37504de8aaab6efb3aee506afc2": {Code: "TAILS", Name: "Wasteland Tails"}, + "e0c16fb43426c722904575b96bc68d2f98e315a108058d08864ef812a92a1041": {Code: "TIBET-O2-XCH", Name: "TibetSwap LP O2-XCH"}, + "e0ed5a8e325a7dbadfefd760533afb37e89d17a3ffa05e681b1c4b897d5e86b7": {Code: "TRTG", Name: "Too Rich GOLD"}, + "e15ff5b5936d6397840f1394123a9fcf2df10b5630c069a319b6202476f91017": {Code: "HEM", Name: "Hem"}, + "e1a97afddabfb1a87fb7df4f90488f44028a57173b9340a6644ae659a7575831": {Code: "STRRO", Name: "STARRO FUN"}, + "e21dcf058e8f1d8b2bd145c6f4891b9b16967384acd1af0c76b110cf02372f81": {Code: "TIBET-XPIG-XCH", Name: "TibetSwap LP XPIG-XCH"}, + "e233f9c0ebc092f083aaacf6295402ed0a0bb1f9acb1b56500d8a4f5a5e4c957": {Code: "MWIF", Name: "Marmot Wif Hat"}, + "e257aca547a83020e537e87f8c83e9332d2c3adb729c052e6f04971317084327": {Code: "ECO.181.2020", Name: "Agricultural Reforestation Project 2020"}, + "e2edfac6692851c1a582f58835dcb086ee0d2251a6c3cd1b2550921bbac2961b": {Code: "HERB", Name: "\U0001F33F"}, + "e335003c6d59aaaabe27eeeaf8a7b1308765f6bc9492a0b16394f50dec6bdcb7": {Code: "\U0001F48E", Name: "HODL"}, + "e3a68cec0ec7826af09244c9dae144b3630ea0ba1c92691d95b1b833081b11e1": {Code: "ROSS", Name: "Ross Ulbricht"}, + "e448ef666aced6dd9ffaa8a8a033bc4e810e42ec1fce1f52bc665fbe678cba7c": {Code: "TIBET-CLN-XCH", Name: "TibetSwap LP CLN-XCH"}, + "e451f895ab2f499238ba60d0ee0e9a06a604def5c8f3307785959f6623d1fbbf": {Code: "PC", Name: "Parsec"}, + "e4546d755d65e22ad430d8183f08370e4f51c7e2bc9a6ddd9c147a6074e56ac7": {Code: "TIBET-SBF-XCH", Name: "TibetSwap LP SBF-XCH"}, + "e4bb9ea5f24009190b1b7bb43f415f48467385ff9ebf88713c64ca4434ceb815": {Code: "TIBET-\U0001FABC-XCH", Name: "TibetSwap LP \U0001FABC-XCH"}, + "e4fc4141d66eb8dc5deed94dac03bdccbad7fb4f98367e18a3b910e8703ced27": {Code: "TIBET-CNUT-XCH", Name: "TibetSwap LP CNUT-XCH"}, + "e515bcb9a2627680c6755c66cbbd8ee270844e9eab2cc6e8f7d92a72d16ae5c4": {Code: "TIBET-STR-XCH", Name: "TibetSwap LP STR-XCH"}, + "e590e3469100106d41170141260237adb2adb5b3110c316b2e10c13b3a5aceb8": {Code: "XCHC", Name: "Chia Classic"}, + "e5a8af7124c2737283838e6797b0f0a5293fc81aca1ffd2720f8506c23f2ad88": {Code: "LKY8", Name: "Lucky 8"}, + "e5ecc8322e6d7da456ba0fe592b5de4fd06a36b83d6e281d3b79140651b6392b": {Code: "\U0001F37A", Name: "Beer Tokens"}, + "e625dae1deedb12a93f360474abb6b398a4514a9e3061e638a58e63b87144816": {Code: "CPZ", Name: "Cyberpunk Zone"}, + "e69231cfa4d86dc7885642276dbc2f284c00ce9b989496b14908d9055281b00b": {Code: "LNR", Name: "LunaRise"}, + "e816ee18ce2337c4128449bc539fbbe2ecfdd2098c4e7cab4667e223c3bdc23d": {Code: "HOA", Name: "HOA COIN"}, + "e896692067d08edd0f1538a370b8d74a5a21b89c25837c3f03abb63b55fdb82f": {Code: "TIBET-BIGD-XCH", Name: "TibetSwap LP BIGD-XCH"}, + "e899dd146ce95e51ada8967d03e087746b2ce0859614c77defbe1f08801a548a": {Code: "TIBET-HDT-XCH", Name: "TibetSwap LP HDT-XCH"}, + "e8e94a2eea7e330f0207f274761ad15bd8c3066fcf7727b7458ce6f91ef9875b": {Code: "HOOVE", Name: "HooveDAO coin"}, + "e9afb5ce4f89074cf84a5f3b872055e479c397e5f0acc16e917903b9991479a2": {Code: "SPORK", Name: "Spork"}, + "ea3ace5525d6aaf6d921b66052afc67da11c820b676de91d61ae1a766c8ce615": {Code: "CRT", Name: "Circuit Token"}, + "ea3e045df9fe9a7844cbb56f500bd146354d9e6f8c970b2ff8f8842d77442e97": {Code: "RAD", Name: "RADICOLA CAPS"}, + "ea6da19358a08fc000002985c8d1ffffc687346aadf2e9a0d98e49f567abccba": {Code: "MAFFF", Name: "ITS JUST MAFFF"}, + "ea830317f831a23b178aa653e50484568d30d2c5b34d8140e71247ead05961c7": {Code: "CC", Name: "Caesar Coin"}, + "ea845547e14506ebfc38cb32359255f0085e912d2a6d6c616e4e42b1dfd2fa54": {Code: "TIBET-DUNK-XCH", Name: "TibetSwap LP DUNK-XCH"}, + "eaa8d4993e37490558f77bd5b18e9c6a88940a4be78115892833f22d76655d79": {Code: "TIBET-WESO-XCH", Name: "TibetSwap LP WESO-XCH"}, + "eaef46c78384488c9b0b1062e96e057ba16b9836c17960731d574b071196bb73": {Code: "C2R", Name: "CAT 2 RESERVE TOKEN"}, + "eb11cb4c71f73e4fedc7f74501b9ec4505bf960fe89924c4952d74a1a204eae1": {Code: "RATZ", Name: "Space RatZ NFT"}, + "eb2155a177b6060535dd8e72e98ddb0c77aea21fab53737de1c1ced3cb38e4c4": {Code: "\U0001FA84\u26A1\uFE0F", Name: "Spell Power"}, + "eb461b8af32975adbdf426d22e77e41547a7081a28663da61fd07fae2933c393": {Code: "GTH", Name: "GRAND THEFT HARDDRIVE"}, + "eb46a60914727af0ff616a6d8e1db1fe73daa2ff6c4261287c9170bd495408a4": {Code: "TIBET-NKDE-XCH", Name: "TibetSwap LP NKDE-XCH"}, + "ebbe8f25d3225d41f757785ff2cbcaf8fbf4f4442407b6b0c49b20f8f57f87c9": {Code: "CPC", Name: "Capy Coin"}, + "ebd153f3a00b159a0e251514866c86eafbb0e74a5304659f256a1517b7319573": {Code: "HBX", Name: "Hardi Bux"}, + "ec208bab726f6a2d9411d7330c3ea92f41c2bc9c832e1efdd1679bdcd175ba26": {Code: "SSP", Name: "SSP 1.9 scenario"}, + "ec25b77bc54df637392d6a0f542de65f45020405d0f36ced723bff2870c378b1": {Code: "MJO", Name: "MoJo\u2022JoJo"}, + "ec70426aebe423192cf8d2b90d11f776785b62e351a1e743da6ec8b2e11e86c8": {Code: "CGC", Name: "Ciger Coin"}, + "ec9d874e152e888231024c72e391fc484e8b6a1cf744430a322a0544e207bf46": {Code: "PEPE", Name: "PepeCoin"}, + "ecd6513e31f92bb5b9f8d87caa362e6a7f9dca8610506a8f57e4e5c283317f7b": {Code: "\u00C6", Name: "\u00C6THER"}, + "ed1594af7c0d4ba7685afbe328129481e885cb8adda3ef280f3f499e5015ced2": {Code: "SCC", Name: "Seasons Club Coin"}, + "ed687a33542ecc3262d366b3ecace6ec5ef91ab2e127464e374475b9a1e75353": {Code: "TIBET-DPLY4-XCH", Name: "TibetSwap LP DPLY4-XCH"}, + "edeb87eabd6b75d08f894df4ce1cb89a78d615208f31c572f464c8511d7cc15c": {Code: "ALExp", Name: "ALExp"}, + "ededc8a0a3210bc6b3f607a67855038ed05219a35577d30498c9762659792db8": {Code: "TIBET-MJO-XCH", Name: "TibetSwap LP MJO-XCH"}, + "ee00d3ccaac92cc0285c51fd63cd7b867117c69ed7a0d6a41b093633756d5c68": {Code: "BBC", Name: "BoBo Coin"}, + "ee495b3795eebe47c5cbf3cd6e47a2e418e6dc65ce493c626c0cc140fcd2dcf1": {Code: "GEN", Name: "Green Energy"}, + "ee6ad2b7b8643d7fe944f2d94f8a14d07e94c072b75c7b74c0ca5d0a9c2c1c94": {Code: "NFT", Name: "Need For Token 2.0"}, + "ee972a13138c48165b1b325189c74af9d195e6b44e721fc1e047663ed81614a2": {Code: "TIBET-ROSS-XCH", Name: "TibetSwap LP ROSS-XCH"}, + "eeaa101fecb72cc986a7441f6a601f4570f8de01c95033010567614adf53f18d": {Code: "\U0001F988", Name: "Sharks"}, + "eee3acd94cf44f8c545f2c40f5b6254d6cd610f535c707ddf3a25ea51820d04c": {Code: "TIBET-ACID-XCH", Name: "TibetSwap LP ACID-XCH"}, + "ef10958177a1944ba2dfb3486a7a805bf82a22f39b97617b3bbb6a30bb3f0e16": {Code: "TIBET-OKRA-XCH", Name: "TibetSwap LP OKRA-XCH"}, + "ef1e7124d4c37429e251768a4245dacc0ee3d0633aec1e8b0e059dd41583623f": {Code: "333", Name: "St. Michael"}, + "ef5517b5ef4ccff1344d971f83551857a6cc3c8ab1a0044f17b5cc5e9c291d2b": {Code: "TIBET-CNI-XCH", Name: "TibetSwap LP CNI-XCH"}, + "ef9df6c687ed9a325369f32ba38eb6dc83a1bd081e159a9b2dddfe6159e2ecf0": {Code: "WTF2", Name: "WTF2 att-adj-tok"}, + "efb8116946878cabd50804dba22a35344ef7d83fc227ee8aad4e2548b997fdde": {Code: "BLOCK", Name: "BLOCK"}, + "efd85235c35cda41c7d58fe0ffed4c54048540d130fe8e6cd645ec5b2aebb9f8": {Code: "BDOG", Name: "BallDog"}, + "efe7422b247b43107970bcffda4c88bf7ae9766984e67430ce969e5aa3f4fada": {Code: "EVG05", Name: "Batch 5 Token"}, + "f090423b386cad6d7d64376397a5503026dc63ea164349c9998b5e3734ea2dce": {Code: "MMW", Name: "MegaMeow"}, + "f0948e2f7eb00b92a4d682c27858ef6c067050af71715065a3bb16be1cabb4fd": {Code: "TIBET-SPELL-XCH", Name: "TibetSwap LP SPELL-XCH"}, + "f10d353591271593149ef18fb925d2e4cd5a4933d6ac969e95f4424c0a08e796": {Code: "TIBET-NBR-XCH", Name: "TibetSwap LP NBR-XCH"}, + "f10d9555b8f20eced3d51678f6651a311267226ee78e367c026c4ebaca43b437": {Code: "TIBET-WOJAK-XCH", Name: "TibetSwap LP WOJAK-XCH"}, + "f1381ce3175277be107b38e40f4078c125adebe4888f3ef74178d653af7b8a04": {Code: "TIBET-FHW-XCH", Name: "TibetSwap LP FHW-XCH"}, + "f1f44ff3da039ae26709ed636d6d91c4b2b2e34e8ab974bd5ec862e380fd266b": {Code: "FBX", Name: "Farmer Bucks"}, + "f2770d78cdc92f311613db98df81f5cdccd61317d1e838c35d64ef332b4d44da": {Code: "WHC", Name: "WishCoin"}, + "f296f60120d8f6693302846889b286b5fe44b63a3b6cb3e9adac367b2876e635": {Code: "TIBET-$MMC-XCH", Name: "TibetSwap LP $MMC-XCH"}, + "f3132ae74e11ffda309b22a204b7f106425420885ad812facda7b908d3a7bbf4": {Code: "TIBET-\U0001F331-XCH", Name: "TibetSwap LP \U0001F331-XCH"}, + "f322a205c034fe28681829fa5a2e483ac421f0952eb1292945c8db06e0a471a6": {Code: "wmilliETH.b", Name: "Base warp.green milliETH"}, + "f3f2b8a1d1480c1d7d241e0fc5e5e3b8a346cb861474fb0b55b512e6f26184e7": {Code: "CCFO", Name: "Compassion CAT - Founder"}, + "f471706d40b3d1938ebf2afd9afb3a8efcf1d6c7f3f68ba8b8c76371e220ee20": {Code: "NIOG", Name: "NioGold"}, + "f4741633837347659fcfa60664719d0fa765e912a62dadde8761f285c06b9298": {Code: "TIBET-XTECH-XCH", Name: "TibetSwap LP XTECH-XCH"}, + "f55fd6a4bc7339e31305e5c23444a7805920110a473cbb2777ff42013eee2cbf": {Code: "LETSGO", Name: "Let's Coin"}, + "f5fdae0ab035784c9aaf161ae96fb368b0ab9355834b4253153d8c31ec46ea65": {Code: "COAL", Name: "Coal"}, + "f6b332157bbd97455f5921fe9bf0fb6b8fd601cb7155545efcc0b21f83d5a393": {Code: "TIBET-\U0001F525-XCH", Name: "TibetSwap LP \U0001F525-XCH"}, + "f6ed38b5a1ced9147928868a4158eafc00543ae77663b3daee982f57c6e6bf37": {Code: "ASHEN", Name: "Ashendane"}, + "f74008e2cfa9c6ebf88a3c473c2603c53b17a3c48a365fbdc752430a07e3921b": {Code: "XMILK", Name: "XMILK"}, + "f7495302b0598cb5004377a364bef89fd17575a2c6a1653ac7a68cb304fb980f": {Code: "BCAP", Name: "Bottle cap"}, + "f79205d06f40dc66cb04bf9369c31a307d70e2fab5a39e4fdce6e475ab33b575": {Code: "TIBET-wUSDT-XCH", Name: "TibetSwap LP wUSDT-XCH"}, + "f7a975340fe224ed6e32467e4cf7ce544c9ddfc1079fc6d0e34e960dc64dbb90": {Code: "TIBET-DBX-XCH", Name: "TibetSwap LP DBX-XCH"}, + "f7ae5eb93781ac90632ce2429be9171b2f3615ac89f70e54a23a1386c910306a": {Code: "KING", Name: "Undead King"}, + "f7d33a036caf9a111a68213d3f2307b67f09f016d8b132e37df96d250d716df4": {Code: "CKPO", Name: "HollyCoin"}, + "f7f44a509faa1c354b8f78f1aef1b79bea31fc3c387ed36934039bc9de023054": {Code: "LSD", Name: "Liquidity Supercharged Dollar"}, + "f83fdaa9749d6f1daa64f2390aab461078b6e16091ecb653808084b761de683c": {Code: "TIBET-MAFFF-XCH", Name: "TibetSwap LP MAFFF-XCH"}, + "f8c63d8a23b3311ca520a8157a8341b6488d112ba61a7aa5b4a39f024d5d5be9": {Code: "TIBET-DUCK-XCH", Name: "TibetSwap LP DUCK-XCH"}, + "f8faf36214e61104d708c8807bcd433ec937ddd18eda81e998471e666f6ef939": {Code: "TIBET-TIBET-\u2728\u2764\uFE0F\u200D\U0001F525\U0001F9D9\u200D\u2642\uFE0F-XCH-XCH", Name: "TibetSwap LP TIBET-\u2728\u2764\uFE0F\u200D\U0001F525\U0001F9D9\u200D\u2642\uFE0F-XCH-XCH"}, + "f930c6c02e8bb72e3d06eabb61483fbd4bbbc41906336e4db49c81affa51df81": {Code: "GFY", Name: "Go Fuck Yourself"}, + "f9529c59c0cfba364671c927a60a23c3cab9dc5e2b1488ba0c7b858e99a8ae2a": {Code: "KATS", Name: "KATS 2.0"}, + "f9fea96774793dedb3e5f2c99497055a2e395e3161f99db0ec8956f1d308cbeb": {Code: "THC", Name: "Tokenized Hyper Coin"}, + "fa18661ee38d938858bf2d1ecf2c8ad5cbed972e09ff6c55f62293d0eaedd1ae": {Code: "TIME", Name: "Time"}, + "fa27338829703d9c195f775b28fce529f35dd185cd9df97eaba860c489ac3991": {Code: "OFT", Name: "OnlyFansToken"}, + "fa4a180ac326e67ea289b869e3448256f6af05721f7cf934cb9901baa6b7a99d": {Code: "wUSDC.b", Name: "Base warp.green USDC"}, + "fa614f0197ec6e0ed0e16dd691bf7c5a797e370e6f16783e3b171adc1f2170a3": {Code: "TIBET-\U0001F990-XCH", Name: "TibetSwap LP \U0001F990-XCH"}, + "fa78ff512fda2a53840cf125eb5db2db1ada9e9d47a6b78128ef99a37b406078": {Code: "LT", Name: "Leaf Token"}, + "fab2f11b14c3bdfa549508494921c6518595ed0425f0a308eb40072ccd92eb9a": {Code: "BGCN", Name: "Board Game Crypto NFT"}, + "faf495ebed941eb57823669196cb47d8030b0c2114abfba46ecea3c3a0b48d0d": {Code: "HTRAT", Name: "Hearty Rations"}, + "fb52035b9e2f64d54210e56b2d3ef0940d5203c1bf25a7a055d1f01e6e13f72d": {Code: "GANG", Name: "Gang"}, + "fbe2694f6fbc63a4d9c8a598dc70f8b6fdf468e8e595ca4c6efee5a2f1ec71a4": {Code: "TIBET-SBX-XCH", Name: "TibetSwap LP SBX-XCH"}, + "fc55fd71d19ff358ab33f668ad7150740b5b932c953abdf531916106e942f600": {Code: "TIBET-ALWOOD-XCH", Name: "TibetSwap LP ALWOOD-XCH"}, + "fc7bfd37a13bf524552a7e3c9f60fcfff8c557b1859dd59f71206396f989681e": {Code: "RATZ2", Name: "Space RatZ NFT GEN2"}, + "fd702a16b14ebb47ca4c3a8bceb25dac68e6fad2257ad4b47fdc0f148659dda8": {Code: "TIBET-NIOG-XCH", Name: "TibetSwap LP NIOG-XCH"}, + "fda5aa3b13fa50288101c68f67a7cb3a92949ee0eecab122d1e980e74c5bd99b": {Code: "KITCAT", Name: "KitCat 2.0"}, + "fde7d2b53ce72c40f75f625f32ff6b8caac62d2a02f7c3bce7c50c4e133861dc": {Code: "KMD", Name: "Komodo"}, + "fe65842a8b4f20c212360e22ad3da51d3b30020b29fd0a7b395ae60fcc6f6321": {Code: "CRU", Name: "Collectible Resource Unit"}, + "fe789863dd6ee42cd54f31a7ecb42dbb37554ccac0a9d03de5d67a534ddbe7af": {Code: "NINJA", Name: "NINJA"}, + "fec8226dae85a86da1c81b6567625542acd8a6fea6c68452cf73f31ed93844e4": {Code: "TIBET-SMOON-XCH", Name: "TibetSwap LP SMOON-XCH"}, + "ff3cde048d8d3d38c207a3bedcc7d57a3af47c33573e3749a43b68a91d5b8022": {Code: "TIBET-DPR-XCH", Name: "TibetSwap LP DPR-XCH"}, +} + +func LookupCatByID(id string) (CatMetadata, bool) { + metadata, ok := catLookupByID[normalizeCatAssetID(id)] + return metadata, ok +} + +func LookupCatNameCodeByID(id string) (name string, code string, ok bool) { + metadata, ok := LookupCatByID(id) + if !ok { + return "", "", false + } + return metadata.Name, metadata.Code, true +} + +func normalizeCatAssetID(id string) string { + id = strings.TrimSpace(id) + id = strings.TrimPrefix(id, "0x") + id = strings.TrimPrefix(id, "0X") + return strings.ToLower(id) +} diff --git a/internal/cmd/coinset/get_block_transactions.go b/internal/cmd/coinset/get_block_transactions.go index f2f90b4..1e3553b 100644 --- a/internal/cmd/coinset/get_block_transactions.go +++ b/internal/cmd/coinset/get_block_transactions.go @@ -11,12 +11,14 @@ var ( getBlockTransactionsCursor string getBlockTransactionsLimit int getBlockTransactionsOrder string + getBlockTransactionsPretty bool ) func init() { getBlockTransactionsCmd.Flags().StringVar(&getBlockTransactionsCursor, "cursor", "", "Pagination cursor from a previous response") getBlockTransactionsCmd.Flags().IntVar(&getBlockTransactionsLimit, "limit", 0, "Maximum number of records to return") getBlockTransactionsCmd.Flags().StringVar(&getBlockTransactionsOrder, "order", "", `Result order: "asc" or "desc"`) + getBlockTransactionsCmd.Flags().BoolVarP(&getBlockTransactionsPretty, "pretty", "p", false, "display formatted transaction output") rootCmd.AddCommand(getBlockTransactionsCmd) } @@ -43,6 +45,6 @@ var getBlockTransactionsCmd = &cobra.Command{ } _ = applyPaginationOptions(jsonData, getBlockTransactionsCursor, getBlockTransactionsLimit, getBlockTransactionsOrder) - makeRequest("get_block_transactions", jsonData) + makeTransactionRequest("get_block_transactions", jsonData, getBlockTransactionsPretty) }, } diff --git a/internal/cmd/coinset/get_pending_transactions_by_p2.go b/internal/cmd/coinset/get_pending_transactions_by_p2.go index 91be0c8..e1f6463 100644 --- a/internal/cmd/coinset/get_pending_transactions_by_p2.go +++ b/internal/cmd/coinset/get_pending_transactions_by_p2.go @@ -6,12 +6,14 @@ var ( getPendingTransactionsByP2Cursor string getPendingTransactionsByP2Limit int getPendingTransactionsByP2Order string + getPendingTransactionsByP2Pretty bool ) func init() { getPendingTransactionsByP2Cmd.Flags().StringVar(&getPendingTransactionsByP2Cursor, "cursor", "", "Pagination cursor from a previous response") getPendingTransactionsByP2Cmd.Flags().IntVar(&getPendingTransactionsByP2Limit, "limit", 0, "Maximum number of records to return") getPendingTransactionsByP2Cmd.Flags().StringVar(&getPendingTransactionsByP2Order, "order", "", `Result order: "asc" or "desc"`) + getPendingTransactionsByP2Cmd.Flags().BoolVarP(&getPendingTransactionsByP2Pretty, "pretty", "p", false, "display formatted transaction output") rootCmd.AddCommand(getPendingTransactionsByP2Cmd) } @@ -33,6 +35,6 @@ var getPendingTransactionsByP2Cmd = &cobra.Command{ "p2": p2, } _ = applyPaginationOptions(jsonData, getPendingTransactionsByP2Cursor, getPendingTransactionsByP2Limit, getPendingTransactionsByP2Order) - makeRequest("get_pending_transactions_by_p2", jsonData) + makeTransactionRequest("get_pending_transactions_by_p2", jsonData, getPendingTransactionsByP2Pretty) }, } diff --git a/internal/cmd/coinset/get_pending_transactions_by_puzzle_hash.go b/internal/cmd/coinset/get_pending_transactions_by_puzzle_hash.go index 05a4b1b..8213bf9 100644 --- a/internal/cmd/coinset/get_pending_transactions_by_puzzle_hash.go +++ b/internal/cmd/coinset/get_pending_transactions_by_puzzle_hash.go @@ -6,12 +6,14 @@ var ( getPendingTransactionsByPuzzleHashCursor string getPendingTransactionsByPuzzleHashLimit int getPendingTransactionsByPuzzleHashOrder string + getPendingTransactionsByPuzzleHashPretty bool ) func init() { getPendingTransactionsByPuzzleHashCmd.Flags().StringVar(&getPendingTransactionsByPuzzleHashCursor, "cursor", "", "Pagination cursor from a previous response") getPendingTransactionsByPuzzleHashCmd.Flags().IntVar(&getPendingTransactionsByPuzzleHashLimit, "limit", 0, "Maximum number of records to return") getPendingTransactionsByPuzzleHashCmd.Flags().StringVar(&getPendingTransactionsByPuzzleHashOrder, "order", "", `Result order: "asc" or "desc"`) + getPendingTransactionsByPuzzleHashCmd.Flags().BoolVarP(&getPendingTransactionsByPuzzleHashPretty, "pretty", "p", false, "display formatted transaction output") rootCmd.AddCommand(getPendingTransactionsByPuzzleHashCmd) } @@ -33,6 +35,6 @@ var getPendingTransactionsByPuzzleHashCmd = &cobra.Command{ "p2": p2, } _ = applyPaginationOptions(jsonData, getPendingTransactionsByPuzzleHashCursor, getPendingTransactionsByPuzzleHashLimit, getPendingTransactionsByPuzzleHashOrder) - makeRequest("get_pending_transactions_by_puzzle_hash", jsonData) + makeTransactionRequest("get_pending_transactions_by_puzzle_hash", jsonData, getPendingTransactionsByPuzzleHashPretty) }, } diff --git a/internal/cmd/coinset/get_transaction.go b/internal/cmd/coinset/get_transaction.go index 64aa509..5d0b1bb 100644 --- a/internal/cmd/coinset/get_transaction.go +++ b/internal/cmd/coinset/get_transaction.go @@ -2,7 +2,10 @@ package cmd import "github.com/spf13/cobra" +var getTransactionPretty bool + func init() { + getTransactionCmd.Flags().BoolVarP(&getTransactionPretty, "pretty", "p", false, "display formatted transaction output") rootCmd.AddCommand(getTransactionCmd) } @@ -18,8 +21,8 @@ var getTransactionCmd = &cobra.Command{ }, Run: func(cmd *cobra.Command, args []string) { txID, _ := normalizeHexArg(args[0], "tx_id") - makeRequest("get_transaction", map[string]interface{}{ + makeTransactionRequest("get_transaction", map[string]interface{}{ "tx_id": txID, - }) + }, getTransactionPretty) }, } diff --git a/internal/cmd/coinset/get_transactions_by_cat_asset_id.go b/internal/cmd/coinset/get_transactions_by_cat_asset_id.go index 1411680..7a2508a 100644 --- a/internal/cmd/coinset/get_transactions_by_cat_asset_id.go +++ b/internal/cmd/coinset/get_transactions_by_cat_asset_id.go @@ -6,12 +6,14 @@ var ( getTransactionsByCatAssetIDCursor string getTransactionsByCatAssetIDLimit int getTransactionsByCatAssetIDOrder string + getTransactionsByCatAssetIDPretty bool ) func init() { getTransactionsByCatAssetIDCmd.Flags().StringVar(&getTransactionsByCatAssetIDCursor, "cursor", "", "Pagination cursor from a previous response") getTransactionsByCatAssetIDCmd.Flags().IntVar(&getTransactionsByCatAssetIDLimit, "limit", 0, "Maximum number of records to return") getTransactionsByCatAssetIDCmd.Flags().StringVar(&getTransactionsByCatAssetIDOrder, "order", "", `Result order: "asc" or "desc"`) + getTransactionsByCatAssetIDCmd.Flags().BoolVarP(&getTransactionsByCatAssetIDPretty, "pretty", "p", false, "display formatted transaction output") rootCmd.AddCommand(getTransactionsByCatAssetIDCmd) } @@ -33,6 +35,6 @@ var getTransactionsByCatAssetIDCmd = &cobra.Command{ "asset_id": assetID, } _ = applyPaginationOptions(jsonData, getTransactionsByCatAssetIDCursor, getTransactionsByCatAssetIDLimit, getTransactionsByCatAssetIDOrder) - makeRequest("get_transactions_by_cat_asset_id", jsonData) + makeTransactionRequest("get_transactions_by_cat_asset_id", jsonData, getTransactionsByCatAssetIDPretty) }, } diff --git a/internal/cmd/coinset/get_transactions_by_coin_name.go b/internal/cmd/coinset/get_transactions_by_coin_name.go index d4129c6..49cfdf4 100644 --- a/internal/cmd/coinset/get_transactions_by_coin_name.go +++ b/internal/cmd/coinset/get_transactions_by_coin_name.go @@ -2,10 +2,14 @@ package cmd import "github.com/spf13/cobra" -var getTransactionsByCoinNameIncludeTransactions bool +var ( + getTransactionsByCoinNameIncludeTransactions bool + getTransactionsByCoinNamePretty bool +) func init() { getTransactionsByCoinNameCmd.Flags().BoolVar(&getTransactionsByCoinNameIncludeTransactions, "include-transactions", false, "Include created/spent transaction payloads when available") + getTransactionsByCoinNameCmd.Flags().BoolVarP(&getTransactionsByCoinNamePretty, "pretty", "p", false, "display formatted transaction output") rootCmd.AddCommand(getTransactionsByCoinNameCmd) } @@ -27,6 +31,6 @@ var getTransactionsByCoinNameCmd = &cobra.Command{ if getTransactionsByCoinNameIncludeTransactions { jsonData["include_transactions"] = true } - makeRequest("get_transactions_by_coin_name", jsonData) + makeTransactionRequest("get_transactions_by_coin_name", jsonData, getTransactionsByCoinNamePretty) }, } diff --git a/internal/cmd/coinset/get_transactions_by_nft_id.go b/internal/cmd/coinset/get_transactions_by_nft_id.go index b4e1b65..52a33d3 100644 --- a/internal/cmd/coinset/get_transactions_by_nft_id.go +++ b/internal/cmd/coinset/get_transactions_by_nft_id.go @@ -10,12 +10,14 @@ var ( getTransactionsByNftIDCursor string getTransactionsByNftIDLimit int getTransactionsByNftIDOrder string + getTransactionsByNftIDPretty bool ) func init() { getTransactionsByNftIDCmd.Flags().StringVar(&getTransactionsByNftIDCursor, "cursor", "", "Pagination cursor from a previous response") getTransactionsByNftIDCmd.Flags().IntVar(&getTransactionsByNftIDLimit, "limit", 0, "Maximum number of records to return") getTransactionsByNftIDCmd.Flags().StringVar(&getTransactionsByNftIDOrder, "order", "", `Result order: "asc" or "desc"`) + getTransactionsByNftIDCmd.Flags().BoolVarP(&getTransactionsByNftIDPretty, "pretty", "p", false, "display formatted transaction output") rootCmd.AddCommand(getTransactionsByNftIDCmd) } @@ -36,6 +38,6 @@ var getTransactionsByNftIDCmd = &cobra.Command{ "nft_id": normalizeMaybeHex(args[0]), } _ = applyPaginationOptions(jsonData, getTransactionsByNftIDCursor, getTransactionsByNftIDLimit, getTransactionsByNftIDOrder) - makeRequest("get_transactions_by_nft_id", jsonData) + makeTransactionRequest("get_transactions_by_nft_id", jsonData, getTransactionsByNftIDPretty) }, } diff --git a/internal/cmd/coinset/get_transactions_by_p2.go b/internal/cmd/coinset/get_transactions_by_p2.go index f43c51f..2feb621 100644 --- a/internal/cmd/coinset/get_transactions_by_p2.go +++ b/internal/cmd/coinset/get_transactions_by_p2.go @@ -6,12 +6,14 @@ var ( getTransactionsByP2Cursor string getTransactionsByP2Limit int getTransactionsByP2Order string + getTransactionsByP2Pretty bool ) func init() { getTransactionsByP2Cmd.Flags().StringVar(&getTransactionsByP2Cursor, "cursor", "", "Pagination cursor from a previous response") getTransactionsByP2Cmd.Flags().IntVar(&getTransactionsByP2Limit, "limit", 0, "Maximum number of records to return") getTransactionsByP2Cmd.Flags().StringVar(&getTransactionsByP2Order, "order", "", `Result order: "asc" or "desc"`) + getTransactionsByP2Cmd.Flags().BoolVarP(&getTransactionsByP2Pretty, "pretty", "p", false, "display formatted transaction output") rootCmd.AddCommand(getTransactionsByP2Cmd) } @@ -33,6 +35,6 @@ var getTransactionsByP2Cmd = &cobra.Command{ "p2": p2, } _ = applyPaginationOptions(jsonData, getTransactionsByP2Cursor, getTransactionsByP2Limit, getTransactionsByP2Order) - makeRequest("get_transactions_by_p2", jsonData) + makeTransactionRequest("get_transactions_by_p2", jsonData, getTransactionsByP2Pretty) }, } diff --git a/internal/cmd/coinset/get_transactions_by_puzzle_hash.go b/internal/cmd/coinset/get_transactions_by_puzzle_hash.go index b02e446..4537b9c 100644 --- a/internal/cmd/coinset/get_transactions_by_puzzle_hash.go +++ b/internal/cmd/coinset/get_transactions_by_puzzle_hash.go @@ -6,12 +6,14 @@ var ( getTransactionsByPuzzleHashCursor string getTransactionsByPuzzleHashLimit int getTransactionsByPuzzleHashOrder string + getTransactionsByPuzzleHashPretty bool ) func init() { getTransactionsByPuzzleHashCmd.Flags().StringVar(&getTransactionsByPuzzleHashCursor, "cursor", "", "Pagination cursor from a previous response") getTransactionsByPuzzleHashCmd.Flags().IntVar(&getTransactionsByPuzzleHashLimit, "limit", 0, "Maximum number of records to return") getTransactionsByPuzzleHashCmd.Flags().StringVar(&getTransactionsByPuzzleHashOrder, "order", "", `Result order: "asc" or "desc"`) + getTransactionsByPuzzleHashCmd.Flags().BoolVarP(&getTransactionsByPuzzleHashPretty, "pretty", "p", false, "display formatted transaction output") rootCmd.AddCommand(getTransactionsByPuzzleHashCmd) } @@ -33,6 +35,6 @@ var getTransactionsByPuzzleHashCmd = &cobra.Command{ "p2": p2, } _ = applyPaginationOptions(jsonData, getTransactionsByPuzzleHashCursor, getTransactionsByPuzzleHashLimit, getTransactionsByPuzzleHashOrder) - makeRequest("get_transactions_by_puzzle_hash", jsonData) + makeTransactionRequest("get_transactions_by_puzzle_hash", jsonData, getTransactionsByPuzzleHashPretty) }, } diff --git a/internal/cmd/coinset/transaction_pretty.go b/internal/cmd/coinset/transaction_pretty.go new file mode 100644 index 0000000..7b76949 --- /dev/null +++ b/internal/cmd/coinset/transaction_pretty.go @@ -0,0 +1,1509 @@ +package cmd + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "log" + "math/big" + "os" + "regexp" + "strings" + "time" + "unicode/utf8" + + "github.com/chia-network/go-chia-libs/pkg/bech32m" + "github.com/chia-network/go-chia-libs/pkg/types" +) + +type transactionSummaryRecord struct { + ID string `json:"id"` + Status string `json:"status"` + FeeMojos string `json:"fee_mojos"` + Cost *int64 `json:"cost"` + FirstSeenMS *int64 `json:"first_seen_ms"` + ConfirmedAt *int64 `json:"confirmed_at_ms"` + LastUpdated int64 `json:"last_updated_ms"` + Events []map[string]interface{} `json:"events"` + Source string `json:"source"` + SchemaNumber int `json:"schema_version"` +} + +type transactionByIDEnvelope struct { + Transaction *transactionSummaryRecord `json:"transaction"` + Success bool `json:"success"` +} + +type transactionListEnvelope struct { + Transactions []transactionSummaryRecord `json:"transactions"` + Truncated *bool `json:"truncated"` + NextCursor string `json:"next_cursor"` + Success bool `json:"success"` +} + +type transactionsByCoinNameEnvelope struct { + CoinName string `json:"coin_name"` + CreatedInTxID *string `json:"created_in_tx_id"` + SpentInTxID *string `json:"spent_in_tx_id"` + CreatedTransaction *transactionSummaryRecord `json:"created_transaction"` + SpentTransaction *transactionSummaryRecord `json:"spent_transaction"` + Success bool `json:"success"` +} + +type ansiStyler struct { + enabled bool +} + +var ( + mojosPerXCH = big.NewInt(1000000000000) + mojosToSixDivisor = big.NewInt(1000000) + sixDigitsMax = big.NewInt(1000000) + roundingOffset = big.NewInt(500000) +) + +const defaultIOLimit = 8 + +var ansiEscapeRegexp = regexp.MustCompile("\x1b\\[[0-9;]*m") + +func makeTransactionRequest(path string, jsonData map[string]interface{}, pretty bool) { + if !pretty { + makeRequest(path, jsonData) + return + } + + if jq != "." || raw || describe { + log.Fatal("--pretty cannot be combined with --query, --raw, or --describe") + } + + jsonResponse, err := doRpc(path, jsonData) + if err != nil { + log.Fatal(err.Error()) + } + + if err := printPrettyTransactionResponse(jsonResponse); err != nil { + log.Printf("pretty format unavailable (%v), falling back to JSON output", err) + printJson(jsonResponse) + } +} + +func printPrettyTransactionResponse(jsonBytes []byte) error { + var envelope map[string]json.RawMessage + if err := json.Unmarshal(jsonBytes, &envelope); err != nil { + return err + } + + if !responseSucceeded(envelope) { + printJson(jsonBytes) + return nil + } + + styler := newANSIStyler() + + if _, ok := envelope["transaction"]; ok { + var payload transactionByIDEnvelope + if err := json.Unmarshal(jsonBytes, &payload); err != nil { + return err + } + if payload.Transaction == nil { + fmt.Println("No transaction found.") + return nil + } + fmt.Println(formatTransactionRecord(*payload.Transaction, styler)) + return nil + } + + if _, ok := envelope["transactions"]; ok { + var payload transactionListEnvelope + if err := json.Unmarshal(jsonBytes, &payload); err != nil { + return err + } + printTransactionList(payload, styler) + return nil + } + + if _, hasCreated := envelope["created_transaction"]; hasCreated { + var payload transactionsByCoinNameEnvelope + if err := json.Unmarshal(jsonBytes, &payload); err != nil { + return err + } + printTransactionsByCoinName(payload, styler) + return nil + } + + return fmt.Errorf("unsupported transaction response shape") +} + +func responseSucceeded(envelope map[string]json.RawMessage) bool { + rawSuccess, ok := envelope["success"] + if !ok { + return true + } + var success bool + if err := json.Unmarshal(rawSuccess, &success); err != nil { + return true + } + return success +} + +func printTransactionList(payload transactionListEnvelope, styler ansiStyler) { + if len(payload.Transactions) == 0 { + fmt.Println("No transactions.") + return + } + + for idx, tx := range payload.Transactions { + if idx > 0 { + fmt.Println() + } + fmt.Println(formatTransactionRecord(tx, styler)) + } + + if payload.Truncated != nil && *payload.Truncated { + fmt.Printf("\nMore results available. Next cursor: %s\n", payload.NextCursor) + } +} + +func printTransactionsByCoinName(payload transactionsByCoinNameEnvelope, styler ansiStyler) { + fmt.Printf("%s %s\n", styler.bold("Coin"), shortenHexIdentifier(payload.CoinName, 10, 8)) + + if payload.CreatedInTxID != nil && strings.TrimSpace(*payload.CreatedInTxID) != "" { + fmt.Printf("%s %s\n", styler.dim("created_in_tx"), shortenHexIdentifier(*payload.CreatedInTxID, 10, 8)) + } + if payload.SpentInTxID != nil && strings.TrimSpace(*payload.SpentInTxID) != "" { + fmt.Printf("%s %s\n", styler.dim("spent_in_tx"), shortenHexIdentifier(*payload.SpentInTxID, 10, 8)) + } + + if payload.CreatedTransaction != nil { + fmt.Printf("\n%s\n", styler.bold("Created Transaction")) + fmt.Println(formatTransactionRecord(*payload.CreatedTransaction, styler)) + } + if payload.SpentTransaction != nil { + fmt.Printf("\n%s\n", styler.bold("Spent Transaction")) + fmt.Println(formatTransactionRecord(*payload.SpentTransaction, styler)) + } +} + +func formatTransactionRecord(tx transactionSummaryRecord, styler ansiStyler) string { + var out strings.Builder + + txID := shortenHexIdentifier(tx.ID, 10, 8) + status := normalizeTxStatus(tx.Status) + cost := "—" + if tx.Cost != nil { + cost = formatSignedStringWithCommas(fmt.Sprintf("%d", *tx.Cost)) + } + + fee := fmt.Sprintf("%s XCH", formatMojosAsXCH(tx.FeeMojos)) + age := formatAge(txTimestampMs(tx)) + + fmt.Fprintf( + &out, + "%s %s %s %s %s %s %s %s %s\n", + styler.bold("TX"), + styler.bold(txID), + styler.color(status, statusColorCode(status)), + styler.dim("fee"), + fee, + styler.dim("cost"), + cost, + styler.dim("age"), + age, + ) + + out.WriteString(styler.bold("Activity")) + out.WriteByte('\n') + if len(tx.Events) == 0 { + out.WriteString(" (no events)") + return out.String() + } + + for i, event := range tx.Events { + eventType := getStringFromMap(event, "type") + headline := formatEventHeadline(event) + if headline == "" { + headline = titleCase(eventType) + } + + chips := formatEventChips(event, styler) + bar := styler.color("|", eventColorCode(eventType)) + + out.WriteString(bar) + out.WriteByte(' ') + out.WriteString(headline) + if chips != "" { + out.WriteString(" ") + out.WriteString(chips) + } + + details := formatEventDetails(event, styler) + if details != "" { + out.WriteByte('\n') + out.WriteString(details) + } + + if i != len(tx.Events)-1 { + out.WriteString("\n\n") + } + } + + return out.String() +} + +func formatEventHeadline(event map[string]interface{}) string { + eventType := strings.ToLower(getStringFromMap(event, "type")) + + switch eventType { + case "transfer": + return formatTransferHeadline(event) + case "swap": + return formatSwapHeadline(event) + case "mint": + return formatMintHeadline(event) + case "melt": + return formatMeltHeadline(event) + case "pool": + return formatPoolHeadline(event) + case "revoke": + return formatRevokeHeadline("Revoke", event) + case "clawback": + return formatRevokeHeadline("Clawback", event) + case "combine": + return formatReshapeHeadline("Combine", event) + case "split": + return formatReshapeHeadline("Split", event) + default: + return fallbackHeadline(titleCase(getStringFromMap(event, "type")), event) + } +} + +func formatTransferHeadline(event map[string]interface{}) string { + participants := getSliceOfMaps(event, "participants") + if len(participants) == 0 { + return fallbackHeadline("Transfer", event) + } + + var sender map[string]interface{} + var receiver map[string]interface{} + + for _, participant := range participants { + sent := hasAnyAmount(getMapFromMap(participant, "sent")) + received := hasAnyAmount(getMapFromMap(participant, "received")) + if sender == nil && sent && !received { + sender = participant + } + if receiver == nil && received && !sent { + receiver = participant + } + } + if sender == nil { + sender = participants[0] + } + if receiver == nil { + for _, participant := range participants { + if getStringFromMap(participant, "p2") != getStringFromMap(sender, "p2") { + receiver = participant + break + } + } + } + + amount := formatAssetAmountsCompact(getMapFromMap(sender, "sent")) + if amount == "" && receiver != nil { + amount = formatAssetAmountsCompact(getMapFromMap(receiver, "received")) + } + + if amount == "" { + return fallbackHeadline("Transfer", event) + } + if receiver == nil { + return fmt.Sprintf("Transfer · %s", amount) + } + + return fmt.Sprintf( + "Transfer · %s · %s -> %s", + amount, + shortP2(getStringFromMap(sender, "p2")), + shortP2(getStringFromMap(receiver, "p2")), + ) +} + +func formatSwapHeadline(event map[string]interface{}) string { + legs := getSliceOfMaps(event, "legs") + if len(legs) == 0 { + return fallbackHeadline("Swap", event) + } + + sent := formatAssetAmountsCompact(getMapFromMap(legs[0], "sent")) + received := formatAssetAmountsCompact(getMapFromMap(legs[0], "received")) + + if sent == "" || received == "" { + return fallbackHeadline("Swap", event) + } + return fmt.Sprintf("Swap · %s <-> %s", sent, received) +} + +func formatMintHeadline(event map[string]interface{}) string { + minted := getMapFromMap(event, "minted") + label := formatMintedAsset(minted) + if label == "" { + return fallbackHeadline("Mint", event) + } + return fmt.Sprintf("Mint · %s", label) +} + +func formatMeltHeadline(event map[string]interface{}) string { + melted := getMapFromMap(event, "melted") + label := formatMintedAsset(melted) + if label == "" { + return fallbackHeadline("Melt", event) + } + return fmt.Sprintf("Melt · %s", label) +} + +func formatPoolHeadline(event map[string]interface{}) string { + action := getMapFromMap(event, "action") + actionName := getStringFromMap(action, "action") + if actionName == "" { + return fallbackHeadline("Pool", event) + } + return fmt.Sprintf("Pool · %s", titleCase(actionName)) +} + +func formatRevokeHeadline(kind string, event map[string]interface{}) string { + asset := getMapFromMap(event, "asset") + assetType := strings.ToLower(getStringFromMap(asset, "asset_type")) + assetID := getStringFromMap(asset, "asset_id") + amount := getStringFromMap(event, "amount") + label := formatAmountByAssetType(assetType, assetID, amount) + if label == "" { + return fallbackHeadline(kind, event) + } + return fmt.Sprintf("%s · %s", kind, label) +} + +func formatReshapeHeadline(kind string, event map[string]interface{}) string { + inCount := intFromAny(event["input_coin_count"]) + outCount := intFromAny(event["output_coin_count"]) + base := fmt.Sprintf("%s · %d -> %d", kind, inCount, outCount) + + asset := getMapFromMap(event, "asset") + assetType := strings.ToLower(getStringFromMap(asset, "asset_type")) + assetID := getStringFromMap(asset, "asset_id") + amount := getStringFromMap(event, "output_amount") + if amount == "" { + amount = getStringFromMap(event, "input_amount") + } + label := formatAmountByAssetType(assetType, assetID, amount) + if label == "" { + return base + } + return fmt.Sprintf("%s · %s", base, label) +} + +func fallbackHeadline(prefix string, event map[string]interface{}) string { + inputs := len(getSliceOfMaps(event, "inputs")) + outputs := len(getSliceOfMaps(event, "outputs")) + if inputs == 0 && outputs == 0 { + return prefix + } + return fmt.Sprintf("%s · %d -> %d", prefix, inputs, outputs) +} + +func formatMintedAsset(asset map[string]interface{}) string { + assetType := strings.ToLower(getStringFromMap(asset, "asset_type")) + assetID := getStringFromMap(asset, "asset_id") + amount := getStringFromMap(asset, "amount") + switch assetType { + case "nft": + return "1 NFT" + case "cat": + return formatAmountByAssetType("cat", assetID, amount) + default: + return "" + } +} + +func formatAssetAmountsCompact(assetAmounts map[string]interface{}) string { + xchAmount := getStringFromMap(assetAmounts, "xch") + cats := getSliceOfMaps(assetAmounts, "cats") + nfts := getSlice(assetAmounts, "nfts") + + if isNonZeroAmount(xchAmount) { + primary := fmt.Sprintf("%s XCH", formatMojosAsXCH(xchAmount)) + extras := []string{} + if len(cats) > 0 { + extras = append(extras, formatCatExtrasSummary(cats)) + } + if len(nfts) > 0 { + if len(nfts) == 1 { + extras = append(extras, "1 NFT") + } else { + extras = append(extras, fmt.Sprintf("%d NFTs", len(nfts))) + } + } + if len(extras) == 0 { + return primary + } + return fmt.Sprintf("%s + %s", primary, strings.Join(extras, " + ")) + } + + if len(cats) > 0 { + primary := formatCatAmountCompact(cats[0]) + remaining := len(cats) - 1 + if remaining > 0 { + primary = fmt.Sprintf("%s +%d assets", primary, remaining) + } + if len(nfts) > 0 { + primary = fmt.Sprintf("%s + %d NFT", primary, len(nfts)) + if len(nfts) > 1 { + primary = fmt.Sprintf("%ss", primary) + } + } + return primary + } + + if len(nfts) > 0 { + if len(nfts) == 1 { + return "1 NFT" + } + return fmt.Sprintf("%d NFTs", len(nfts)) + } + + return "" +} + +func formatAmountByAssetType(assetType string, assetID string, amount string) string { + switch strings.ToLower(assetType) { + case "xch": + if amount == "" { + return "" + } + return fmt.Sprintf("%s XCH", formatMojosAsXCH(amount)) + case "cat": + return formatCatAmount(amount, assetID) + case "nft": + if amount == "" || amount == "1" { + return "1 NFT" + } + return fmt.Sprintf("%s NFTs", formatSignedStringWithCommas(amount)) + default: + if amount == "" { + return "" + } + return formatSignedStringWithCommas(amount) + } +} + +func formatCatExtrasSummary(cats []map[string]interface{}) string { + if len(cats) == 0 { + return "" + } + + primary := formatCatAmountCompact(cats[0]) + if len(cats) == 1 { + return primary + } + + return fmt.Sprintf("%s +%d assets", primary, len(cats)-1) +} + +func formatCatAmountCompact(cat map[string]interface{}) string { + return formatCatAmount(getStringFromMap(cat, "amount"), getStringFromMap(cat, "asset_id")) +} + +func formatCatAmount(amount string, assetID string) string { + label := catDisplayLabel(assetID) + if amount == "" { + return label + } + return fmt.Sprintf("%s %s", formatSignedStringWithCommas(amount), label) +} + +func catDisplayLabel(assetID string) string { + _, code, ok := LookupCatNameCodeByID(assetID) + if ok && strings.TrimSpace(code) != "" { + return colorCatLabel(code, true) + } + + normalized := normalizeCatAssetID(assetID) + if normalized == "" { + return colorCatLabel("CAT", false) + } + if len(normalized) >= 8 { + return colorCatLabel(fmt.Sprintf("CAT(%s...)", normalized[:8]), false) + } + return colorCatLabel(fmt.Sprintf("CAT(%s)", normalized), false) +} + +func colorFlowAmount(value string, isSent bool, styler ansiStyler) string { + if value == "" || value == "—" { + return value + } + if isSent { + return styler.color(value, "31") + } + return styler.color(value, "32") +} + +func styleIOHeader(value string, isInput bool, styler ansiStyler) string { + _ = isInput + return styler.bold(value) +} + +func colorCatLabel(value string, known bool) string { + styler := newANSIStyler() + if known { + return styler.color(value, "36") + } + return styler.color(value, "33") +} + +func formatEventChips(event map[string]interface{}, styler ansiStyler) string { + chips := []string{} + seen := map[string]bool{} + addChip := func(chip string) { + chip = strings.TrimSpace(chip) + if chip == "" || seen[chip] || len(chips) >= 3 { + return + } + seen[chip] = true + chips = append(chips, chip) + } + + eventType := strings.ToLower(getStringFromMap(event, "type")) + amm := getMapFromMap(event, "amm") + if protocol := getStringFromMap(amm, "protocol"); protocol != "" { + addChip(titleCase(protocol)) + } else if eventType == "swap" { + addChip("Offer") + } + + if len(getSliceOfMaps(event, "royalties")) > 0 { + addChip("Royalties") + } + if eventHasClawback(event) { + addChip("Clawback") + } + if len(getSlice(event, "memos")) > 0 { + addChip("Memos") + } + + if len(chips) == 0 { + return "" + } + + parts := make([]string, 0, len(chips)) + for _, chip := range chips { + parts = append(parts, styler.dim("["+chip+"]")) + } + return strings.Join(parts, " ") +} + +func formatEventDetails(event map[string]interface{}, styler ansiStyler) string { + eventType := strings.ToLower(getStringFromMap(event, "type")) + sections := []string{} + + switch eventType { + case "transfer": + if section := formatParticipantsSection("Participants", getSliceOfMaps(event, "participants"), "p2", styler); section != "" { + sections = append(sections, section) + } + case "swap": + if section := formatParticipantsSection("Legs", getSliceOfMaps(event, "legs"), "p2", styler); section != "" { + sections = append(sections, section) + } + if section := formatRoyaltiesSection(getSliceOfMaps(event, "royalties"), styler); section != "" { + sections = append(sections, section) + } + if section := formatAmmSection(getMapFromMap(event, "amm"), styler); section != "" { + sections = append(sections, section) + } + case "mint": + if section := formatMintSection(event, styler); section != "" { + sections = append(sections, section) + } + case "melt": + if section := formatMeltSection(event, styler); section != "" { + sections = append(sections, section) + } + case "revoke", "clawback": + if section := formatRevokeSection(event, styler); section != "" { + sections = append(sections, section) + } + case "combine", "split": + if section := formatReshapeSection(event, styler); section != "" { + sections = append(sections, section) + } + case "pool": + if section := formatPoolSection(event, styler); section != "" { + sections = append(sections, section) + } + } + + if section := formatIOTableSection(event, styler); section != "" { + sections = append(sections, section) + } + if section := formatMemosSection(event, styler); section != "" { + sections = append(sections, section) + } + if section := formatConstraintsSection(event, styler); section != "" { + sections = append(sections, section) + } + + return strings.Join(sections, "\n") +} + +func formatParticipantsSection(title string, entries []map[string]interface{}, addrKey string, styler ansiStyler) string { + if len(entries) == 0 { + return "" + } + + type participantRow struct { + addr string + sent string + received string + } + + rows := make([]participantRow, 0, len(entries)) + maxPrefixWidth := 0 + + for _, entry := range entries { + addr := shortP2(getStringFromMap(entry, addrKey)) + sent := formatAssetAmountsCompact(getMapFromMap(entry, "sent")) + received := formatAssetAmountsCompact(getMapFromMap(entry, "received")) + if sent == "" { + sent = "—" + } + if received == "" { + received = "—" + } + sent = colorFlowAmount(sent, true, styler) + received = colorFlowAmount(received, false, styler) + + prefix := fmt.Sprintf("%s sent: %s", addr, sent) + if width := displayWidth(prefix); width > maxPrefixWidth { + maxPrefixWidth = width + } + + rows = append(rows, participantRow{ + addr: addr, + sent: sent, + received: received, + }) + } + + lines := make([]string, 0, len(rows)) + for _, row := range rows { + prefix := fmt.Sprintf("%s sent: %s", row.addr, row.sent) + pad := maxPrefixWidth - displayWidth(prefix) + if pad < 0 { + pad = 0 + } + lines = append(lines, fmt.Sprintf("%s%s received: %s", prefix, strings.Repeat(" ", pad), row.received)) + } + + return formatSection(title, lines, styler) +} + +func formatRoyaltiesSection(royalties []map[string]interface{}, styler ansiStyler) string { + if len(royalties) == 0 { + return "" + } + + lines := make([]string, 0, len(royalties)) + for _, royalty := range royalties { + addr := shortP2(getStringFromMap(royalty, "recipient_p2")) + amount := formatAssetAmountsCompact(getMapFromMap(royalty, "amount")) + if amount == "" { + amount = "—" + } + lines = append(lines, fmt.Sprintf("%s %s", addr, amount)) + } + + return formatSection("Royalties", lines, styler) +} + +func formatAmmSection(amm map[string]interface{}, styler ansiStyler) string { + if len(amm) == 0 { + return "" + } + + lines := []string{} + if protocol := getStringFromMap(amm, "protocol"); protocol != "" { + lines = append(lines, "protocol: "+titleCase(protocol)) + } + if pair := getStringFromMap(amm, "pair"); pair != "" { + lines = append(lines, "pair: "+shortenIdentifier(pair, 14, 10)) + } + + return formatSection("AMM", lines, styler) +} + +func formatMintSection(event map[string]interface{}, styler ansiStyler) string { + lines := []string{} + + if recipient := getStringFromMap(event, "recipient_p2"); recipient != "" { + lines = append(lines, "recipient: "+shortP2(recipient)) + } + if minter := getStringFromMap(event, "minter_p2"); minter != "" { + lines = append(lines, "minter: "+shortP2(minter)) + } + + minted := formatMintedAsset(getMapFromMap(event, "minted")) + if minted != "" { + lines = append(lines, "asset: "+minted) + } + + return formatSection("Mint", lines, styler) +} + +func formatMeltSection(event map[string]interface{}, styler ansiStyler) string { + lines := []string{} + + if owner := getStringFromMap(event, "owner_p2"); owner != "" { + lines = append(lines, "owner: "+shortP2(owner)) + } + + melted := formatMintedAsset(getMapFromMap(event, "melted")) + if melted != "" { + lines = append(lines, "asset: "+melted) + } + + return formatSection("Melt", lines, styler) +} + +func formatRevokeSection(event map[string]interface{}, styler ansiStyler) string { + lines := []string{} + + if by := getStringFromMap(event, "by_p2"); by != "" { + lines = append(lines, "by: "+shortP2(by)) + } + if from := getStringFromMap(event, "from_p2"); from != "" { + lines = append(lines, "from: "+shortP2(from)) + } + if to := getStringFromMap(event, "to_p2"); to != "" { + lines = append(lines, "to: "+shortP2(to)) + } + + asset := getMapFromMap(event, "asset") + assetType := getStringFromMap(asset, "asset_type") + assetID := getStringFromMap(asset, "asset_id") + amount := formatAmountByAssetType(assetType, assetID, getStringFromMap(event, "amount")) + if amount != "" { + lines = append(lines, "amount: "+amount) + } + + if mechanism := getStringFromMap(event, "mechanism"); mechanism != "" { + lines = append(lines, "mechanism: "+titleCase(mechanism)) + } + if confidence := getStringFromMap(event, "confidence"); confidence != "" { + lines = append(lines, "confidence: "+titleCase(confidence)) + } + + kind := titleCase(getStringFromMap(event, "type")) + if kind == "" { + kind = "Revoke" + } + return formatSection(kind, lines, styler) +} + +func formatReshapeSection(event map[string]interface{}, styler ansiStyler) string { + lines := []string{} + + if p2 := getStringFromMap(event, "p2"); p2 != "" { + lines = append(lines, "p2: "+shortP2(p2)) + } + + inCount := intFromAny(event["input_coin_count"]) + outCount := intFromAny(event["output_coin_count"]) + lines = append(lines, fmt.Sprintf("coins: %d -> %d", inCount, outCount)) + + asset := getMapFromMap(event, "asset") + assetType := getStringFromMap(asset, "asset_type") + assetID := getStringFromMap(asset, "asset_id") + amount := getStringFromMap(event, "output_amount") + if amount == "" { + amount = getStringFromMap(event, "input_amount") + } + if label := formatAmountByAssetType(assetType, assetID, amount); label != "" { + lines = append(lines, "amount: "+label) + } + + kind := titleCase(getStringFromMap(event, "type")) + if kind == "" { + kind = "Reshape" + } + return formatSection(kind, lines, styler) +} + +func formatPoolSection(event map[string]interface{}, styler ansiStyler) string { + lines := []string{} + + if launcher := getStringFromMap(event, "launcher_id"); launcher != "" { + lines = append(lines, "launcher: "+shortenHexIdentifier(launcher, 10, 8)) + } + if owner := getStringFromMap(event, "owner_p2"); owner != "" { + lines = append(lines, "owner: "+shortP2(owner)) + } + + action := getMapFromMap(event, "action") + if actionName := getStringFromMap(action, "action"); actionName != "" { + lines = append(lines, "action: "+titleCase(actionName)) + } + + return formatSection("Pool", lines, styler) +} + +func formatIOTableSection(event map[string]interface{}, styler ansiStyler) string { + inputs := getSliceOfMaps(event, "inputs") + outputs := getSliceOfMaps(event, "outputs") + if len(inputs) == 0 && len(outputs) == 0 { + return "" + } + + missingRow := "- —" + + showInputs := minInt(len(inputs), defaultIOLimit) + showOutputs := minInt(len(outputs), defaultIOLimit) + + leftHeader := fmt.Sprintf("Inputs (%d/%d)", showInputs, len(inputs)) + rightHeader := fmt.Sprintf("Outputs (%d/%d)", showOutputs, len(outputs)) + + leftRows := make([]string, 0, showInputs) + for i := 0; i < showInputs; i++ { + leftRows = append(leftRows, formatCoinRow(inputs[i])) + } + + rightRows := make([]string, 0, showOutputs) + for i := 0; i < showOutputs; i++ { + rightRows = append(rightRows, formatCoinRow(outputs[i])) + } + + leftWidth := displayWidth(leftHeader) + for _, row := range leftRows { + if rowWidth := displayWidth(row); rowWidth > leftWidth { + leftWidth = rowWidth + } + } + if rowWidth := displayWidth(missingRow); rowWidth > leftWidth { + leftWidth = rowWidth + } + + var out strings.Builder + out.WriteString(" ") + out.WriteString(styleIOHeader(leftHeader, true, styler)) + if pad := leftWidth - displayWidth(leftHeader); pad > 0 { + out.WriteString(strings.Repeat(" ", pad)) + } + out.WriteString(" ") + out.WriteString(styleIOHeader(rightHeader, false, styler)) + + rowCount := maxInt(showInputs, showOutputs) + for i := 0; i < rowCount; i++ { + left := missingRow + right := missingRow + if i < len(leftRows) { + left = leftRows[i] + } + if i < len(rightRows) { + right = rightRows[i] + } + + out.WriteByte('\n') + out.WriteString(" ") + out.WriteString(left) + if pad := leftWidth - displayWidth(left); pad > 0 { + out.WriteString(strings.Repeat(" ", pad)) + } + out.WriteString(" ") + out.WriteString(right) + } + + moreInputs := len(inputs) - showInputs + moreOutputs := len(outputs) - showOutputs + if moreInputs > 0 || moreOutputs > 0 { + hints := []string{} + if moreInputs > 0 { + hints = append(hints, fmt.Sprintf("+%d more inputs", moreInputs)) + } + if moreOutputs > 0 { + hints = append(hints, fmt.Sprintf("+%d more outputs", moreOutputs)) + } + + out.WriteByte('\n') + out.WriteString(" ... ") + out.WriteString(strings.Join(hints, ", ")) + } + + return out.String() +} + +func formatCoinRow(coin map[string]interface{}) string { + coinID := shortenHexIdentifier(getStringFromMap(coin, "coin_id"), 10, 8) + amount := formatCoinAmountLabel(coin) + if amount == "" { + amount = "—" + } + return fmt.Sprintf("- %s %s", coinID, amount) +} + +func formatCoinAmountLabel(coin map[string]interface{}) string { + coinType := getMapFromMap(coin, "coin_type") + + custodyType := strings.ToLower(getStringFromMap(coinType, "custody_puzzle_type")) + if custodyType == "vault" { + return "1 Vault" + } + + outerType := strings.ToLower(getStringFromMap(coinType, "outer_puzzle_type")) + assetID := getStringFromMap(coinType, "asset_id") + + switch outerType { + case "cat": + return formatCatAmount(getStringFromMap(coin, "amount"), assetID) + case "nft", "singleton": + amount := getStringFromMap(coin, "amount") + if amount == "" || amount == "1" { + return "1 NFT" + } + return fmt.Sprintf("%s NFTs", formatSignedStringWithCommas(amount)) + } + + amount := getStringFromMap(coin, "amount") + if amount == "" { + return "" + } + return fmt.Sprintf("%s XCH", formatMojosAsXCH(amount)) +} + +func formatMemosSection(event map[string]interface{}, styler ansiStyler) string { + memos := getSlice(event, "memos") + if len(memos) == 0 { + return "" + } + + lines := make([]string, 0, len(memos)) + for _, memo := range memos { + lines = append(lines, fmt.Sprintf("%v", memo)) + } + + return formatSection(fmt.Sprintf("Memos (%d)", len(memos)), lines, styler) +} + +func formatConstraintsSection(event map[string]interface{}, styler ansiStyler) string { + constraints := getMapFromMap(event, "constraints") + if len(constraints) == 0 { + return "" + } + + pretty, err := json.MarshalIndent(constraints, "", " ") + if err != nil { + return formatSection("Constraints", []string{fmt.Sprintf("%v", constraints)}, styler) + } + + var out strings.Builder + out.WriteString(" ") + out.WriteString(styler.bold("Constraints")) + for _, line := range strings.Split(string(pretty), "\n") { + out.WriteByte('\n') + out.WriteString(" ") + out.WriteString(line) + } + return out.String() +} + +func formatSection(title string, lines []string, styler ansiStyler) string { + if len(lines) == 0 { + return "" + } + + var out strings.Builder + out.WriteString(" ") + out.WriteString(styler.bold(title)) + for _, line := range lines { + out.WriteByte('\n') + out.WriteString(" - ") + out.WriteString(line) + } + return out.String() +} + +func eventHasClawback(event map[string]interface{}) bool { + if strings.EqualFold(getStringFromMap(event, "type"), "Clawback") { + return true + } + + inputs := getSliceOfMaps(event, "inputs") + for _, input := range inputs { + if len(getMapFromMap(input, "clawback")) > 0 { + return true + } + } + + outputs := getSliceOfMaps(event, "outputs") + for _, output := range outputs { + if len(getMapFromMap(output, "clawback")) > 0 { + return true + } + } + + return false +} + +func txTimestampMs(tx transactionSummaryRecord) int64 { + if tx.ConfirmedAt != nil && *tx.ConfirmedAt > 0 { + return *tx.ConfirmedAt + } + if tx.FirstSeenMS != nil && *tx.FirstSeenMS > 0 { + return *tx.FirstSeenMS + } + if tx.LastUpdated > 0 { + return tx.LastUpdated + } + return 0 +} + +func formatAge(timestampMs int64) string { + if timestampMs <= 0 { + return "—" + } + + timestamp := time.UnixMilli(timestampMs).Local() + relative := formatRelativeDuration(time.Since(timestamp)) + absolute := timestamp.Format("2006-01-02 15:04:05") + return fmt.Sprintf("%s (%s)", relative, absolute) +} + +func formatRelativeDuration(duration time.Duration) string { + future := false + if duration < 0 { + future = true + duration = -duration + } + if duration < time.Second { + if future { + return "in 0s" + } + return "0s" + } + + seconds := int(duration.Seconds()) + days := seconds / 86400 + hours := (seconds % 86400) / 3600 + minutes := (seconds % 3600) / 60 + secs := seconds % 60 + + var value string + switch { + case days > 0: + value = fmt.Sprintf("%dd%dh", days, hours) + case hours > 0: + value = fmt.Sprintf("%dh%dm", hours, minutes) + case minutes > 0: + value = fmt.Sprintf("%dm%ds", minutes, secs) + default: + value = fmt.Sprintf("%ds", secs) + } + + if future { + return "in " + value + } + return value +} + +func normalizeTxStatus(status string) string { + switch strings.ToLower(strings.TrimSpace(status)) { + case "", "confirmed": + return "CONFIRMED" + case "pending", "mempool": + return "PENDING" + default: + return strings.ToUpper(status) + } +} + +func formatMojosAsXCH(mojos string) string { + mojoInt, ok := parseAmount(mojos) + if !ok { + return "0" + } + + sign := "" + if mojoInt.Sign() < 0 { + sign = "-" + } + + absMojos := new(big.Int).Abs(mojoInt) + whole := new(big.Int) + remainder := new(big.Int) + whole.QuoRem(absMojos, mojosPerXCH, remainder) + + if remainder.Sign() == 0 { + return sign + whole.String() + } + + roundedSix := new(big.Int).Add(remainder, roundingOffset) + roundedSix.Quo(roundedSix, mojosToSixDivisor) + roundedWhole := new(big.Int).Set(whole) + + if roundedSix.Cmp(sixDigitsMax) >= 0 { + roundedWhole.Add(roundedWhole, big.NewInt(1)) + roundedSix.SetInt64(0) + } + + if roundedSix.Sign() == 0 { + if whole.Sign() == 0 && remainder.Sign() > 0 { + return sign + "0." + leftPad(remainder.String(), 12) + } + return sign + roundedWhole.String() + } + + return sign + roundedWhole.String() + "." + leftPad(roundedSix.String(), 6) +} + +func parseAmount(value string) (*big.Int, bool) { + clean := strings.TrimSpace(value) + if clean == "" { + return nil, false + } + v := new(big.Int) + if _, ok := v.SetString(clean, 10); !ok { + return nil, false + } + return v, true +} + +func isNonZeroAmount(value string) bool { + parsed, ok := parseAmount(value) + return ok && parsed.Sign() != 0 +} + +func formatSignedStringWithCommas(value string) string { + parsed, ok := parseAmount(value) + if !ok { + return value + } + sign := "" + if parsed.Sign() < 0 { + sign = "-" + } + digits := new(big.Int).Abs(parsed).String() + return sign + addCommas(digits) +} + +func addCommas(digits string) string { + if len(digits) <= 3 { + return digits + } + + prefix := len(digits) % 3 + var b strings.Builder + if prefix > 0 { + b.WriteString(digits[:prefix]) + if len(digits) > prefix { + b.WriteByte(',') + } + } + + for i := prefix; i < len(digits); i += 3 { + b.WriteString(digits[i : i+3]) + if i+3 < len(digits) { + b.WriteByte(',') + } + } + + return b.String() +} + +func leftPad(value string, width int) string { + if len(value) >= width { + return value + } + return strings.Repeat("0", width-len(value)) + value +} + +func displayWidth(value string) int { + clean := ansiEscapeRegexp.ReplaceAllString(value, "") + return utf8.RuneCountInString(clean) +} + +func minInt(a int, b int) int { + if a < b { + return a + } + return b +} + +func maxInt(a int, b int) int { + if a > b { + return a + } + return b +} + +func shortP2(p2 string) string { + if p2 == "" { + return "—" + } + if strings.HasPrefix(p2, "xch") || strings.HasPrefix(p2, "txch") { + return shortenIdentifier(p2, 8, 6) + } + + if !isHex(p2) { + return shortenIdentifier(p2, 8, 6) + } + + normalized := formatHex(p2) + address, err := p2ToAddress(normalized) + if err == nil { + return shortenIdentifier(address, 8, 6) + } + return shortenHexIdentifier(normalized, 8, 6) +} + +func p2ToAddress(p2 string) (string, error) { + hexBytes, err := hex.DecodeString(strings.TrimPrefix(p2, "0x")) + if err != nil { + return "", err + } + hexBytes32, err := types.BytesToBytes32(hexBytes) + if err != nil { + return "", err + } + + prefix := "xch" + if testnet { + prefix = "txch" + } + + return bech32m.EncodePuzzleHash(hexBytes32, prefix) +} + +func shortenHexIdentifier(value string, left int, right int) string { + if strings.TrimSpace(value) == "" { + return "—" + } + + lower := strings.ToLower(strings.TrimSpace(value)) + if strings.HasPrefix(lower, "0x") { + lower = lower[2:] + } + + if !isHexDigits(lower) { + return shortenIdentifier(value, left, right) + } + + if len(lower) <= left+right { + return "0x" + lower + } + return "0x" + lower[:left] + "..." + lower[len(lower)-right:] +} + +func shortenIdentifier(value string, left int, right int) string { + clean := strings.TrimSpace(value) + if clean == "" { + return "—" + } + if len(clean) <= left+right { + return clean + } + return clean[:left] + "..." + clean[len(clean)-right:] +} + +func isHexDigits(value string) bool { + if value == "" { + return false + } + for _, r := range value { + isDigit := r >= '0' && r <= '9' + isLower := r >= 'a' && r <= 'f' + if !isDigit && !isLower { + return false + } + } + return true +} + +func hasAnyAmount(amounts map[string]interface{}) bool { + if isNonZeroAmount(getStringFromMap(amounts, "xch")) { + return true + } + cats := getSliceOfMaps(amounts, "cats") + for _, cat := range cats { + if isNonZeroAmount(getStringFromMap(cat, "amount")) { + return true + } + } + return len(getSlice(amounts, "nfts")) > 0 +} + +func getStringFromMap(input map[string]interface{}, key string) string { + if input == nil { + return "" + } + raw, ok := input[key] + if !ok || raw == nil { + return "" + } + switch v := raw.(type) { + case string: + return v + default: + return fmt.Sprintf("%v", v) + } +} + +func getMapFromMap(input map[string]interface{}, key string) map[string]interface{} { + if input == nil { + return nil + } + raw, ok := input[key] + if !ok || raw == nil { + return nil + } + output, ok := raw.(map[string]interface{}) + if !ok { + return nil + } + return output +} + +func getSliceOfMaps(input map[string]interface{}, key string) []map[string]interface{} { + items := getSlice(input, key) + if len(items) == 0 { + return nil + } + result := make([]map[string]interface{}, 0, len(items)) + for _, item := range items { + entry, ok := item.(map[string]interface{}) + if ok { + result = append(result, entry) + } + } + return result +} + +func getSlice(input map[string]interface{}, key string) []interface{} { + if input == nil { + return nil + } + raw, ok := input[key] + if !ok || raw == nil { + return nil + } + items, ok := raw.([]interface{}) + if !ok { + return nil + } + return items +} + +func intFromAny(value interface{}) int { + switch v := value.(type) { + case int: + return v + case int64: + return int(v) + case float64: + return int(v) + case json.Number: + n, err := v.Int64() + if err == nil { + return int(n) + } + case string: + parsed, ok := parseAmount(v) + if ok { + return int(parsed.Int64()) + } + } + return 0 +} + +func newANSIStyler() ansiStyler { + return ansiStyler{ + enabled: stdoutIsTerminal() && os.Getenv("NO_COLOR") == "", + } +} + +func stdoutIsTerminal() bool { + info, err := os.Stdout.Stat() + if err != nil { + return false + } + return (info.Mode() & os.ModeCharDevice) != 0 +} + +func (s ansiStyler) dim(value string) string { + return s.wrap("2", value) +} + +func (s ansiStyler) bold(value string) string { + return s.wrap("1", value) +} + +func (s ansiStyler) color(value string, code string) string { + if code == "" { + return value + } + return s.wrap(code, value) +} + +func (s ansiStyler) wrap(code string, value string) string { + if !s.enabled { + return value + } + return "\033[" + code + "m" + value + "\033[0m" +} + +func statusColorCode(status string) string { + switch strings.ToUpper(strings.TrimSpace(status)) { + case "CONFIRMED": + return "32" + case "PENDING": + return "33" + case "REMOVED": + return "31" + default: + return "90" + } +} + +func eventColorCode(eventType string) string { + switch strings.ToLower(strings.TrimSpace(eventType)) { + case "transfer": + return "36" + case "swap": + return "32" + case "mint": + return "35" + case "melt": + return "33" + case "pool": + return "33" + case "revoke", "clawback": + return "31" + case "combine", "split": + return "32" + default: + return "90" + } +} + +func titleCase(value string) string { + clean := strings.TrimSpace(value) + if clean == "" { + return "" + } + clean = strings.ReplaceAll(clean, "_", " ") + clean = strings.ReplaceAll(clean, "-", " ") + + parts := strings.Fields(strings.ToLower(clean)) + for i, part := range parts { + if len(part) == 0 { + continue + } + parts[i] = strings.ToUpper(part[:1]) + part[1:] + } + return strings.Join(parts, " ") +} From 13cb0829091ecee6a5a0cf4420342b6909409669 Mon Sep 17 00:00:00 2001 From: Cameron Cooper Date: Tue, 3 Mar 2026 14:48:57 -0600 Subject: [PATCH 3/9] added clvm command and --inspect option --- .github/workflows/build-pr.yml | 13 ++- .github/workflows/release.yml | 123 ++++++++++++++++++++++-- .gitignore | 5 +- .goreleaser.darwin.yaml | 63 ++++++++++++ .goreleaser.linux.yaml | 42 ++++++++ .goreleaser.windows.yaml | 42 ++++++++ .goreleaser.yaml | 45 +++------ README.md | 28 ++++++ internal/cmd/coinset/clvm.go | 91 ++++++++++++++++++ internal/cmd/coinset/root.go | 2 + internal/cmd/coinset/util.go | 83 ++++++++++++++++ internal/coinsetffi/coinsetffi.go | 108 +++++++++++++++++++++ internal/coinsetffi/coinsetffi_nocgo.go | 21 ++++ 13 files changed, 624 insertions(+), 42 deletions(-) create mode 100644 .goreleaser.darwin.yaml create mode 100644 .goreleaser.linux.yaml create mode 100644 .goreleaser.windows.yaml create mode 100644 internal/cmd/coinset/clvm.go create mode 100644 internal/coinsetffi/coinsetffi.go create mode 100644 internal/coinsetffi/coinsetffi_nocgo.go diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index bfb868d..e674d62 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -19,9 +19,20 @@ jobs: uses: actions/setup-go@v5 with: go-version: stable + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-unknown-linux-gnu + - name: Build coinset-inspect staticlib + run: | + cargo build --release -p coinset-ffi --target x86_64-unknown-linux-gnu + mkdir -p cgo-lib/x86_64-unknown-linux-gnu + cp "target/x86_64-unknown-linux-gnu/release/libcoinset.a" cgo-lib/x86_64-unknown-linux-gnu/ - name: Run GoReleaser (Build) uses: goreleaser/goreleaser-action@v5 with: distribution: goreleaser version: ~> v1 - args: build --clean --skip=validate + args: build --clean --skip=validate -f .goreleaser.linux.yaml + env: + CGO_LDFLAGS: -L${{ github.workspace }}/cgo-lib/x86_64-unknown-linux-gnu diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4e44430..af20e4a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,6 +8,8 @@ on: jobs: tag: runs-on: ubuntu-latest + outputs: + new_version: ${{ steps.increment_version.outputs.new_version }} steps: - name: Checkout repository uses: actions/checkout@v4 @@ -51,38 +53,143 @@ jobs: echo "New version: $new_version" - # Save the new version to the GitHub environment to be used in future steps - echo "new_version=$new_version" >> $GITHUB_ENV + echo "new_version=$new_version" >> "$GITHUB_OUTPUT" - name: Create and push new tag env: GITHUB_TOKEN: ${{ secrets.ACTIONS_GITHUB_TOKEN }} run: | - new_version=${{ env.new_version }} + new_version=${{ steps.increment_version.outputs.new_version }} # Create a new tag git tag "$new_version" # Push the new tag to the remote repository git push origin "$new_version" - release: + + release_linux: needs: tag runs-on: ubuntu-latest environment: goreleaser steps: - - name: Checkout + - name: Checkout tag + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ needs.tag.outputs.new_version }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: stable + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-unknown-linux-gnu + + - name: Build coinset-inspect staticlib + run: | + cargo build --release -p coinset-ffi --target x86_64-unknown-linux-gnu + mkdir -p cgo-lib/x86_64-unknown-linux-gnu + cp "target/x86_64-unknown-linux-gnu/release/libcoinset.a" cgo-lib/x86_64-unknown-linux-gnu/ + + - name: Run GoReleaser (linux) + uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser + version: ~> v1 + args: release --clean -f .goreleaser.linux.yaml + env: + GITHUB_TOKEN: ${{ secrets.ACTIONS_GITHUB_TOKEN }} + CGO_LDFLAGS: -L${{ github.workspace }}/cgo-lib/x86_64-unknown-linux-gnu + + release_macos: + needs: [tag, release_linux] + runs-on: macos-latest + environment: goreleaser + steps: + - name: Checkout tag + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ needs.tag.outputs.new_version }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: stable + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-apple-darwin + + - name: Build coinset-inspect staticlib + run: | + MACOSX_DEPLOYMENT_TARGET=12.0 cargo build --release -p coinset-ffi --target aarch64-apple-darwin + mkdir -p cgo-lib/aarch64-apple-darwin + cp "target/aarch64-apple-darwin/release/libcoinset.a" cgo-lib/aarch64-apple-darwin/ + + - name: Run GoReleaser (darwin) + uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser + version: ~> v1 + args: release --clean -f .goreleaser.darwin.yaml + env: + GITHUB_TOKEN: ${{ secrets.ACTIONS_GITHUB_TOKEN }} + CGO_CFLAGS: -mmacosx-version-min=12.0 + CGO_LDFLAGS: -mmacosx-version-min=12.0 -L${{ github.workspace }}/cgo-lib/aarch64-apple-darwin + MACOSX_DEPLOYMENT_TARGET: "12.0" + + release_windows: + needs: [tag, release_macos] + runs-on: windows-latest + environment: goreleaser + steps: + - name: Checkout tag uses: actions/checkout@v4 with: fetch-depth: 0 + ref: ${{ needs.tag.outputs.new_version }} + - name: Set up Go uses: actions/setup-go@v5 with: go-version: stable - - name: Run GoReleaser + + - name: Setup MSYS2 (mingw64) + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + update: true + install: >- + mingw-w64-x86_64-gcc + - name: Add MinGW to PATH + shell: pwsh + run: | + "C:\\msys64\\mingw64\\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-pc-windows-gnu + + - name: Build coinset-inspect staticlib + shell: msys2 {0} + run: | + cargo build --release -p coinset-ffi --target x86_64-pc-windows-gnu + mkdir -p cgo-lib/x86_64-pc-windows-gnu + cp "target/x86_64-pc-windows-gnu/release/libcoinset.a" cgo-lib/x86_64-pc-windows-gnu/ + + - name: Run GoReleaser (windows) uses: goreleaser/goreleaser-action@v5 with: distribution: goreleaser version: ~> v1 - args: release --clean + args: release --clean -f .goreleaser.windows.yaml env: - GITHUB_TOKEN: ${{ secrets.ACTIONS_GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.ACTIONS_GITHUB_TOKEN }} + CGO_LDFLAGS: -L${{ github.workspace }}/cgo-lib/x86_64-pc-windows-gnu + CC: gcc diff --git a/.gitignore b/.gitignore index 353f6d1..05c3eef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ dist/ -.DS_Store \ No newline at end of file +.DS_Store +target/ +cgo-lib/ +bin/ diff --git a/.goreleaser.darwin.yaml b/.goreleaser.darwin.yaml new file mode 100644 index 0000000..1bb08dd --- /dev/null +++ b/.goreleaser.darwin.yaml @@ -0,0 +1,63 @@ +version: 1 + +project_name: coinset + +before: + hooks: + - go mod tidy + - go generate ./... + +builds: + - id: coinset-darwin + main: ./cmd/coinset + env: + - CGO_ENABLED=1 + - MACOSX_DEPLOYMENT_TARGET=12.0 + - CGO_CFLAGS=-mmacosx-version-min=12.0 + - CGO_LDFLAGS=-mmacosx-version-min=12.0 + goos: + - darwin + goarch: + - arm64 + ldflags: + - -s -w -X main.version={{ .Version }} + +archives: + - format: tar.gz + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + +release: + github: + owner: coinset-org + name: cli + +brews: + - commit_author: + name: Cameron Cooper + email: cameron@coinset.org + homepage: "https://www.coinset.org/" + description: "CLI for accessing the Chia blockchain." + license: "MIT" + repository: + owner: coinset-org + name: homebrew-cli + install: | + bin.install "coinset" => "coinset" + output = Utils.popen_read("SHELL=bash #{bin}/coinset completion bash") + (bash_completion/"coinset").write output + output = Utils.popen_read("SHELL=zsh #{bin}/coinset completion zsh") + (zsh_completion/"_coinset").write output + prefix.install_metafiles diff --git a/.goreleaser.linux.yaml b/.goreleaser.linux.yaml new file mode 100644 index 0000000..a3327c3 --- /dev/null +++ b/.goreleaser.linux.yaml @@ -0,0 +1,42 @@ +version: 1 + +project_name: coinset + +before: + hooks: + - go mod tidy + - go generate ./... + +builds: + - id: coinset-linux + main: ./cmd/coinset + env: + - CGO_ENABLED=1 + goos: + - linux + goarch: + - amd64 + ldflags: + - -s -w -X main.version={{ .Version }} + +archives: + - format: tar.gz + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + +release: + github: + owner: coinset-org + name: cli diff --git a/.goreleaser.windows.yaml b/.goreleaser.windows.yaml new file mode 100644 index 0000000..8a19e5e --- /dev/null +++ b/.goreleaser.windows.yaml @@ -0,0 +1,42 @@ +version: 1 + +project_name: coinset + +before: + hooks: + - go mod tidy + - go generate ./... + +builds: + - id: coinset-windows + main: ./cmd/coinset + env: + - CGO_ENABLED=1 + goos: + - windows + goarch: + - amd64 + ldflags: + - -s -w -X main.version={{ .Version }} + +archives: + - format: zip + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + +release: + github: + owner: coinset-org + name: cli diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 1151829..a1261a1 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,10 +1,10 @@ -# This is an example .goreleaser.yml file with some sensible defaults. -# Make sure to check the documentation at https://goreleaser.com - -# The lines below are called `modelines`. See `:help modeline` -# Feel free to remove those if you don't want/need to use them. # yaml-language-server: $schema=https://goreleaser.com/static/schema.json -# vim: set ts=2 sw=2 tw=0 fo=cnqoj +# NOTE: CGO + Rust staticlib builds are runner-specific. +# In CI we run GoReleaser with one of: +# - `.goreleaser.linux.yaml` +# - `.goreleaser.darwin.yaml` +# - `.goreleaser.windows.yaml` +# This default config is kept runnable locally and targets Linux only. version: 1 @@ -16,13 +16,16 @@ before: - go generate ./... builds: - - main: ./cmd/coinset + - id: coinset-linux + main: ./cmd/coinset env: - - CGO_ENABLED=0 + - CGO_ENABLED=1 goos: - linux - - windows - - darwin + goarch: + - amd64 + ldflags: + - -s -w -X main.version={{ .Version }} archives: - format: tar.gz @@ -34,10 +37,6 @@ archives: {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }} - # use zip for windows archives - format_overrides: - - goos: windows - format: zip changelog: sort: asc @@ -51,21 +50,3 @@ release: owner: coinset-org name: cli -brews: - - commit_author: - name: Cameron Cooper - email: cameron@coinset.org - homepage: "https://www.coinset.org/" - description: "CLI for accessing the Chia blockchain." - license: "MIT" - repository: - owner: coinset-org - name: homebrew-cli - install: | - bin.install "coinset" => "coinset" - output = Utils.popen_read("SHELL=bash #{bin}/coinset completion bash") - (bash_completion/"coinset").write output - output = Utils.popen_read("SHELL=zsh #{bin}/coinset completion zsh") - (zsh_completion/"_coinset").write output - prefix.install_metafiles - diff --git a/README.md b/README.md index 9b90b93..3621ba0 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,34 @@ $ coinset get_coin_records_by_parent_ids 0xa908ee64a5821b7bda5d798c053a79c8b3d7c } ``` +### Spend inspection (`--inspect`) +If an RPC response contains spend data (spend bundles / coin spends), you can add `--inspect` to replace the output with a best-effort interpreted view: cost, conditions, additions/removals, signatures, and optional CLVM disassembly. + +Example: + +```bash +coinset get_block_spends 5000000 --inspect +``` + +`--inspect` is supported when the returned JSON contains one of these shapes: +- `mempool_item` +- `spend_bundle` / `spend_bundle_bytes` +- `coin_spends` / `block_spends` +- `coin_spend` (single spend) + +Notes: +- Release/brew installs include `--inspect`. +- Building from source requires the Rust static library. The easiest path is `make build` (see `Makefile`). + +### CLVM tools (`coinset clvm`) +Local CLVM helpers are available under `coinset clvm`: + +```bash +coinset clvm decompile 0xff0101 +coinset clvm compile "(q . 1)" +coinset clvm run "(q . 1)" "()" --cost +``` + ## Available Commands The following table shows all available CLI commands organized by functionality: diff --git a/internal/cmd/coinset/clvm.go b/internal/cmd/coinset/clvm.go new file mode 100644 index 0000000..9f94ef3 --- /dev/null +++ b/internal/cmd/coinset/clvm.go @@ -0,0 +1,91 @@ +package cmd + +import ( + "fmt" + "log" + + "github.com/coinset-org/cli/internal/coinsetffi" + "github.com/spf13/cobra" +) + +var clvmCmd = &cobra.Command{ + Use: "clvm", + Short: "CLVM utilities (compile/decompile/run)", +} + +func init() { + rootCmd.AddCommand(clvmCmd) + + clvmCmd.AddCommand(clvmDecompileCmd) + clvmCmd.AddCommand(clvmCompileCmd) + clvmCmd.AddCommand(clvmRunCmd) + + clvmRunCmd.Flags().String("program", "", "CLVM program (hex bytes or s-expression)") + clvmRunCmd.Flags().String("env", "()", "CLVM environment (hex bytes or s-expression)") + clvmRunCmd.Flags().Uint64("max-cost", 0, "Maximum cost (0 = unlimited)") + clvmRunCmd.Flags().Bool("cost", false, "Include cost in output") +} + +var clvmDecompileCmd = &cobra.Command{ + Use: "decompile ", + Short: "Decode CLVM bytes to readable CLVM", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + out, err := coinsetffi.ClvmDecompile(args[0], false) + if err != nil { + log.Fatal(err.Error()) + } + printJson(out) + }, +} + +var clvmCompileCmd = &cobra.Command{ + Use: "compile ", + Short: "Encode readable CLVM to bytes", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + out, err := coinsetffi.ClvmCompile(args[0], false) + if err != nil { + log.Fatal(err.Error()) + } + printJson(out) + }, +} + +var clvmRunCmd = &cobra.Command{ + Use: "run [program] [env]", + Short: "Run CLVM program with environment", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) > 2 { + return fmt.Errorf("too many arguments") + } + if len(args) == 0 { + prog, _ := cmd.Flags().GetString("program") + if prog == "" { + return fmt.Errorf("provide program as arg or via --program") + } + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + progFlag, _ := cmd.Flags().GetString("program") + envFlag, _ := cmd.Flags().GetString("env") + maxCost, _ := cmd.Flags().GetUint64("max-cost") + includeCost, _ := cmd.Flags().GetBool("cost") + + program := progFlag + env := envFlag + if len(args) >= 1 { + program = args[0] + } + if len(args) >= 2 { + env = args[1] + } + + out, err := coinsetffi.ClvmRun(program, env, maxCost, includeCost, false) + if err != nil { + log.Fatal(err.Error()) + } + printJson(out) + }, +} diff --git a/internal/cmd/coinset/root.go b/internal/cmd/coinset/root.go index 07d645e..ef9ed76 100644 --- a/internal/cmd/coinset/root.go +++ b/internal/cmd/coinset/root.go @@ -34,6 +34,7 @@ var local bool var raw bool var api string var describe bool +var inspect bool var version = "dev" func init() { @@ -44,5 +45,6 @@ func init() { 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.PersistentFlags().BoolVar(&inspect, "inspect", false, "replace output with inspected spend interpretation when possible") rootCmd.MarkFlagsMutuallyExclusive("mainnet", "testnet", "api") } diff --git a/internal/cmd/coinset/util.go b/internal/cmd/coinset/util.go index 20d0147..286c9ff 100644 --- a/internal/cmd/coinset/util.go +++ b/internal/cmd/coinset/util.go @@ -2,6 +2,7 @@ package cmd import ( "encoding/json" + "errors" "fmt" "log" "math/big" @@ -15,6 +16,7 @@ import ( "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/coinset-org/cli/internal/coinsetffi" "github.com/dustin/go-humanize" "github.com/itchyny/gojq" ) @@ -145,9 +147,90 @@ func makeRequest(path string, jsonData map[string]interface{}) { if err != nil { log.Fatal(err.Error()) } + if inspect { + inspected, err := inspectRpcOutput(jsonResponse) + if err != nil { + log.Fatal(err.Error()) + } + printJson(inspected) + return + } printJson(jsonResponse) } +func inspectRpcOutput(jsonBytes []byte) ([]byte, error) { + var v interface{} + if err := json.Unmarshal(jsonBytes, &v); err != nil { + return nil, fmt.Errorf("invalid JSON from RPC: %w", err) + } + + // Many full node RPCs return mempool items as a map keyed by tx id. + if m, ok := v.(map[string]interface{}); ok { + if items, ok := m["mempool_items"]; ok { + if itemsMap, ok := items.(map[string]interface{}); ok { + out := make(map[string]json.RawMessage, len(itemsMap)) + for txID, item := range itemsMap { + wrapped := map[string]interface{}{"mempool_item": item} + wrappedBytes, err := json.Marshal(wrapped) + if err != nil { + return nil, fmt.Errorf("failed to re-marshal mempool_item %s: %w", txID, err) + } + inspected, err := coinsetffi.Inspect(wrappedBytes, false, false) + if err != nil { + return nil, fmt.Errorf("inspect mempool_item %s: %w", txID, err) + } + out[txID] = json.RawMessage(inspected) + } + return json.Marshal(out) + } + } + } + + if !looksInspectable(v) { + return nil, errors.New("--inspect not supported for this endpoint/output shape") + } + return coinsetffi.Inspect(jsonBytes, false, false) +} + +func looksInspectable(v interface{}) bool { + switch x := v.(type) { + case []interface{}: + // Often block spends are arrays of coin spend entries. + return len(x) > 0 + case map[string]interface{}: + if _, ok := x["mempool_item"]; ok { + return true + } + if _, ok := x["mempool_items"]; ok { + return true + } + if _, ok := x["spend_bundle"]; ok { + return true + } + if _, ok := x["spend_bundle_bytes"]; ok { + return true + } + if _, ok := x["coin_spends"]; ok { + return true + } + if _, ok := x["block_spends"]; ok { + return true + } + if _, ok := x["coin_spend"]; ok { + return true + } + if _, ok := x["puzzle_reveal"]; ok { + return true + } + if _, ok := x["solution"]; ok { + return true + } + return false + default: + return false + } +} + // Cache for block records (height -> timestamp) var blockRecordCache = make(map[int]int64) var blockRecordCacheMutex sync.RWMutex diff --git a/internal/coinsetffi/coinsetffi.go b/internal/coinsetffi/coinsetffi.go new file mode 100644 index 0000000..2c81cf0 --- /dev/null +++ b/internal/coinsetffi/coinsetffi.go @@ -0,0 +1,108 @@ +//go:build cgo + +package coinsetffi + +/* +#include +#include + +#cgo darwin,arm64 LDFLAGS: -L${SRCDIR}/../../cgo-lib/aarch64-apple-darwin +#cgo linux,amd64 LDFLAGS: -L${SRCDIR}/../../cgo-lib/x86_64-unknown-linux-gnu +#cgo windows,amd64 LDFLAGS: -L${SRCDIR}/../../cgo-lib/x86_64-pc-windows-gnu +#cgo LDFLAGS: -lcoinset + +char* coinset_inspect(const uint8_t* input_ptr, size_t input_len, uint32_t flags); +char* coinset_clvm_decompile(const uint8_t* input_ptr, size_t input_len, uint32_t flags); +char* coinset_clvm_compile(const uint8_t* input_ptr, size_t input_len, uint32_t flags); +char* coinset_clvm_run(const uint8_t* program_ptr, size_t program_len, const uint8_t* env_ptr, size_t env_len, uint64_t max_cost, uint32_t flags); +void coinset_free(char* s); +const char* coinset_version(void); +*/ +import "C" + +import ( + "errors" + "unsafe" +) + +const ( + flagPretty = 1 << 0 + flagConditionsOnly = 1 << 1 + flagIncludeCost = 1 << 2 +) + +func Inspect(input []byte, pretty bool, conditionsOnly bool) ([]byte, error) { + if len(input) == 0 { + return nil, errors.New("empty input") + } + var flags C.uint32_t + if pretty { + flags |= flagPretty + } + if conditionsOnly { + flags |= flagConditionsOnly + } + + ptr := (*C.uint8_t)(unsafe.Pointer(&input[0])) + out := C.coinset_inspect(ptr, C.size_t(len(input)), flags) + return readAndFree(out) +} + +func ClvmDecompile(hexBytes string, pretty bool) ([]byte, error) { + in := []byte(hexBytes) + if len(in) == 0 { + return nil, errors.New("empty input") + } + var flags C.uint32_t + if pretty { + flags |= flagPretty + } + ptr := (*C.uint8_t)(unsafe.Pointer(&in[0])) + out := C.coinset_clvm_decompile(ptr, C.size_t(len(in)), flags) + return readAndFree(out) +} + +func ClvmCompile(program string, pretty bool) ([]byte, error) { + in := []byte(program) + if len(in) == 0 { + return nil, errors.New("empty input") + } + var flags C.uint32_t + if pretty { + flags |= flagPretty + } + ptr := (*C.uint8_t)(unsafe.Pointer(&in[0])) + out := C.coinset_clvm_compile(ptr, C.size_t(len(in)), flags) + return readAndFree(out) +} + +func ClvmRun(program string, env string, maxCost uint64, includeCost bool, pretty bool) ([]byte, error) { + p := []byte(program) + if len(p) == 0 { + return nil, errors.New("empty program") + } + e := []byte(env) + if len(e) == 0 { + e = []byte("()") + } + var flags C.uint32_t + if pretty { + flags |= flagPretty + } + if includeCost { + flags |= flagIncludeCost + } + + pPtr := (*C.uint8_t)(unsafe.Pointer(&p[0])) + ePtr := (*C.uint8_t)(unsafe.Pointer(&e[0])) + out := C.coinset_clvm_run(pPtr, C.size_t(len(p)), ePtr, C.size_t(len(e)), C.uint64_t(maxCost), flags) + return readAndFree(out) +} + +func readAndFree(out *C.char) ([]byte, error) { + if out == nil { + return nil, errors.New("ffi returned null") + } + defer C.coinset_free(out) + return []byte(C.GoString(out)), nil +} diff --git a/internal/coinsetffi/coinsetffi_nocgo.go b/internal/coinsetffi/coinsetffi_nocgo.go new file mode 100644 index 0000000..c7dc6ca --- /dev/null +++ b/internal/coinsetffi/coinsetffi_nocgo.go @@ -0,0 +1,21 @@ +//go:build !cgo + +package coinsetffi + +import "errors" + +func Inspect(_ []byte, _ bool, _ bool) ([]byte, error) { + return nil, errors.New("coinset ffi requires cgo-enabled build") +} + +func ClvmDecompile(_ string, _ bool) ([]byte, error) { + return nil, errors.New("coinset ffi requires cgo-enabled build") +} + +func ClvmCompile(_ string, _ bool) ([]byte, error) { + return nil, errors.New("coinset ffi requires cgo-enabled build") +} + +func ClvmRun(_ string, _ string, _ uint64, _ bool, _ bool) ([]byte, error) { + return nil, errors.New("coinset ffi requires cgo-enabled build") +} From e30073b812e9f7c50ee81c1310e5c33ba756aa45 Mon Sep 17 00:00:00 2001 From: Cameron Cooper Date: Tue, 3 Mar 2026 14:53:39 -0600 Subject: [PATCH 4/9] added rust code --- Cargo.lock | 1667 ++++++++++++++++++++++++ Cargo.toml | 26 + crates/coinset-clvm-core/Cargo.toml | 14 + crates/coinset-clvm-core/src/lib.rs | 108 ++ crates/coinset-ffi/Cargo.toml | 16 + crates/coinset-ffi/src/lib.rs | 148 +++ crates/coinset-inspect-core/Cargo.toml | 19 + crates/coinset-inspect-core/src/lib.rs | 750 +++++++++++ 8 files changed, 2748 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 crates/coinset-clvm-core/Cargo.toml create mode 100644 crates/coinset-clvm-core/src/lib.rs create mode 100644 crates/coinset-ffi/Cargo.toml create mode 100644 crates/coinset-ffi/src/lib.rs create mode 100644 crates/coinset-inspect-core/Cargo.toml create mode 100644 crates/coinset-inspect-core/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e70c094 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1667 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "binascii" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chia-bls" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38840e8c8c5c6eb61304d85102230e74c2015c2bfb945292777f83f48915544c" +dependencies = [ + "blst", + "chia-sha2 0.28.2", + "chia-traits 0.28.2", + "hex", + "hkdf", + "linked-hash-map", + "sha2", + "thiserror", +] + +[[package]] +name = "chia-bls" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a74b1699d07115c71c081826608e3fb73f2aed94f9060fa09d074a95cfc93ab" +dependencies = [ + "blst", + "chia-sha2 0.30.0", + "chia-traits 0.30.0", + "hex", + "hkdf", + "linked-hash-map", + "sha2", + "thiserror", +] + +[[package]] +name = "chia-bls" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f02cbfd038d9050d45edbe8f38e09391c73479c0cca5b37925daf48c4d4fcd4" +dependencies = [ + "blst", + "chia-serde", + "chia-sha2 0.36.1", + "chia-traits 0.36.1", + "hex", + "hkdf", + "linked-hash-map", + "serde", + "sha2", + "thiserror", +] + +[[package]] +name = "chia-consensus" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d942bcb20b243213becbd94630dd7122c18dea7450beab38cbb4a6fcac993cae" +dependencies = [ + "chia-bls 0.36.1", + "chia-protocol", + "chia-puzzle-types", + "chia-puzzles", + "chia-sha2 0.36.1", + "chia-traits 0.36.1", + "chia_streamable_macro 0.36.1", + "clvm-traits", + "clvm-utils", + "clvmr", + "hex", + "hex-literal", + "thiserror", +] + +[[package]] +name = "chia-protocol" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "150d1f8a5978fe58bce49af89ef2abfd292dad324fcbbced5972de78772f6e9c" +dependencies = [ + "chia-bls 0.36.1", + "chia-serde", + "chia-sha2 0.36.1", + "chia-traits 0.36.1", + "chia_streamable_macro 0.36.1", + "clvm-traits", + "clvm-utils", + "clvmr", + "hex", + "serde", + "serde_arrays", +] + +[[package]] +name = "chia-puzzle-types" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37af8b2e82a898e35eea5e2766d18d616ab266343fa567042b6217bd96ef4819" +dependencies = [ + "chia-bls 0.36.1", + "chia-protocol", + "chia-puzzles", + "chia-sha2 0.36.1", + "clvm-traits", + "clvm-utils", + "clvmr", + "hex-literal", + "num-bigint", +] + +[[package]] +name = "chia-puzzles" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "553363ce9550cfde0ae1306a0a79bf9afd36ea09e0222b874b6287b44ad9d178" +dependencies = [ + "hex", + "hex-literal", +] + +[[package]] +name = "chia-serde" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f776d8555d463e6915e02598bb2be2123c57f6eaeb5c8078565957f062b92d1d" +dependencies = [ + "hex", + "serde", +] + +[[package]] +name = "chia-sha2" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f03c71bc63599a711caad15dafca1d2a948c26b4c8a021709a338149632769" +dependencies = [ + "sha2", +] + +[[package]] +name = "chia-sha2" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4114775b95f24b296047dc9c91e43834aa651c7c25bb2bbae05d23d79c9c777b" +dependencies = [ + "sha2", +] + +[[package]] +name = "chia-sha2" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0934b0d6b878f29ba6c958e56e4b7158f9e687c200ffdca141dbc408a5cce42e" +dependencies = [ + "sha2", +] + +[[package]] +name = "chia-traits" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52463c56cdf35aac55ecb35ffc0a615f0780f33f144503957e83f5b1ad92da3c" +dependencies = [ + "chia-sha2 0.28.2", + "chia_streamable_macro 0.28.2", + "thiserror", +] + +[[package]] +name = "chia-traits" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143545b13658929fce2e770a499fd4755f04a4d5f6459dc76a79068ae192a8c5" +dependencies = [ + "chia-sha2 0.30.0", + "chia_streamable_macro 0.30.0", + "thiserror", +] + +[[package]] +name = "chia-traits" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4922b447b2d8418213948af1a448c3ca7b84e149b51b2c87a2e00e80bb19b0" +dependencies = [ + "chia-sha2 0.36.1", + "chia_streamable_macro 0.36.1", + "thiserror", +] + +[[package]] +name = "chia_streamable_macro" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6b807a9c04440d3f30b5534b84b462b996b34eee7fe0b7f8737ae6dfc6b9d4" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "chia_streamable_macro" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25bd4fbc3f05375c267ee4cb2a4a1bf8125fa0d817e57d7b141993c35994c751" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "chia_streamable_macro" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b60cefc5fe39f695816d42a327cbefad3d6d6a8ecadad1b58d7507067c25da8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "chialisp" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa5f2217b8d1a7575ca7889a2f655294c02607cabfe9029e2ed8de13104edeca" +dependencies = [ + "binascii", + "chia-bls 0.30.0", + "clvmr", + "do-notation", + "getrandom 0.2.17", + "hashlink", + "hex", + "indoc", + "js-sys", + "lazy_static", + "num", + "num-bigint", + "num-traits", + "pyo3-build-config", + "regex", + "serde", + "serde_json", + "sha2", + "tempfile", + "unicode-segmentation", + "wasm-bindgen", + "yaml-rust2", +] + +[[package]] +name = "clvm-derive" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2f39856545faf48ae1e191b24fa0b74ff79e57b0b9d269a35d14282803d2b2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clvm-traits" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96380e11b921106bd3d2e24f8b85b6de2f99a760a6d5f0bd6d68ec096695b34b" +dependencies = [ + "chia-bls 0.36.1", + "clvm-derive", + "clvmr", + "num-bigint", + "thiserror", +] + +[[package]] +name = "clvm-utils" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65adea81c7ab1659180a4ffb10a0df63eb049b5b8de2caa8bf2d762f55c7c4db" +dependencies = [ + "chia-sha2 0.36.1", + "clvm-traits", + "clvmr", + "hex", + "hex-literal", +] + +[[package]] +name = "clvmr" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf14f44f7c6b1d7972753cdd66f6afae38b117b752e6d1256b6f532dc3a0d304" +dependencies = [ + "bitvec", + "bumpalo", + "chia-bls 0.28.2", + "chia-sha2 0.28.2", + "hex", + "hex-literal", + "k256", + "lazy_static", + "num-bigint", + "num-integer", + "num-traits", + "p256", + "rand", + "sha1", + "sha3", + "thiserror", +] + +[[package]] +name = "coinset-clvm-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "chialisp", + "clvmr", + "hex", + "serde", + "serde_json", +] + +[[package]] +name = "coinset-ffi" +version = "0.1.0" +dependencies = [ + "anyhow", + "coinset-clvm-core", + "coinset-inspect-core", + "serde_json", +] + +[[package]] +name = "coinset-inspect-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "chia-bls 0.36.1", + "chia-consensus", + "chia-protocol", + "chia-traits 0.36.1", + "chialisp", + "clvm-utils", + "clvmr", + "hex", + "serde", + "serde_json", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "do-notation" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e16a80c1dda2cf52fa07106427d3d798b6331dca8155fcb8c39f7fc78f6dd2" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3-build-config" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_arrays" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a16b99c5ea4fe3daccd14853ad260ec00ea043b2708d1fd1da3106dcd8d9df" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "serde", + "serde_json", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yaml-rust2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e8eb266 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[workspace] +members = [ + "crates/coinset-clvm-core", + "crates/coinset-inspect-core", + "crates/coinset-ffi", +] +resolver = "2" + +[workspace.package] +edition = "2024" +license = "MIT" +version = "0.0.0" + +[workspace.dependencies] +anyhow = "1.0.102" +hex = "0.4.3" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" + +chia-consensus = "0.36.1" +chia-bls = "0.36.1" +chia-protocol = { version = "0.36.1", features = ["serde"] } +chia-traits = "0.36.1" +clvm-utils = "0.36.1" +clvmr = "=0.16.2" +chialisp = "0.4.1" diff --git a/crates/coinset-clvm-core/Cargo.toml b/crates/coinset-clvm-core/Cargo.toml new file mode 100644 index 0000000..ab78e88 --- /dev/null +++ b/crates/coinset-clvm-core/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "coinset-clvm-core" +version = "0.1.0" +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +hex.workspace = true +serde.workspace = true +serde_json.workspace = true + +clvmr.workspace = true +chialisp.workspace = true diff --git a/crates/coinset-clvm-core/src/lib.rs b/crates/coinset-clvm-core/src/lib.rs new file mode 100644 index 0000000..502028a --- /dev/null +++ b/crates/coinset-clvm-core/src/lib.rs @@ -0,0 +1,108 @@ +use anyhow::{Context, Result, bail}; +use chialisp::classic::clvm::OPERATORS_LATEST_VERSION; +use chialisp::classic::clvm_tools::binutils::{assemble, disassemble}; +use clvmr::allocator::Allocator; +use clvmr::chia_dialect::{ChiaDialect, MEMPOOL_MODE}; +use clvmr::cost::Cost; +use clvmr::run_program::run_program; +use clvmr::serde::{node_from_bytes_backrefs, node_to_bytes}; +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] +pub struct CompileOutput { + pub bytes: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct DecompileOutput { + pub clvm: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct RunOutput { + pub result: String, + pub cost: Option, +} + +pub fn compile_to_hex(program: &str) -> Result { + let mut allocator = Allocator::new(); + let node = assemble(&mut allocator, program) + .map_err(|e| anyhow::anyhow!("failed to assemble CLVM: {e}"))?; + let bytes = node_to_bytes(&allocator, node)?; + Ok(CompileOutput { + bytes: encode_hex_prefixed(&bytes), + }) +} + +pub fn decompile_from_hex(hex_bytes: &str) -> Result { + let bytes = decode_hex_prefixed(hex_bytes)?; + let mut allocator = Allocator::new(); + let node = node_from_bytes_backrefs(&mut allocator, &bytes)?; + Ok(DecompileOutput { + clvm: disassemble(&allocator, node, Some(OPERATORS_LATEST_VERSION)), + }) +} + +pub fn run_program_text( + program: &str, + env: &str, + max_cost: Option, + include_cost: bool, +) -> Result { + let mut allocator = Allocator::new(); + let program_node = parse_program_input(&mut allocator, program)?; + let env_node = parse_program_input(&mut allocator, env)?; + + let dialect = ChiaDialect::new(MEMPOOL_MODE); + let cost_limit: Cost = match max_cost { + Some(v) => v as Cost, + None => 0, + }; + + let reduction = run_program(&mut allocator, &dialect, program_node, env_node, cost_limit) + .map_err(|e| anyhow::anyhow!("CLVM runtime error: {e:?}"))?; + let cost = reduction.0 as u64; + let result_node = reduction.1; + + Ok(RunOutput { + result: disassemble(&allocator, result_node, Some(OPERATORS_LATEST_VERSION)), + cost: if include_cost { Some(cost) } else { None }, + }) +} + +fn parse_program_input(allocator: &mut Allocator, input: &str) -> Result { + if looks_like_hex(input) { + let bytes = decode_hex_prefixed(input)?; + let node = node_from_bytes_backrefs(allocator, &bytes)?; + return Ok(node); + } + let node = assemble(allocator, input).map_err(|e| anyhow::anyhow!("failed to assemble CLVM: {e}"))?; + Ok(node) +} + +fn decode_hex_prefixed(input: &str) -> Result> { + let raw = input + .strip_prefix("0x") + .or_else(|| input.strip_prefix("0X")) + .unwrap_or(input) + .trim(); + if raw.is_empty() { + return Ok(Vec::new()); + } + if raw.len() % 2 != 0 { + bail!("hex input must have even length"); + } + Ok(hex::decode(raw).with_context(|| "failed to decode hex input")?) +} + +fn encode_hex_prefixed(bytes: &[u8]) -> String { + format!("0x{}", hex::encode(bytes)) +} + +fn looks_like_hex(input: &str) -> bool { + let raw = input + .strip_prefix("0x") + .or_else(|| input.strip_prefix("0X")) + .unwrap_or(input); + !raw.is_empty() && raw.len() % 2 == 0 && raw.bytes().all(|b| b.is_ascii_hexdigit()) +} diff --git a/crates/coinset-ffi/Cargo.toml b/crates/coinset-ffi/Cargo.toml new file mode 100644 index 0000000..822214c --- /dev/null +++ b/crates/coinset-ffi/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "coinset-ffi" +version = "0.1.0" +edition.workspace = true +license.workspace = true + +[lib] +name = "coinset" +crate-type = ["staticlib"] + +[dependencies] +anyhow.workspace = true +serde_json.workspace = true + +coinset-inspect-core = { path = "../coinset-inspect-core" } +coinset-clvm-core = { path = "../coinset-clvm-core" } diff --git a/crates/coinset-ffi/src/lib.rs b/crates/coinset-ffi/src/lib.rs new file mode 100644 index 0000000..9146d4c --- /dev/null +++ b/crates/coinset-ffi/src/lib.rs @@ -0,0 +1,148 @@ +use std::ffi::{CStr, CString, c_char}; + +use coinset_clvm_core::{compile_to_hex, decompile_from_hex, run_program_text}; +use coinset_inspect_core::{ExplainLevel, inspect_json_string}; +use serde_json::json; + +const FLAG_PRETTY: u32 = 1 << 0; +const FLAG_CONDITIONS_ONLY: u32 = 1 << 1; +const FLAG_INCLUDE_COST: u32 = 1 << 2; + +#[unsafe(no_mangle)] +pub extern "C" fn coinset_inspect(input_ptr: *const u8, input_len: usize, flags: u32) -> *mut c_char { + let pretty = (flags & FLAG_PRETTY) != 0; + let explain_level = if (flags & FLAG_CONDITIONS_ONLY) != 0 { + ExplainLevel::Conditions + } else { + ExplainLevel::Deep + }; + match with_utf8_input(input_ptr, input_len, |s| inspect_json_string(s, pretty, explain_level)) { + Ok(s) => CString::new(s).unwrap_or_else(|_| render_error("invalid_output", "output contained NUL")), + Err(e) => render_error("inspect_failed", &format!("{e:#}")), + } + .into_raw() +} + +#[unsafe(no_mangle)] +pub extern "C" fn coinset_clvm_decompile(input_ptr: *const u8, input_len: usize, flags: u32) -> *mut c_char { + let pretty = (flags & FLAG_PRETTY) != 0; + match with_utf8_input(input_ptr, input_len, |s| { + let out = decompile_from_hex(s)?; + if pretty { + Ok(serde_json::to_string_pretty(&out)?) + } else { + Ok(serde_json::to_string(&out)?) + } + }) { + Ok(s) => CString::new(s).unwrap_or_else(|_| render_error("invalid_output", "output contained NUL")), + Err(e) => render_error("clvm_decompile_failed", &format!("{e:#}")), + } + .into_raw() +} + +#[unsafe(no_mangle)] +pub extern "C" fn coinset_clvm_compile(input_ptr: *const u8, input_len: usize, flags: u32) -> *mut c_char { + let pretty = (flags & FLAG_PRETTY) != 0; + match with_utf8_input(input_ptr, input_len, |s| { + let out = compile_to_hex(s)?; + if pretty { + Ok(serde_json::to_string_pretty(&out)?) + } else { + Ok(serde_json::to_string(&out)?) + } + }) { + Ok(s) => CString::new(s).unwrap_or_else(|_| render_error("invalid_output", "output contained NUL")), + Err(e) => render_error("clvm_compile_failed", &format!("{e:#}")), + } + .into_raw() +} + +#[unsafe(no_mangle)] +pub extern "C" fn coinset_clvm_run( + program_ptr: *const u8, + program_len: usize, + env_ptr: *const u8, + env_len: usize, + max_cost: u64, + flags: u32, +) -> *mut c_char { + let pretty = (flags & FLAG_PRETTY) != 0; + let include_cost = (flags & FLAG_INCLUDE_COST) != 0; + + let program = match unsafe_bytes_to_str(program_ptr, program_len) { + Ok(s) => s, + Err(e) => return render_error("invalid_utf8", &format!("{e:#}")).into_raw(), + }; + let env = match unsafe_bytes_to_str(env_ptr, env_len) { + Ok(s) => s, + Err(e) => return render_error("invalid_utf8", &format!("{e:#}")).into_raw(), + }; + + let max_cost = if max_cost == 0 { None } else { Some(max_cost) }; + match run_program_text(program, env, max_cost, include_cost) { + Ok(out) => { + let s = if pretty { + serde_json::to_string_pretty(&out) + } else { + serde_json::to_string(&out) + }; + match s { + Ok(s) => CString::new(s) + .unwrap_or_else(|_| render_error("invalid_output", "output contained NUL")), + Err(e) => render_error("invalid_output", &format!("{e:#}")), + } + } + Err(e) => render_error("clvm_run_failed", &format!("{e:#}")), + } + .into_raw() +} + +#[unsafe(no_mangle)] +pub extern "C" fn coinset_free(s: *mut c_char) { + if s.is_null() { + return; + } + unsafe { + drop(CString::from_raw(s)); + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn coinset_version() -> *const c_char { + static VERSION: &CStr = unsafe { + CStr::from_bytes_with_nul_unchecked(concat!(env!("CARGO_PKG_VERSION"), "\0").as_bytes()) + }; + VERSION.as_ptr() +} + +fn with_utf8_input(input_ptr: *const u8, input_len: usize, f: F) -> anyhow::Result +where + F: FnOnce(&str) -> anyhow::Result, +{ + let s = unsafe_bytes_to_str(input_ptr, input_len)?; + f(s) +} + +fn unsafe_bytes_to_str<'a>(ptr: *const u8, len: usize) -> anyhow::Result<&'a str> { + if ptr.is_null() && len != 0 { + anyhow::bail!("invalid_argument: input_ptr was null"); + } + let bytes = unsafe { std::slice::from_raw_parts(ptr, len) }; + Ok(std::str::from_utf8(bytes)?) +} + +fn render_error(kind: &str, message: &str) -> CString { + let obj = json!({ + "schema_version": "coinset.ffi.v1", + "tool": { + "name": "coinset-ffi", + "version": env!("CARGO_PKG_VERSION"), + }, + "result": { + "status": "failed", + "error": { "kind": kind, "message": message }, + } + }); + let s = serde_json::to_string(&obj).unwrap_or_else(|_| "{\"status\":\"failed\"}".to_string()); + CString::new(s).unwrap_or_else(|_| CString::new("{\"status\":\"failed\"}").expect("cstr")) +} diff --git a/crates/coinset-inspect-core/Cargo.toml b/crates/coinset-inspect-core/Cargo.toml new file mode 100644 index 0000000..84d78e2 --- /dev/null +++ b/crates/coinset-inspect-core/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "coinset-inspect-core" +version = "0.1.0" +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +hex.workspace = true +serde.workspace = true +serde_json.workspace = true + +chia-consensus.workspace = true +chia-bls.workspace = true +chia-protocol.workspace = true +chia-traits.workspace = true +clvm-utils.workspace = true +clvmr.workspace = true +chialisp.workspace = true diff --git a/crates/coinset-inspect-core/src/lib.rs b/crates/coinset-inspect-core/src/lib.rs new file mode 100644 index 0000000..a077dcf --- /dev/null +++ b/crates/coinset-inspect-core/src/lib.rs @@ -0,0 +1,750 @@ +use std::collections::BTreeMap; + +use anyhow::{Context, Result, bail}; +use chia_bls::PublicKey; +use chia_consensus::allocator::make_allocator; +use chia_consensus::consensus_constants::TEST_CONSTANTS; +use chia_consensus::owned_conditions::{OwnedSpendBundleConditions, OwnedSpendConditions}; +use chia_consensus::spendbundle_conditions::get_conditions_from_spendbundle; +use chia_protocol::{Bytes, Coin, CoinSpend, SpendBundle}; +use chia_traits::Streamable; +use chialisp::classic::clvm::OPERATORS_LATEST_VERSION; +use chialisp::classic::clvm_tools::binutils::disassemble; +use clvm_utils::tree_hash_from_bytes; +use clvmr::allocator::Allocator as ClvmAllocator; +use clvmr::serde::node_from_bytes_backrefs; +use clvmr::LIMIT_HEAP; +use serde::Serialize; +use serde_json::{Map, Value, json}; + +const DEFAULT_MAX_COST: u64 = 11_000_000_000; +const DEFAULT_PREV_TX_HEIGHT: u32 = 10_000_000; + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum ExplainLevel { + Conditions, + Deep, +} + +impl Default for ExplainLevel { + fn default() -> Self { + Self::Deep + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct InspectionOutput { + pub schema_version: String, + pub tool: ToolInfo, + pub input: InputInfo, + pub result: ResultInfo, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ToolInfo { + pub name: String, + pub version: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct InputInfo { + pub kind: String, + pub notes: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ResultInfo { + pub status: String, + pub error: Option, + pub summary: Summary, + pub spends: Vec, + pub signatures: SignatureSummary, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ErrorInfo { + pub kind: String, + pub message: String, +} + +#[derive(Debug, Clone, Serialize, Default)] +pub struct Summary { + pub removals: Vec, + pub additions: Vec, + pub fee_mojos: u64, + pub net_xch_delta_by_puzzle_hash: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct NetDelta { + pub puzzle_hash: String, + pub delta_mojos: i128, +} + +#[derive(Debug, Clone, Serialize)] +pub struct SpendAnalysis { + pub coin_spend: CoinSpendView, + pub evaluation: EvaluationInfo, + pub clvm: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct CoinSpendView { + pub coin: CoinRef, + pub puzzle_reveal: String, + pub solution: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct CoinRef { + pub coin_id: String, + pub parent_coin_id: String, + pub puzzle_hash: String, + pub amount: u64, +} + +#[derive(Debug, Clone, Serialize)] +pub struct EvaluationInfo { + pub status: String, + pub cost: u64, + pub conditions: Vec, + pub additions: Vec, + pub failure: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ConditionInfo { + pub opcode: String, + pub args: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct FailureInfo { + pub kind: String, + pub message: String, +} + +#[derive(Debug, Clone, Serialize, Default)] +pub struct SignatureSummary { + pub aggregated_signature: String, + pub agg_sig_me: Vec, + pub agg_sig_unsafe: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct AggSigInfo { + pub pubkey: String, + pub msg: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ClvmInfo { + pub puzzle_opd: String, + pub solution_opd: String, + pub tree_hash: String, + pub uses_backrefs: bool, +} + +#[derive(Debug, Clone)] +enum InputKind { + Mempool, + Block, + Coin, +} + +impl InputKind { + fn as_str(&self) -> &'static str { + match self { + Self::Mempool => "mempool", + Self::Block => "block", + Self::Coin => "coin", + } + } +} + +pub fn inspect_json(input_json: &str, explain_level: ExplainLevel) -> Result { + let (kind, bundle, notes) = load_input(input_json)?; + inspect_bundle(kind, bundle, notes, explain_level) +} + +pub fn inspect_json_string(input_json: &str, pretty: bool, explain_level: ExplainLevel) -> Result { + let output = inspect_json(input_json, explain_level)?; + if pretty { + Ok(serde_json::to_string_pretty(&output)?) + } else { + Ok(serde_json::to_string(&output)?) + } +} + +fn inspect_bundle( + kind: InputKind, + spend_bundle: SpendBundle, + notes: Vec, + explain_level: ExplainLevel, +) -> Result { + let mut allocator = make_allocator(LIMIT_HEAP); + let eval = get_conditions_from_spendbundle( + &mut allocator, + &spend_bundle, + DEFAULT_MAX_COST, + DEFAULT_PREV_TX_HEIGHT, + &TEST_CONSTANTS, + ); + + match eval { + Ok(conditions) => { + let owned = OwnedSpendBundleConditions::from(&allocator, conditions); + Ok(build_success_output(kind, notes, spend_bundle, owned, explain_level)) + } + Err(err) => Ok(build_error_output( + kind, + notes, + spend_bundle, + &format!("{err:?}"), + explain_level, + )), + } +} + +fn build_success_output( + kind: InputKind, + notes: Vec, + spend_bundle: SpendBundle, + owned: OwnedSpendBundleConditions, + explain_level: ExplainLevel, +) -> InspectionOutput { + let mut spends = Vec::::new(); + let mut removals = Vec::::new(); + let mut additions = Vec::::new(); + let mut agg_sig_me = Vec::::new(); + let mut agg_sig_unsafe = Vec::::new(); + + let spend_count = spend_bundle.coin_spends.len().min(owned.spends.len()); + for idx in 0..spend_count { + let spend = &spend_bundle.coin_spends[idx]; + let conds = &owned.spends[idx]; + let spend_analysis = analyze_single_spend(spend, conds, explain_level, &mut agg_sig_me); + removals.push(coin_ref_from_coin(&spend.coin)); + additions.extend(spend_analysis.evaluation.additions.iter().cloned()); + spends.push(spend_analysis); + } + + for (pk, msg) in &owned.agg_sig_unsafe { + agg_sig_unsafe.push(AggSigInfo { + pubkey: encode_hex_prefixed(&pk.to_bytes()), + msg: encode_hex_prefixed(msg.as_ref()), + }); + } + + removals.sort_by(|a, b| a.coin_id.cmp(&b.coin_id)); + additions.sort_by(|a, b| a.coin_id.cmp(&b.coin_id)); + agg_sig_me.sort_by(|a, b| a.pubkey.cmp(&b.pubkey).then(a.msg.cmp(&b.msg))); + agg_sig_unsafe.sort_by(|a, b| a.pubkey.cmp(&b.pubkey).then(a.msg.cmp(&b.msg))); + + let fee_mojos = owned + .removal_amount + .saturating_sub(owned.addition_amount) + .try_into() + .unwrap_or(u64::MAX); + let net_xch_delta_by_puzzle_hash = compute_net_delta(&removals, &additions); + + InspectionOutput { + schema_version: "coinset.inspect.v1".to_string(), + tool: ToolInfo { + name: "coinset-inspect".to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }, + input: InputInfo { + kind: kind.as_str().to_string(), + notes, + }, + result: ResultInfo { + status: "ok".to_string(), + error: None, + summary: Summary { + removals, + additions, + fee_mojos, + net_xch_delta_by_puzzle_hash, + }, + spends, + signatures: SignatureSummary { + aggregated_signature: encode_hex_prefixed( + &spend_bundle.aggregated_signature.to_bytes(), + ), + agg_sig_me, + agg_sig_unsafe, + }, + }, + } +} + +fn build_error_output( + kind: InputKind, + notes: Vec, + spend_bundle: SpendBundle, + message: &str, + explain_level: ExplainLevel, +) -> InspectionOutput { + let mut spends = Vec::new(); + let mut removals = Vec::new(); + for spend in &spend_bundle.coin_spends { + removals.push(coin_ref_from_coin(&spend.coin)); + let clvm = if explain_level == ExplainLevel::Deep { + Some(analyze_clvm(spend)) + } else { + None + }; + spends.push(SpendAnalysis { + coin_spend: CoinSpendView { + coin: coin_ref_from_coin(&spend.coin), + puzzle_reveal: encode_hex_prefixed(spend.puzzle_reveal.as_ref()), + solution: encode_hex_prefixed(spend.solution.as_ref()), + }, + evaluation: EvaluationInfo { + status: "failed".to_string(), + cost: 0, + conditions: Vec::new(), + additions: Vec::new(), + failure: Some(FailureInfo { + kind: "validation_error".to_string(), + message: message.to_string(), + }), + }, + clvm, + }); + } + + InspectionOutput { + schema_version: "coinset.inspect.v1".to_string(), + tool: ToolInfo { + name: "coinset-inspect".to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }, + input: InputInfo { + kind: kind.as_str().to_string(), + notes, + }, + result: ResultInfo { + status: "failed".to_string(), + error: Some(ErrorInfo { + kind: "validation_error".to_string(), + message: message.to_string(), + }), + summary: Summary { + removals, + additions: Vec::new(), + fee_mojos: 0, + net_xch_delta_by_puzzle_hash: Vec::new(), + }, + spends, + signatures: SignatureSummary { + aggregated_signature: encode_hex_prefixed( + &spend_bundle.aggregated_signature.to_bytes(), + ), + agg_sig_me: Vec::new(), + agg_sig_unsafe: Vec::new(), + }, + }, + } +} + +fn analyze_single_spend( + spend: &CoinSpend, + conds: &OwnedSpendConditions, + explain_level: ExplainLevel, + agg_sig_me_out: &mut Vec, +) -> SpendAnalysis { + let mut conditions = Vec::::new(); + let mut additions = Vec::::new(); + + add_signature_conditions( + &conds.agg_sig_me, + "AGG_SIG_ME", + &mut conditions, + agg_sig_me_out, + ); + add_signature_conditions( + &conds.agg_sig_parent, + "AGG_SIG_PARENT", + &mut conditions, + &mut Vec::new(), + ); + add_signature_conditions( + &conds.agg_sig_puzzle, + "AGG_SIG_PUZZLE", + &mut conditions, + &mut Vec::new(), + ); + add_signature_conditions( + &conds.agg_sig_amount, + "AGG_SIG_AMOUNT", + &mut conditions, + &mut Vec::new(), + ); + add_signature_conditions( + &conds.agg_sig_puzzle_amount, + "AGG_SIG_PUZZLE_AMOUNT", + &mut conditions, + &mut Vec::new(), + ); + add_signature_conditions( + &conds.agg_sig_parent_amount, + "AGG_SIG_PARENT_AMOUNT", + &mut conditions, + &mut Vec::new(), + ); + add_signature_conditions( + &conds.agg_sig_parent_puzzle, + "AGG_SIG_PARENT_PUZZLE", + &mut conditions, + &mut Vec::new(), + ); + + let mut create_coin = conds.create_coin.clone(); + create_coin.sort_by(|a, b| { + a.0.as_ref() + .cmp(b.0.as_ref()) + .then(a.1.cmp(&b.1)) + .then(a.2.as_ref().map(Bytes::as_ref).cmp(&b.2.as_ref().map(Bytes::as_ref))) + }); + for (puzzle_hash, amount, hint) in create_coin { + let new_coin = Coin::new(conds.coin_id, puzzle_hash, amount); + let coin_ref = coin_ref_from_coin(&new_coin); + let mut args = vec![json!(encode_hex_prefixed(puzzle_hash.as_ref())), json!(amount)]; + if let Some(ref hint) = hint { + args.push(json!([encode_hex_prefixed(hint.as_ref())])); + } + conditions.push(ConditionInfo { + opcode: "CREATE_COIN".to_string(), + args, + }); + additions.push(coin_ref); + } + + add_optional_assertion( + "ASSERT_HEIGHT_RELATIVE", + conds.height_relative.map(u64::from), + &mut conditions, + ); + add_optional_assertion( + "ASSERT_SECONDS_RELATIVE", + conds.seconds_relative, + &mut conditions, + ); + add_optional_assertion( + "ASSERT_BEFORE_HEIGHT_RELATIVE", + conds.before_height_relative.map(u64::from), + &mut conditions, + ); + add_optional_assertion( + "ASSERT_BEFORE_SECONDS_RELATIVE", + conds.before_seconds_relative, + &mut conditions, + ); + add_optional_assertion( + "ASSERT_MY_BIRTH_HEIGHT", + conds.birth_height.map(u64::from), + &mut conditions, + ); + add_optional_assertion( + "ASSERT_MY_BIRTH_SECONDS", + conds.birth_seconds, + &mut conditions, + ); + + SpendAnalysis { + coin_spend: CoinSpendView { + coin: coin_ref_from_coin(&spend.coin), + puzzle_reveal: encode_hex_prefixed(spend.puzzle_reveal.as_ref()), + solution: encode_hex_prefixed(spend.solution.as_ref()), + }, + evaluation: EvaluationInfo { + status: "ok".to_string(), + cost: conds.execution_cost + conds.condition_cost, + conditions, + additions, + failure: None, + }, + clvm: if explain_level == ExplainLevel::Deep { + Some(analyze_clvm(spend)) + } else { + None + }, + } +} + +fn analyze_clvm(spend: &CoinSpend) -> ClvmInfo { + let mut allocator = ClvmAllocator::new(); + let (puzzle_opd, uses_backrefs_p) = disasm_bytes(&mut allocator, spend.puzzle_reveal.as_ref()); + let (solution_opd, uses_backrefs_s) = disasm_bytes(&mut allocator, spend.solution.as_ref()); + let uses_backrefs = uses_backrefs_p || uses_backrefs_s; + let tree_hash = tree_hash_from_bytes(spend.puzzle_reveal.as_ref()) + .map(|h| encode_hex_prefixed(h.as_ref())) + .unwrap_or_else(|_| encode_hex_prefixed(spend.coin.puzzle_hash.as_ref())); + ClvmInfo { + puzzle_opd, + solution_opd, + tree_hash, + uses_backrefs, + } +} + +fn disasm_bytes(allocator: &mut ClvmAllocator, bytes: &[u8]) -> (String, bool) { + let uses_backrefs = bytes.contains(&0xfe); + match node_from_bytes_backrefs(allocator, bytes) { + Ok(node) => ( + disassemble(allocator, node, Some(OPERATORS_LATEST_VERSION)), + uses_backrefs, + ), + Err(err) => (format!(""), uses_backrefs), + } +} + +fn add_signature_conditions( + pairs: &[(PublicKey, Bytes)], + opcode: &str, + conditions: &mut Vec, + agg_sig_out: &mut Vec, +) { + for (pk, msg) in pairs { + let pk_hex = encode_hex_prefixed(&pk.to_bytes()); + let msg_hex = encode_hex_prefixed(msg.as_ref()); + conditions.push(ConditionInfo { + opcode: opcode.to_string(), + args: vec![json!(pk_hex.clone()), json!(msg_hex.clone())], + }); + agg_sig_out.push(AggSigInfo { + pubkey: pk_hex, + msg: msg_hex, + }); + } +} + +fn add_optional_assertion(opcode: &str, value: Option, conditions: &mut Vec) { + if let Some(v) = value { + conditions.push(ConditionInfo { + opcode: opcode.to_string(), + args: vec![json!(v)], + }); + } +} + +fn compute_net_delta(removals: &[CoinRef], additions: &[CoinRef]) -> Vec { + let mut map = BTreeMap::::new(); + for coin in removals { + *map.entry(coin.puzzle_hash.clone()).or_insert(0) -= i128::from(coin.amount); + } + for coin in additions { + *map.entry(coin.puzzle_hash.clone()).or_insert(0) += i128::from(coin.amount); + } + map.into_iter() + .map(|(puzzle_hash, delta_mojos)| NetDelta { + puzzle_hash, + delta_mojos, + }) + .collect() +} + +fn coin_ref_from_coin(coin: &Coin) -> CoinRef { + CoinRef { + coin_id: encode_hex_prefixed(coin.coin_id().as_ref()), + parent_coin_id: encode_hex_prefixed(coin.parent_coin_info.as_ref()), + puzzle_hash: encode_hex_prefixed(coin.puzzle_hash.as_ref()), + amount: coin.amount, + } +} + +fn encode_hex_prefixed(bytes: &[u8]) -> String { + format!("0x{}", hex::encode(bytes)) +} + +fn decode_hex_prefixed(s: &str) -> Result> { + let raw = s + .strip_prefix("0x") + .or_else(|| s.strip_prefix("0X")) + .unwrap_or(s) + .trim(); + if raw.is_empty() { + return Ok(Vec::new()); + } + if raw.len() % 2 != 0 { + bail!("hex input must have even length"); + } + Ok(hex::decode(raw)?) +} + +fn normalize_hex_no_prefix(s: &str) -> Result { + let bytes = decode_hex_prefixed(s)?; + Ok(hex::encode(bytes)) +} + +fn load_input(input_json: &str) -> Result<(InputKind, SpendBundle, Vec)> { + let value: Value = serde_json::from_str(input_json)?; + let mut notes = Vec::::new(); + + // Mempool-ish wrappers + let scope = if let Some(wrapper) = value.get("mempool_item") { + notes.push("input contained mempool_item wrapper; using nested payload".to_string()); + wrapper + } else { + &value + }; + + // Spend bundle shapes + if let Some(sb) = scope.get("spend_bundle") { + let bundle = parse_spend_bundle_object(sb)?; + return Ok((InputKind::Mempool, bundle, notes)); + } + if let Some(sb_bytes) = scope.get("spend_bundle_bytes").and_then(Value::as_str) { + notes.push("input contained spend_bundle_bytes; decoded with streamable parser".to_string()); + let bundle = parse_spend_bundle_bytes(sb_bytes)?; + return Ok((InputKind::Mempool, bundle, notes)); + } + if scope.get("coin_spends").is_some() && scope.is_object() { + // Ambiguous shape: could be a full SpendBundle or a block-style coin spend list. + // Treat it as a SpendBundle only if it includes an aggregate signature. + if scope.get("aggregated_signature").is_some() { + let bundle = parse_spend_bundle_object(scope)?; + return Ok((InputKind::Mempool, bundle, notes)); + } + } + + // Block-ish shapes + if let Some(items) = value.get("block_spends") { + let spends = parse_coin_spend_list(items)?; + notes.push("block input normalized to SpendBundle with default aggregate signature".to_string()); + return Ok((InputKind::Block, SpendBundle::new(spends, Default::default()), notes)); + } + if let Some(items) = value.get("coin_spends") { + // Some endpoints return block objects with coin_spends. + let spends = parse_coin_spend_list(items)?; + notes.push("block input normalized to SpendBundle with default aggregate signature".to_string()); + return Ok((InputKind::Block, SpendBundle::new(spends, Default::default()), notes)); + } + if value.is_array() { + let spends = parse_coin_spend_list(&value)?; + notes.push("array input normalized to SpendBundle with default aggregate signature".to_string()); + return Ok((InputKind::Block, SpendBundle::new(spends, Default::default()), notes)); + } + + // Single coin spend shapes + if value.get("coin_spend").is_some() || value.get("puzzle_reveal").is_some() { + let spend_value = value.get("coin_spend").unwrap_or(&value); + let spend = parse_coin_spend(spend_value)?; + notes.push("coin input normalized to single-spend SpendBundle".to_string()); + return Ok((InputKind::Coin, SpendBundle::new(vec![spend], Default::default()), notes)); + } + + bail!("unsupported input shape (no spend bundle/spends found)") +} + +fn parse_spend_bundle_object(value: &Value) -> Result { + let normalized = normalize_spend_bundle_value(value)?; + serde_json::from_value(normalized).context("failed to parse spend bundle JSON") +} + +fn parse_spend_bundle_bytes(hex_value: &str) -> Result { + let bytes = decode_hex_prefixed(hex_value)?; + SpendBundle::from_bytes(&bytes).context("failed to parse spend bundle bytes") +} + +fn parse_coin_spend_list(value: &Value) -> Result> { + let arr = value.as_array().context("coin spend list must be an array")?; + let mut ret = Vec::with_capacity(arr.len()); + for item in arr { + ret.push(parse_coin_spend(item)?); + } + Ok(ret) +} + +fn parse_coin_spend(value: &Value) -> Result { + let normalized = normalize_coin_spend_value(value)?; + serde_json::from_value(normalized).context("failed to parse coin spend") +} + +fn normalize_spend_bundle_value(value: &Value) -> Result { + let mut obj = value + .as_object() + .context("spend bundle JSON must be an object")? + .clone(); + let coin_spends = obj + .get("coin_spends") + .cloned() + .context("spend bundle is missing coin_spends")?; + let normalized_spends = normalize_coin_spend_array(&coin_spends)?; + obj.insert("coin_spends".to_string(), normalized_spends); + if let Some(sig) = obj.get("aggregated_signature").and_then(Value::as_str) { + // Some JSON uses 0x prefix; chia-protocol serde expects hex without 0x. + obj.insert( + "aggregated_signature".to_string(), + Value::String(normalize_hex_no_prefix(sig)?), + ); + } + Ok(Value::Object(obj)) +} + +fn normalize_coin_spend_array(value: &Value) -> Result { + let arr = value + .as_array() + .context("coin_spends must be an array of coin spend objects")?; + let mut out = Vec::with_capacity(arr.len()); + for item in arr { + out.push(normalize_coin_spend_value(item)?); + } + Ok(Value::Array(out)) +} + +fn normalize_coin_spend_value(value: &Value) -> Result { + let obj = value + .as_object() + .context("coin spend entry must be an object")?; + let mut out = Map::new(); + for (k, v) in obj { + if k == "puzzle_reveal" || k == "solution" { + let s = v + .as_str() + .with_context(|| format!("{k} must be a hex string"))?; + out.insert(k.clone(), json!(normalize_hex_no_prefix(s)?)); + } else { + out.insert(k.clone(), v.clone()); + } + } + Ok(Value::Object(out)) +} + +#[cfg(test)] +mod tests { + use super::*; + use chia_protocol::{Coin, Program}; + + fn sample_spend_bundle() -> SpendBundle { + let parent = [0x11_u8; 32]; + let puzzle = Program::from(vec![0x01_u8]); + let puzzle_hash = tree_hash_from_bytes(puzzle.as_ref()).expect("tree hash"); + let coin = Coin::new(parent.into(), puzzle_hash.into(), 1); + let solution = Program::from(vec![0x80]); + let spend = CoinSpend::new(coin, puzzle, solution); + SpendBundle::new(vec![spend], Default::default()) + } + + #[test] + fn mempool_wrapper_spend_bundle_parses() { + let bundle = sample_spend_bundle(); + let blob = json!({ "spend_bundle": bundle }); + let (kind, parsed, _notes) = + load_input(&serde_json::to_string(&blob).expect("json")).expect("parse"); + assert_eq!(kind.as_str(), "mempool"); + assert_eq!(parsed.coin_spends.len(), 1); + } + + #[test] + fn block_spends_object_parses() { + let bundle = sample_spend_bundle(); + let blob = json!({ "coin_spends": bundle.coin_spends }); + let (kind, parsed, _notes) = + load_input(&serde_json::to_string(&blob).expect("json")).expect("parse"); + assert_eq!(kind.as_str(), "block"); + assert_eq!(parsed.coin_spends.len(), 1); + } +} From 1f5f886037fd8597811204f84fdac3367ca6705c Mon Sep 17 00:00:00 2001 From: Cameron Cooper Date: Tue, 3 Mar 2026 14:58:49 -0600 Subject: [PATCH 5/9] add -lm --- internal/coinsetffi/coinsetffi.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/coinsetffi/coinsetffi.go b/internal/coinsetffi/coinsetffi.go index 2c81cf0..3778587 100644 --- a/internal/coinsetffi/coinsetffi.go +++ b/internal/coinsetffi/coinsetffi.go @@ -10,6 +10,7 @@ package coinsetffi #cgo linux,amd64 LDFLAGS: -L${SRCDIR}/../../cgo-lib/x86_64-unknown-linux-gnu #cgo windows,amd64 LDFLAGS: -L${SRCDIR}/../../cgo-lib/x86_64-pc-windows-gnu #cgo LDFLAGS: -lcoinset +#cgo linux LDFLAGS: -lm char* coinset_inspect(const uint8_t* input_ptr, size_t input_len, uint32_t flags); char* coinset_clvm_decompile(const uint8_t* input_ptr, size_t input_len, uint32_t flags); From fb2d4a23ed67efcefa1118ccc7bded13fbb5e874 Mon Sep 17 00:00:00 2001 From: Cameron Cooper Date: Tue, 3 Mar 2026 15:40:39 -0600 Subject: [PATCH 6/9] build all three os on pr --- .github/workflows/build-pr.yml | 98 +++++++++++++++++++++++++++++----- Makefile | 59 ++++++++++++++++++++ 2 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 Makefile diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index e674d62..d30f518 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -4,35 +4,109 @@ on: pull_request: permissions: - contents: write + contents: read jobs: - goreleaser: - runs-on: ubuntu-latest - environment: goreleaser + goreleaser-build: + name: Build (${{ matrix.name }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - name: linux-amd64 + os: ubuntu-latest + rust_target: x86_64-unknown-linux-gnu + goreleaser_config: .goreleaser.linux.yaml + cgo_lib_dir: cgo-lib/x86_64-unknown-linux-gnu + - name: macos-arm64 + os: macos-latest + rust_target: aarch64-apple-darwin + goreleaser_config: .goreleaser.darwin.yaml + cgo_lib_dir: cgo-lib/aarch64-apple-darwin + - name: windows-amd64 + os: windows-latest + rust_target: x86_64-pc-windows-gnu + goreleaser_config: .goreleaser.windows.yaml + cgo_lib_dir: cgo-lib/x86_64-pc-windows-gnu + steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Set up Go uses: actions/setup-go@v5 with: go-version: stable + + - name: Setup MSYS2 (mingw64) + if: runner.os == 'Windows' + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + update: true + install: >- + mingw-w64-x86_64-gcc + + - name: Add MinGW to PATH + if: runner.os == 'Windows' + shell: pwsh + run: | + "C:\\msys64\\mingw64\\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: Setup Rust uses: dtolnay/rust-toolchain@stable with: - targets: x86_64-unknown-linux-gnu - - name: Build coinset-inspect staticlib + targets: ${{ matrix.rust_target }} + + - name: Build libcoinset staticlib + shell: bash + if: runner.os != 'Windows' + run: | + if [ "${{ runner.os }}" = "macOS" ]; then export MACOSX_DEPLOYMENT_TARGET=12.0; fi + cargo build --release -p coinset-ffi --target "${{ matrix.rust_target }}" + mkdir -p "${{ matrix.cgo_lib_dir }}" + cp "target/${{ matrix.rust_target }}/release/libcoinset.a" "${{ matrix.cgo_lib_dir }}/" + + - name: Build libcoinset staticlib (windows) + if: runner.os == 'Windows' + shell: pwsh run: | - cargo build --release -p coinset-ffi --target x86_64-unknown-linux-gnu - mkdir -p cgo-lib/x86_64-unknown-linux-gnu - cp "target/x86_64-unknown-linux-gnu/release/libcoinset.a" cgo-lib/x86_64-unknown-linux-gnu/ - - name: Run GoReleaser (Build) + cargo build --release -p coinset-ffi --target "${{ matrix.rust_target }}" + New-Item -ItemType Directory -Force -Path "${{ matrix.cgo_lib_dir }}" | Out-Null + Copy-Item "target\\${{ matrix.rust_target }}\\release\\libcoinset.a" "${{ matrix.cgo_lib_dir }}\\" + + - name: Run GoReleaser (Build linux) + if: runner.os == 'Linux' + uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser + version: ~> v1 + args: build --clean --skip=validate -f ${{ matrix.goreleaser_config }} + env: + CGO_LDFLAGS: -L${{ github.workspace }}/${{ matrix.cgo_lib_dir }} + + - name: Run GoReleaser (Build macOS) + if: runner.os == 'macOS' + uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser + version: ~> v1 + args: build --clean --skip=validate -f ${{ matrix.goreleaser_config }} + env: + MACOSX_DEPLOYMENT_TARGET: "12.0" + CGO_CFLAGS: -mmacosx-version-min=12.0 + CGO_LDFLAGS: -mmacosx-version-min=12.0 -L${{ github.workspace }}/${{ matrix.cgo_lib_dir }} + + - name: Run GoReleaser (Build windows) + if: runner.os == 'Windows' uses: goreleaser/goreleaser-action@v5 with: distribution: goreleaser version: ~> v1 - args: build --clean --skip=validate -f .goreleaser.linux.yaml + args: build --clean --skip=validate -f ${{ matrix.goreleaser_config }} env: - CGO_LDFLAGS: -L${{ github.workspace }}/cgo-lib/x86_64-unknown-linux-gnu + CC: gcc + CGO_LDFLAGS: -L${{ github.workspace }}/${{ matrix.cgo_lib_dir }} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..18ee466 --- /dev/null +++ b/Makefile @@ -0,0 +1,59 @@ +.PHONY: all build rustlib test clean run + +GOOS ?= $(shell go env GOOS) +GOARCH ?= $(shell go env GOARCH) + +ifeq ($(GOOS),darwin) + MACOSX_DEPLOYMENT_TARGET ?= 12.0 + export MACOSX_DEPLOYMENT_TARGET + + # Ensure cgo compilation uses the same min version as linking. + CGO_CFLAGS ?= -mmacosx-version-min=$(MACOSX_DEPLOYMENT_TARGET) + CGO_LDFLAGS ?= -mmacosx-version-min=$(MACOSX_DEPLOYMENT_TARGET) + export CGO_CFLAGS + export CGO_LDFLAGS +endif + +# Map GOOS/GOARCH to a Rust target triple we build the staticlib for. +ifeq ($(GOOS),darwin) + ifeq ($(GOARCH),arm64) + RUST_TARGET ?= aarch64-apple-darwin + else + RUST_TARGET ?= x86_64-apple-darwin + endif +else ifeq ($(GOOS),linux) + ifeq ($(GOARCH),amd64) + RUST_TARGET ?= x86_64-unknown-linux-gnu + else + RUST_TARGET ?= aarch64-unknown-linux-gnu + endif +else ifeq ($(GOOS),windows) + RUST_TARGET ?= x86_64-pc-windows-gnu +else + RUST_TARGET ?= $(shell rustc -vV | awk '/host:/ {print $$2}') +endif + +LIBDIR := cgo-lib/$(RUST_TARGET) +LIB := $(LIBDIR)/libcoinset.a + +all: build + +$(LIB): + mkdir -p "$(LIBDIR)" + cargo build --release -p coinset-ffi --target "$(RUST_TARGET)" + cp "target/$(RUST_TARGET)/release/libcoinset.a" "$(LIBDIR)/" + +rustlib: $(LIB) + +build: rustlib + CGO_ENABLED=1 go build -o bin/coinset ./cmd/coinset + +run: rustlib + CGO_ENABLED=1 go run ./cmd/coinset + +test: rustlib + cargo test -p coinset-inspect-core + CGO_ENABLED=1 go test ./... + +clean: + rm -rf bin cgo-lib From a01508b4ee513ec6c9c949660001f08bea92fc9d Mon Sep 17 00:00:00 2001 From: Cameron Cooper Date: Wed, 4 Mar 2026 15:37:29 -0600 Subject: [PATCH 7/9] added SKILL.md and curry --- Cargo.lock | 745 ++++++++++++++++- Cargo.toml | 6 + Makefile | 10 +- README.md | 30 + SKILL.md | 731 +++++++++++++++++ crates/coinset-clvm-core/Cargo.toml | 2 + crates/coinset-clvm-core/src/lib.rs | 312 ++++++- crates/coinset-ffi/Cargo.toml | 5 + crates/coinset-ffi/src/lib.rs | 142 +++- crates/coinset-inspect-core/Cargo.toml | 3 + crates/coinset-inspect-core/src/lib.rs | 37 +- crates/coinset-inspect-core/src/recognize.rs | 811 +++++++++++++++++++ internal/cmd/coinset/clvm.go | 87 +- internal/cmd/coinset/inspect_test.go | 54 ++ internal/coinsetffi/coinsetffi.go | 63 ++ internal/coinsetffi/coinsetffi_nocgo.go | 12 + internal/coinsetffi/coinsetffi_test.go | 70 ++ 17 files changed, 3080 insertions(+), 40 deletions(-) create mode 100644 SKILL.md create mode 100644 crates/coinset-inspect-core/src/recognize.rs create mode 100644 internal/cmd/coinset/inspect_test.go create mode 100644 internal/coinsetffi/coinsetffi_test.go diff --git a/Cargo.lock b/Cargo.lock index e70c094..f7b3db1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "autocfg" version = "1.5.0" @@ -41,12 +47,45 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bigdecimal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "binascii" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "serde", + "unicode-normalization", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "hex-conservative", +] + [[package]] name = "bitflags" version = "2.11.0" @@ -121,7 +160,7 @@ dependencies = [ "hkdf", "linked-hash-map", "sha2", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -137,7 +176,7 @@ dependencies = [ "hkdf", "linked-hash-map", "sha2", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -155,7 +194,7 @@ dependencies = [ "linked-hash-map", "serde", "sha2", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -171,12 +210,12 @@ dependencies = [ "chia-sha2 0.36.1", "chia-traits 0.36.1", "chia_streamable_macro 0.36.1", - "clvm-traits", + "clvm-traits 0.36.1", "clvm-utils", "clvmr", "hex", "hex-literal", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -190,7 +229,7 @@ dependencies = [ "chia-sha2 0.36.1", "chia-traits 0.36.1", "chia_streamable_macro 0.36.1", - "clvm-traits", + "clvm-traits 0.36.1", "clvm-utils", "clvmr", "hex", @@ -208,7 +247,7 @@ dependencies = [ "chia-protocol", "chia-puzzles", "chia-sha2 0.36.1", - "clvm-traits", + "clvm-traits 0.36.1", "clvm-utils", "clvmr", "hex-literal", @@ -225,6 +264,101 @@ dependencies = [ "hex-literal", ] +[[package]] +name = "chia-sdk-derive" +version = "0.33.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?tag=0.33.0#cee7c75a1930e824585408de277973e2d229eb72" +dependencies = [ + "convert_case 0.8.0", + "quote", + "syn", +] + +[[package]] +name = "chia-sdk-driver" +version = "0.33.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?tag=0.33.0#cee7c75a1930e824585408de277973e2d229eb72" +dependencies = [ + "bigdecimal", + "bip39", + "chia-bls 0.36.1", + "chia-consensus", + "chia-protocol", + "chia-puzzle-types", + "chia-puzzles", + "chia-sdk-signer", + "chia-sdk-types", + "chia-secp", + "chia-sha2 0.36.1", + "chia-traits 0.36.1", + "chia_streamable_macro 0.36.1", + "clvm-traits 0.36.1", + "clvm-utils", + "clvmr", + "getrandom 0.3.4", + "hex", + "hex-literal", + "indexmap", + "num-bigint", + "rand 0.9.2", + "rand_chacha 0.9.0", + "thiserror 2.0.18", +] + +[[package]] +name = "chia-sdk-signer" +version = "0.33.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?tag=0.33.0#cee7c75a1930e824585408de277973e2d229eb72" +dependencies = [ + "chia-bls 0.36.1", + "chia-consensus", + "chia-protocol", + "chia-sdk-types", + "chia-secp", + "chia-sha2 0.36.1", + "clvm-traits 0.36.1", + "clvmr", + "k256", + "rue-lir", + "thiserror 2.0.18", +] + +[[package]] +name = "chia-sdk-types" +version = "0.33.0" +source = "git+https://github.com/xch-dev/chia-wallet-sdk?tag=0.33.0#cee7c75a1930e824585408de277973e2d229eb72" +dependencies = [ + "chia-bls 0.36.1", + "chia-consensus", + "chia-protocol", + "chia-puzzle-types", + "chia-puzzles", + "chia-sdk-derive", + "chia-secp", + "chia-sha2 0.36.1", + "chialisp", + "clvm-traits 0.36.1", + "clvm-utils", + "clvmr", + "hex-literal", + "rue-compiler", + "rue-lir", + "rue-options", + "thiserror 2.0.18", +] + +[[package]] +name = "chia-secp" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fc00f8ad264eb260fd69fe2c7cb10c45b0a0699765e8ed8eac9be064ba3b55" +dependencies = [ + "chia-sha2 0.36.1", + "hex", + "k256", + "p256", +] + [[package]] name = "chia-serde" version = "0.36.1" @@ -270,7 +404,7 @@ checksum = "52463c56cdf35aac55ecb35ffc0a615f0780f33f144503957e83f5b1ad92da3c" dependencies = [ "chia-sha2 0.28.2", "chia_streamable_macro 0.28.2", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -281,7 +415,7 @@ checksum = "143545b13658929fce2e770a499fd4755f04a4d5f6459dc76a79068ae192a8c5" dependencies = [ "chia-sha2 0.30.0", "chia_streamable_macro 0.30.0", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -292,7 +426,7 @@ checksum = "1f4922b447b2d8418213948af1a448c3ca7b84e149b51b2c87a2e00e80bb19b0" dependencies = [ "chia-sha2 0.36.1", "chia_streamable_macro 0.36.1", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -301,7 +435,7 @@ version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc6b807a9c04440d3f30b5534b84b462b996b34eee7fe0b7f8737ae6dfc6b9d4" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn", @@ -313,7 +447,7 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25bd4fbc3f05375c267ee4cb2a4a1bf8125fa0d817e57d7b141993c35994c751" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn", @@ -325,7 +459,7 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b60cefc5fe39f695816d42a327cbefad3d6d6a8ecadad1b58d7507067c25da8" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn", @@ -372,6 +506,17 @@ dependencies = [ "syn", ] +[[package]] +name = "clvm-traits" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e74489df9233fdf7605d79032fe18f808ff33d7e69a9932db4c96ffd0e50e9" +dependencies = [ + "clvmr", + "num-bigint", + "thiserror 1.0.69", +] + [[package]] name = "clvm-traits" version = "0.36.1" @@ -379,10 +524,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96380e11b921106bd3d2e24f8b85b6de2f99a760a6d5f0bd6d68ec096695b34b" dependencies = [ "chia-bls 0.36.1", + "chia-secp", "clvm-derive", "clvmr", "num-bigint", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -392,7 +538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65adea81c7ab1659180a4ffb10a0df63eb049b5b8de2caa8bf2d762f55c7c4db" dependencies = [ "chia-sha2 0.36.1", - "clvm-traits", + "clvm-traits 0.36.1", "clvmr", "hex", "hex-literal", @@ -416,10 +562,10 @@ dependencies = [ "num-integer", "num-traits", "p256", - "rand", + "rand 0.8.5", "sha1", "sha3", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -428,6 +574,8 @@ version = "0.1.0" dependencies = [ "anyhow", "chialisp", + "clvm-traits 0.36.1", + "clvm-utils", "clvmr", "hex", "serde", @@ -439,8 +587,11 @@ name = "coinset-ffi" version = "0.1.0" dependencies = [ "anyhow", + "chia-protocol", + "clvm-utils", "coinset-clvm-core", "coinset-inspect-core", + "serde", "serde_json", ] @@ -452,6 +603,9 @@ dependencies = [ "chia-bls 0.36.1", "chia-consensus", "chia-protocol", + "chia-puzzle-types", + "chia-sdk-driver", + "chia-sdk-types", "chia-traits 0.36.1", "chialisp", "clvm-utils", @@ -461,12 +615,45 @@ dependencies = [ "serde_json", ] +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys", +] + [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "convert_case" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -483,7 +670,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -509,6 +696,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + [[package]] name = "digest" version = "0.10.7" @@ -541,6 +751,12 @@ dependencies = [ "spki", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "elliptic-curve" version = "0.13.8" @@ -555,7 +771,7 @@ dependencies = [ "group", "pem-rfc7468", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -598,7 +814,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -620,6 +836,48 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "generic-array" version = "0.14.9" @@ -644,6 +902,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -652,7 +922,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -670,10 +940,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -716,6 +992,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + [[package]] name = "hex-literal" version = "0.4.1" @@ -767,6 +1052,15 @@ dependencies = [ "rustversion", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -824,6 +1118,12 @@ version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -882,6 +1182,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -950,6 +1261,12 @@ dependencies = [ "sha2", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -959,6 +1276,12 @@ dependencies = [ "base64ct", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "pkcs8" version = "0.10.2" @@ -1004,7 +1327,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.10+spec-1.0.0", ] [[package]] @@ -1035,6 +1367,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -1054,8 +1392,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1065,7 +1413,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1077,6 +1435,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "regex" version = "1.12.3" @@ -1106,6 +1473,12 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "rfc6979" version = "0.4.0" @@ -1116,6 +1489,193 @@ dependencies = [ "subtle", ] +[[package]] +name = "rowan" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "rustc-hash", + "text-size", +] + +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + +[[package]] +name = "rue-ast" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8feffcdd1f81db8d83980714cdd15d53608f7cc49e4d4a1c52d0fe1d523c4b" +dependencies = [ + "paste", + "rue-parser", +] + +[[package]] +name = "rue-compiler" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b58c388afe6e7fb78e63f43b455c57b6f788f1e2b65ae895e9ff38039ab2fdc" +dependencies = [ + "clvmr", + "hex", + "id-arena", + "indexmap", + "log", + "num-bigint", + "num-traits", + "rowan", + "rue-ast", + "rue-diagnostic", + "rue-hir", + "rue-lexer", + "rue-lir", + "rue-options", + "rue-parser", + "rue-types", + "thiserror 2.0.18", +] + +[[package]] +name = "rue-diagnostic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a953cccb3041f21c77e80ea8fdb03e7214b91195d939f3146f054e27020ea1a9" +dependencies = [ + "derive_more", + "thiserror 2.0.18", +] + +[[package]] +name = "rue-hir" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d73674bfcd164e453dbeabfa99cb952827abf7bc0540ed5bf6d2ccea97d06a" +dependencies = [ + "derive_more", + "hex", + "id-arena", + "indexmap", + "log", + "num-bigint", + "rue-diagnostic", + "rue-lir", + "rue-options", + "rue-types", +] + +[[package]] +name = "rue-lexer" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68345f23ba80d548d4a671de14e455de74ae58fccfa436db105b35308a1780ed" + +[[package]] +name = "rue-lir" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099f3c3d6b432bb94cf8876b7f71b650d6887ae9193e8063c529feca37276b6f" +dependencies = [ + "chialisp", + "clvm-traits 0.28.1", + "clvmr", + "colored", + "id-arena", + "num-bigint", + "num-integer", + "sha2", + "sha3", + "thiserror 2.0.18", +] + +[[package]] +name = "rue-options" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f94c541b1397b7fbc4aaba36440c0bc11ddb703b024a424dd2fe193ec413b7" +dependencies = [ + "serde", + "thiserror 2.0.18", + "toml", +] + +[[package]] +name = "rue-parser" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d23577d98f535c02d2898e0a0e9cdf04b82d1ea91b15cd31e66141bce73c4c5" +dependencies = [ + "derive_more", + "indexmap", + "itertools", + "num-derive", + "num-traits", + "rowan", + "rue-diagnostic", + "rue-lexer", +] + +[[package]] +name = "rue-types" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6dc5f584d1ff7cf562737aa990d746def396ca4211c8f56c2d28d6dc00718b" +dependencies = [ + "clvmr", + "derive_more", + "hex", + "id-arena", + "indexmap", + "log", + "rstest", + "rue-diagnostic", + "thiserror 2.0.18", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.4" @@ -1207,6 +1767,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + [[package]] name = "sha1" version = "0.10.6" @@ -1252,9 +1821,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "spki" version = "0.7.3" @@ -1307,13 +1882,28 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -1327,6 +1917,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "threadpool" version = "1.8.1" @@ -1336,12 +1937,51 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.14", +] + [[package]] name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.19.15" @@ -1349,10 +1989,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", - "toml_datetime", - "winnow", + "toml_datetime 0.6.11", + "winnow 0.5.40", ] +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow 0.7.14", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow 0.7.14", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + [[package]] name = "typenum" version = "1.19.0" @@ -1365,6 +2032,15 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -1512,6 +2188,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index e8eb266..017570d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,3 +24,9 @@ chia-traits = "0.36.1" clvm-utils = "0.36.1" clvmr = "=0.16.2" chialisp = "0.4.1" +clvm-traits = { version = "0.36.1", features = ["derive", "chia-bls", "chia-secp"] } + +# Wallet SDK (layer puzzle recognition) +chia-sdk-driver = { git = "https://github.com/xch-dev/chia-wallet-sdk", tag = "0.33.0" } +chia-sdk-types = { git = "https://github.com/xch-dev/chia-wallet-sdk", tag = "0.33.0" } +chia-puzzle-types = "0.36.1" diff --git a/Makefile b/Makefile index 18ee466..9ff9c01 100644 --- a/Makefile +++ b/Makefile @@ -38,22 +38,22 @@ LIB := $(LIBDIR)/libcoinset.a all: build -$(LIB): +rustlib: mkdir -p "$(LIBDIR)" cargo build --release -p coinset-ffi --target "$(RUST_TARGET)" cp "target/$(RUST_TARGET)/release/libcoinset.a" "$(LIBDIR)/" -rustlib: $(LIB) - build: rustlib - CGO_ENABLED=1 go build -o bin/coinset ./cmd/coinset + # Go's build cache does not account for changes in linked static libraries. + # Force a rebuild so updates to libcoinset.a are reflected in the binary. + CGO_ENABLED=1 go build -a -o bin/coinset ./cmd/coinset run: rustlib CGO_ENABLED=1 go run ./cmd/coinset test: rustlib cargo test -p coinset-inspect-core - CGO_ENABLED=1 go test ./... + CGO_ENABLED=1 go test -count=1 ./... clean: rm -rf bin cgo-lib diff --git a/README.md b/README.md index 3621ba0..f1a97de 100644 --- a/README.md +++ b/README.md @@ -100,10 +100,40 @@ Notes: ### CLVM tools (`coinset clvm`) Local CLVM helpers are available under `coinset clvm`: +- Inputs accept **either** hex bytes (e.g. `0xff0101`) **or** a readable s-expression (e.g. `"(q . 1)"`). +- Outputs are JSON so you can pipe through `--raw -q` / `jq`. + ```bash coinset clvm decompile 0xff0101 coinset clvm compile "(q . 1)" coinset clvm run "(q . 1)" "()" --cost +coinset clvm tree_hash "(q . 1)" +coinset clvm curry "(q . 1)" 42 75 +coinset clvm uncurry 0xff02ffff01ff0101ffff04ffff0142ffff04ffff0175ff01808080 +``` + +#### `coinset clvm tree_hash` +Compute a program's tree hash. + +```bash +coinset clvm tree_hash "(q . 1)" +coinset clvm tree_hash 0xff0101 +``` + +#### `coinset clvm curry` +Curry arguments into a module. Arguments are provided as **individual CLI parameters**. + +```bash +coinset clvm curry "(q . 1)" 42 75 +coinset clvm curry 0xff0101 0x2a 0x4b +``` + +#### `coinset clvm uncurry` +Attempt to parse a curried program and return the original module + arguments. + +```bash +coinset clvm uncurry 0xff02ffff01ff0101ffff04ffff0142ffff04ffff0175ff01808080 +coinset clvm uncurry "(a (q . 1) (c (q . 42) (c (q . 75) 1)))" ``` ## Available Commands diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..2968bb4 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,731 @@ +# coinset CLI + +Use this tool to query, inspect, and investigate the Chia blockchain. `coinset` wraps the Chia Full Node RPC (hosted at api.coinset.org or any full node) and includes built-in spend inspection and local CLVM utilities. + +## When to use this skill + +- Query blockchain state, blocks, coins, mempool items +- Inspect spend bundles and coin spends (conditions, cost, puzzle recognition) +- Convert between addresses and puzzle hashes +- Compute coin IDs +- Decompile, compile, or run CLVM programs +- Monitor real-time blockchain events + +## Command grammar + +``` +coinset [args...] [flags...] +``` + +### Global flags + +| Flag | Short | Description | +|------|-------|-------------| +| `--query ` | `-q` | Apply a jq filter to output (default: `.`) | +| `--raw` | `-r` | Output raw JSON (no color/formatting) | +| `--describe` | `-d` | Add human-readable `_description` fields (XCH amounts, relative timestamps) | +| `--inspect` | | Replace output with interpreted spend analysis (only works when output contains spend data) | +| `--testnet` | `-t` | Use testnet11 (`https://testnet11.api.coinset.org`) | +| `--local` | `-l` | Use local full node (auto-config) | +| `--api ` | `-a` | Custom API endpoint (mutually exclusive with `--testnet`/`--mainnet`) | +| `--mainnet` | | Use mainnet (default) | + +### Input normalization + +- **Puzzle hash inputs** accept either `xch…`/`txch…` addresses or `0x…` hex. The CLI normalizes to `0x…` hex. +- **Block selectors** accept either a numeric height or a `0x…` header hash. Heights are resolved to header hashes automatically. +- **Hex values** accept with or without `0x` prefix. + +## Commands by intent + +### Find coins + +```bash +coinset get_coin_record_by_name <0xCOIN_ID> +coinset get_coin_records_by_puzzle_hash [--include-spent-coins] [--start-height N] [--end-height N] +coinset get_coin_records_by_puzzle_hashes ... [--include-spent-coins] +coinset get_coin_records_by_parent_ids ... [--include-spent-coins] +coinset get_coin_records_by_hint [--include-spent-coins] +coinset get_coin_records_by_hints ... [--include-spent-coins] +coinset get_coin_records_by_names ... [--include-spent-coins] +``` + +By default, coin record queries return only **unspent** coins. Pass `--include-spent-coins` / `-s` to include spent coins. + +### Inspect spends + +```bash +coinset get_puzzle_and_solution <0xCOIN_ID> +coinset get_puzzle_and_solution_with_conditions <0xCOIN_ID> +coinset get_block_spends +coinset get_block_spends_with_conditions +coinset get_mempool_item_by_tx_id <0xTX_ID> +``` + +All of these support `--inspect` to replace the raw JSON output with a structured spend analysis. + +### Block and chain state + +```bash +coinset get_blockchain_state +coinset get_network_info +coinset get_block_record_by_height +coinset get_block_record +coinset get_block_records +coinset get_block +coinset get_blocks +coinset get_block_count_metrics +coinset get_network_space +coinset get_additions_and_removals +``` + +### Mempool + +```bash +coinset get_all_mempool_tx_ids +coinset get_all_mempool_items +coinset get_mempool_item_by_tx_id <0xTX_ID> +coinset get_mempool_items_by_coin_name <0xCOIN_NAME> +``` + +### Transaction submission + +```bash +coinset push_tx '' +coinset push_tx ./spend_bundle.json +coinset push_tx -f ./spend_bundle.json +``` + +### Fee estimation + +```bash +coinset get_fee_estimate +``` + +### Utilities + +```bash +coinset address encode <0xPUZZLE_HASH> +coinset address decode +coinset coin_id <0xPARENT_COIN_ID> <0xPUZZLE_HASH> +coinset get_memos_by_coin_name <0xCOIN_NAME> +coinset get_aggsig_additional_data +``` + +### CLVM tools + +```bash +coinset clvm decompile +coinset clvm compile "" +coinset clvm run "" "" [--cost] [--max-cost N] +coinset clvm run --program "" --env "" [--cost] [--max-cost N] +coinset clvm tree_hash +coinset clvm curry [arg1] [arg2] ... [--atom val] [--tree-hash val] [--program val] +coinset clvm uncurry +``` + +All CLVM subcommands accept both hex bytes and s-expressions as input. The CLI auto-detects the format. + +#### `curry` arg type handling + +The `curry` command supports typed arguments for computing curried puzzle hashes: + +- **Mod (first positional arg)**: if 32 bytes hex, treated as the mod's tree hash (hash-only mode). Otherwise treated as a full serialized program. +- **Positional curry args**: if 32 bytes hex, treated as an atom. Otherwise treated as a serialized program. +- **`--atom `**: explicitly mark an arg as a raw atom value. Tree hash = `sha256(1, bytes)`. +- **`--tree-hash `**: arg is already a tree hash. Used directly -- no hashing applied. +- **`--program `**: explicitly mark an arg as a serialized CLVM program. + +Flag args are appended after positional args in order: `--atom`, then `--tree-hash`, then `--program`. + +**Output**: when all inputs have full bytes, `curried.bytes` and `curried.tree_hash` are both returned. When any input is a tree hash (mod is 32 bytes, or `--tree-hash` is used), only `curried.tree_hash` is returned (full bytes cannot be computed). + +The key distinction: an atom arg like a TAIL hash is a 32-byte data value curried into the puzzle as a leaf node. A tree-hash arg like an inner puzzle hash represents a full subtree. Even if both are 32-byte hex, they contribute differently to the curried puzzle hash. + +### Real-time events + +```bash +coinset events # all events +coinset events peak # new block peaks only +coinset events transaction # mempool transactions +coinset events offer # offer activity +``` + +## `--inspect` output schema + +When `--inspect` is used, the output follows `coinset.inspect.v1`: + +``` +{ + "schema_version": "coinset.inspect.v1", + "tool": { "name": "coinset-inspect", "version": "..." }, + "input": { "kind": "mempool|block|coin", "notes": [...] }, + "result": { + "status": "ok|failed", + "error": null | { "kind": "...", "message": "..." }, + "summary": { + "removals": [{ "coin_id", "parent_coin_id", "puzzle_hash", "amount" }], + "additions": [{ "coin_id", "parent_coin_id", "puzzle_hash", "amount" }], + "fee_mojos": , + "net_xch_delta_by_puzzle_hash": [{ "puzzle_hash", "delta_mojos" }] + }, + "spends": [{ + "coin_spend": { "coin": {...}, "puzzle_reveal": "0x...", "solution": "0x..." }, + "evaluation": { + "status": "ok|failed", + "cost": , + "conditions": [{ "opcode": "CREATE_COIN", "args": [...] }, ...], + "additions": [...], + "failure": null | { "kind", "message" } + }, + "clvm": { + "puzzle_opd": "", + "solution_opd": "", + "tree_hash": "0x...", + "uses_backrefs": true|false + }, + "puzzle_recognition": { + "recognized": true|false, + "candidates": [{ "name", "confidence", "source_repo", "source_path", "source_ref" }], + "wrappers": [{ + "name": "cat_layer|singleton_layer|...", + "mod_hash": "0x...", + "params": { ... }, + "inner_puzzle_tree_hash": "0x...", + "parse_error": null | "..." + }], + "parsed_solution": { "layers": [...] } + } + }], + "signatures": { + "aggregated_signature": "0x...", + "agg_sig_me": [{ "pubkey", "msg" }], + "agg_sig_unsafe": [{ "pubkey", "msg" }] + } + } +} +``` + +### Supported `--inspect` input shapes + +`--inspect` works when the RPC response contains one of: +- `mempool_item` (with nested `spend_bundle` or `spend_bundle_bytes`) +- `spend_bundle` / `spend_bundle_bytes` +- `coin_spends` / `block_spends` (array of coin spends) +- `coin_spend` or a single object with `puzzle_reveal` + `solution` +- `mempool_items` (map of tx_id -> mempool_item; each is inspected individually) + +If the output shape doesn't match, `--inspect` returns an error. Fall back to the command without `--inspect`. + +### Recognized puzzle layers + +The inspector peels puzzle layers from outside in: + +| Layer name | What it means | +|---|---| +| `cat_layer` | CAT (Chia Asset Token). Params include `asset_id`. | +| `singleton_layer` | Singleton wrapper. Params include `launcher_id`. | +| `did_layer` | DID (Decentralized Identity). Params include `launcher_id`. | +| `nft_state_layer` | NFT state metadata layer. | +| `nft_ownership_layer` | NFT ownership/transfer layer. Params include `current_owner`. | +| `royalty_transfer_layer` | NFT royalty enforcement. Params include `royalty_basis_points`. | +| `augmented_condition_layer` | Prepends extra conditions to inner puzzle output. | +| `bulletin_layer` | On-chain bulletin board data layer. | +| `option_contract_layer` | Option contract wrapper. | +| `revocation_layer` | Clawback/revocation custody. Params include `hidden_puzzle_hash`. | +| `p2_singleton_layer` | Pay-to-singleton (pool farming reward target). | +| `p2_curried_layer` | Pay-to-curried-puzzle. | +| `p2_one_of_many_layer` | Merkle-tree multisig or multi-path spend. | +| `p2_delegated_conditions_layer` | Direct delegated conditions with public key. | +| `settlement_layer` | Offer settlement payments puzzle. | +| `stream_layer` | Payment streaming / vesting. | +| `standard_layer` | Standard transaction (`p2_delegated_puzzle_or_hidden_puzzle`). Params include `synthetic_key`. This is typically the innermost layer. | + +Recognition is best-effort. If a `parse_error` appears on a wrapper, keep the successfully parsed layers but treat confidence as partial. If recognition returns `recognized: false`, rely on raw conditions and CLVM disassembly. + +## Workflows + +### Look up a coin and check if it was spent + +```bash +coinset get_coin_record_by_name 0xCOIN_ID -q '.coin_record' +``` + +Check `spent` (boolean) and `spent_block_index` in the result. + +### Get puzzle and solution for a spent coin + +```bash +coinset get_puzzle_and_solution 0xCOIN_ID --inspect +``` + +### Inspect all spends in a block + +```bash +coinset get_block_spends 5000000 --inspect +``` + +### Inspect a mempool transaction + +```bash +coinset get_mempool_item_by_tx_id 0xTX_ID --inspect +``` + +### Trace coin lineage (parent -> children) + +```bash +# Find children of a coin +coinset get_coin_records_by_parent_ids 0xPARENT_COIN_ID --include-spent-coins + +# Find the parent's spend (to see what created this coin) +coinset get_puzzle_and_solution 0xPARENT_COIN_ID --inspect +``` + +### Decompile a puzzle reveal + +```bash +coinset clvm decompile 0xff02ffff01ff... +``` + +### Run a CLVM program and see cost + +```bash +coinset clvm run "(+ (q . 2) (q . 3))" "()" --cost +``` + +### Compute a puzzle hash + +```bash +coinset clvm tree_hash ff02ffff01ff02ffff03... +``` + +`tree_hash` computes the sha256 tree hash of a CLVM program. This is the puzzle hash used to identify coins on-chain. Accepts hex or s-expression input. + +### Curry and uncurry puzzles + +```bash +# Curry arguments into a puzzle template (full bytes) +coinset clvm curry ... + +# Recover the mod and curried arguments from a full puzzle +coinset clvm uncurry +``` + +`uncurry` output includes `kind` ("atom" or "tree") and `value` (raw atom bytes without CLVM serialization prefix) for each arg. For atom args, `value` is directly matchable against known hashes and IDs without needing to strip CLVM serialization prefixes: + +```bash +# 1. Get the puzzle reveal from a spend +coinset get_puzzle_and_solution <0xCOIN_ID> -q '.coin_solution.puzzle_reveal' --raw + +# 2. Uncurry it to see the base mod and curried args +coinset clvm uncurry + +# 3. Match mod_.tree_hash against known puzzle hashes (see "Known puzzle mod hashes" section) +``` + +### Compute a CAT puzzle hash from known hashes + +The most common use case for `curry` with `--tree-hash` is computing CAT puzzle hashes. When sending someone a CAT, you only need their address (inner puzzle hash) -- the full inner puzzle is never needed to compute the puzzle hash. + +```bash +# Compute CAT puzzle hash for a specific asset + owner +# MOD_HASH and TAIL_HASH are atoms (32 bytes → auto-detected) +# Inner puzzle hash needs --tree-hash since it's a tree hash, not an atom +coinset clvm curry 0x37bef360ee858133b69d595a906dc45d01af50379dad515eb9518abb7c1d2a7a \ + 0x37bef360ee858133b69d595a906dc45d01af50379dad515eb9518abb7c1d2a7a \ + 0x \ + --tree-hash 0x \ + -q '.curried.tree_hash' +``` + +### Compute a CAT settlement puzzle hash + +Find all offer coins for a specific CAT asset: + +```bash +# Settlement payments has no curried args, so its mod hash IS its tree hash +coinset clvm curry 0x37bef360ee858133b69d595a906dc45d01af50379dad515eb9518abb7c1d2a7a \ + 0x37bef360ee858133b69d595a906dc45d01af50379dad515eb9518abb7c1d2a7a \ + 0x \ + --tree-hash 0xcfbfdeed5c4ca2de3d0bf520b9cb4bb7743a359bd2e6a188d19ce7dffc21d3e7 \ + -q '.curried.tree_hash' + +# Then search for offer coins +coinset get_coin_records_by_puzzle_hash +``` + +### Lineage proofs in CAT solutions + +When investigating CAT spends, the solution includes a **lineage proof** to verify the parent was a legitimate CAT of the same type. The proof contains `(parent_parent_coin_id, parent_inner_puzzle_hash, parent_amount)`. This allows tracing CAT coin lineage backwards through the chain. + +### Filter output with jq + +```bash +# Get current peak height +coinset get_blockchain_state -q .blockchain_state.peak.height + +# Get just the fee from a mempool item +coinset get_mempool_item_by_tx_id 0xTX_ID -q .mempool_item.fee + +# Count coins for an address +coinset get_coin_records_by_puzzle_hash xch1... -q '.coin_records | length' +``` + +### Human-readable output + +```bash +coinset get_coin_record_by_name 0xCOIN_ID --describe +``` + +Adds `amount_description` (e.g., `"0.000300000000 XCH"`), `timestamp_description` (e.g., `"2 hours ago, 2024-02-06 15:33:27"`), and `confirmed_block_index_description` fields. + +### Direct HTTP fallback + +If you need to call the Full Node RPC directly: + +```bash +# Mainnet +curl -s https://api.coinset.org/get_blockchain_state -d '{}' + +# Testnet11 +curl -s https://testnet11.api.coinset.org/get_blockchain_state -d '{}' +``` + +## Chia blockchain concepts + +### The coin model + +Chia uses a **coin set** model (analogous to Bitcoin's UTXO model). There are no accounts or balances at the protocol level -- only coins. + +A **coin** is defined by three values: +- `parent_coin_info` -- the coin ID of the coin that created this one +- `puzzle_hash` -- the hash of the CLVM program (puzzle) that locks this coin +- `amount` -- value in mojos (1 XCH = 1,000,000,000,000 mojos) + +The **coin ID** (also called coin name) is `sha256(parent_coin_info + puzzle_hash + amount)`. It uniquely identifies a coin. + +A **coin record** is the blockchain's view of a coin: the coin itself plus metadata (confirmed block index, timestamp, spent status, spent block index). + +### Spending coins + +To spend a coin, you must provide: +- **puzzle reveal** -- the full CLVM program whose hash matches the coin's `puzzle_hash` +- **solution** -- CLVM data passed as input to the puzzle + +The puzzle is executed with the solution as its environment. If execution succeeds, it returns a list of **conditions** that dictate what happens. + +A **coin spend** is the tuple `(coin, puzzle_reveal, solution)`. + +A **spend bundle** is a group of coin spends plus an `aggregated_signature` (BLS12-381). All coin spends in a bundle are atomic -- they either all succeed or all fail. + +### Additions, removals, and fees + +- **Removals** are the coins being spent (inputs) +- **Additions** are the coins being created via `CREATE_COIN` conditions (outputs) +- **Fee** = sum of removal amounts - sum of addition amounts + +### Addresses and puzzle hashes + +A Chia **address** (e.g., `xch1...`) is the bech32m encoding of a puzzle hash. `xch` prefix is mainnet, `txch` is testnet. They are interchangeable representations of the same 32-byte value. + +### Hints and memos + +When a `CREATE_COIN` condition includes an optional third argument (a list), the first element is the **hint** if it's exactly 32 bytes. Hints typically contain the inner puzzle hash (p2) of the created coin, allowing wallets and indexers to find coins that belong to them even when the outer puzzle hash differs (as with CATs, NFTs, etc.). + +`get_coin_records_by_hint` uses these hints to find coins. `get_memos_by_coin_name` retrieves the full memo list. + +### Mempool vs confirmed + +**Mempool items** are unconfirmed transactions waiting to be included in a block. They can be evicted, replaced, or reorganized away. **Block spends** are confirmed and considered final (modulo chain reorgs, which are extremely rare on Chia). + +Prefer confirmed block data for definitive analysis. Use mempool data for investigating pending transactions. + +## CLVM essentials + +### Data model + +CLVM (Chialisp Virtual Machine) operates on a binary tree of **atoms** and **cons pairs**: +- An **atom** is a sequence of bytes (numbers, hashes, public keys, or empty) +- A **cons pair** is `(first . rest)` -- two children +- A **list** is a chain of cons pairs ending in nil: `(a . (b . (c . nil)))` +- **Nil** is the empty atom, represented as `()` in text or `0x80` in serialized form + +### Programs and evaluation + +A CLVM program is itself a tree. When evaluated: +- If it's an **atom**, it's a path lookup into the environment (1 = whole env, 2 = first, 3 = rest, 5 = first of rest, etc.) +- If it's a **cons pair**, the left element is the operator and the right elements are operands +- The **quote operator** `q` returns its argument as a literal value without evaluating it +- The **apply operator** `a` runs a program with a given environment + +### Serialization + +Puzzle reveals and solutions are hex-encoded serialized CLVM: +- `0xFF` prefix = cons pair (followed by first, then rest) +- `0x80` = nil +- `0x00`-`0x7F` = single-byte atom +- Larger atoms use length-prefixed encoding + +Use `coinset clvm decompile ` to convert to readable s-expression form. Use `coinset clvm tree_hash ` to compute the sha256 tree hash of a serialized program -- this is how puzzle hashes and coin IDs are derived. + +### Currying + +Currying pre-bakes parameters into a puzzle template before deployment. In Chialisp source, `SCREAMING_SNAKE_CASE` parameters are conventionally meant to be curried in. For example, the standard transaction curries in a `SYNTHETIC_PUBLIC_KEY`. The CAT puzzle curries in `MOD_HASH`, `TAIL_PROGRAM_HASH`, and `INNER_PUZZLE`. + +The curried puzzle is a new program with a different tree hash than the uncurried template. Crucially, the curried puzzle hash is deterministic from the mod hash and the tree hashes of the curried arguments -- you don't need the full serialized programs to compute it. This is how wallets derive puzzle hashes for coins they create: they know the mod hash (a constant) and the tree hashes of the args (derived from the recipient's address, asset ID, etc.). + +Use `coinset clvm curry [args...]` to curry arguments into a puzzle template, and `coinset clvm uncurry ` to reverse the process -- recovering the original mod and the curried arguments. Uncurrying a `puzzle_reveal` is one of the most useful investigation techniques: it separates the reusable puzzle template from the instance-specific parameters (public keys, asset IDs, launcher IDs, etc.). + +When currying, each arg is either an **atom** (a data value like a hash or key) or a **tree** (a full CLVM program like an inner puzzle). Atoms contribute `sha256(1, atom_bytes)` to the curried tree hash, while trees contribute their recursive tree hash directly. The `--atom`, `--tree-hash`, and `--program` flags on `curry` control this distinction. + +### Cost + +Every CLVM operation has a cost. Key numbers: +- **Max cost per block**: 11,000,000,000 (11 billion) +- **Per-byte cost**: 12,000 per byte of serialized puzzle + solution +- **AGG_SIG_\* condition**: 1,200,000 each +- **CREATE_COIN condition**: 1,800,000 each +- **Fee priority**: mojos per cost. When the mempool is full, ~5 mojos/cost is needed to displace existing transactions. + +`coinset clvm run ... --cost` and `--inspect` both report execution cost. + +### Common operators + +| Operator | Description | +|---|---| +| `q` (quote) | Return value literally | +| `a` (apply) | Run program with environment | +| `i` (if) | Conditional branch | +| `c` (cons) | Create pair | +| `f` (first) | First element of pair | +| `r` (rest) | Rest element of pair | +| `+`, `-`, `*`, `/` | Arithmetic | +| `=` | Equality test | +| `sha256` | SHA-256 hash | +| `concat` | Concatenate bytes | +| `substr` | Slice bytes | +| `logand`, `logior`, `logxor`, `lognot` | Bitwise operations | +| `pubkey_for_exp` | Secret key to G1 public key | +| `g1_add`, `g1_multiply` | BLS G1 point operations | +| `coinid` | Compute coin ID with validation (CHIP-11) | +| `softfork` | Future-proof operator for soft-forked features | +| `x` (exit) | Terminate program / raise error | + +## Conditions reference + +When a puzzle executes, it returns a list of conditions. Each condition is a list starting with an opcode number. + +### Coin creation + +| Opcode | Name | Format | Description | +|---|---|---|---| +| 51 | `CREATE_COIN` | `(51 puzzle_hash amount (...memos)?)` | Create a new coin. Optional memos list; first 32-byte memo is the hint. Cost: 1,800,000. | +| 52 | `RESERVE_FEE` | `(52 amount)` | Assert minimum fee in this transaction. | + +### Signatures + +All AGG_SIG conditions cost 1,200,000 each. + +| Opcode | Name | Description | +|---|---|---| +| 49 | `AGG_SIG_UNSAFE` | Verify signature on raw message. No domain separation -- can be replayed. | +| 50 | `AGG_SIG_ME` | Verify signature on message + coin_id + genesis_id. Recommended for most uses. | +| 43-48 | `AGG_SIG_PARENT` / `AGG_SIG_PUZZLE` / `AGG_SIG_AMOUNT` / `AGG_SIG_PUZZLE_AMOUNT` / `AGG_SIG_PARENT_AMOUNT` / `AGG_SIG_PARENT_PUZZLE` | CHIP-11 domain-separated signature variants binding to specific coin attributes. | + +### Announcements (legacy) + +| Opcode | Name | Description | +|---|---|---| +| 60 | `CREATE_COIN_ANNOUNCEMENT` | Create announcement bound to this coin's ID. | +| 61 | `ASSERT_COIN_ANNOUNCEMENT` | Assert `sha256(coin_id + message)` was announced. | +| 62 | `CREATE_PUZZLE_ANNOUNCEMENT` | Create announcement bound to this coin's puzzle hash. | +| 63 | `ASSERT_PUZZLE_ANNOUNCEMENT` | Assert `sha256(puzzle_hash + message)` was announced. | + +### Messages (modern replacement for announcements) + +| Opcode | Name | Description | +|---|---|---| +| 66 | `SEND_MESSAGE` | `(66 mode message ...)` -- Send message with sender/receiver commitment via mode bitmask. | +| 67 | `RECEIVE_MESSAGE` | `(67 mode message ...)` -- Assert receipt of matching message. | + +The `mode` byte is 6 bits: 3 bits for sender commitment, 3 bits for receiver commitment. Each 3-bit group encodes which coin attributes (parent, puzzle, amount) to commit to. For example, `mode=0b111110` means sender commits to all three (coin ID) and receiver commits to parent+puzzle. + +### Self-assertions + +| Opcode | Name | Description | +|---|---|---| +| 70 | `ASSERT_MY_COIN_ID` | Assert this coin's ID matches. | +| 71 | `ASSERT_MY_PARENT_ID` | Assert this coin's parent ID matches. | +| 72 | `ASSERT_MY_PUZZLEHASH` | Assert this coin's puzzle hash matches. | +| 73 | `ASSERT_MY_AMOUNT` | Assert this coin's amount matches. | +| 74 | `ASSERT_MY_BIRTH_SECONDS` | Assert coin creation timestamp. | +| 75 | `ASSERT_MY_BIRTH_HEIGHT` | Assert coin creation height. | +| 76 | `ASSERT_EPHEMERAL` | Assert this coin was created in the current block. | + +### Timelocks + +| Opcode | Name | Description | +|---|---|---| +| 80 | `ASSERT_SECONDS_RELATIVE` | Min seconds since coin creation. | +| 81 | `ASSERT_SECONDS_ABSOLUTE` | Min absolute timestamp. | +| 82 | `ASSERT_HEIGHT_RELATIVE` | Min blocks since coin creation. | +| 83 | `ASSERT_HEIGHT_ABSOLUTE` | Min absolute block height. | +| 84 | `ASSERT_BEFORE_SECONDS_RELATIVE` | Max seconds since coin creation. | +| 85 | `ASSERT_BEFORE_SECONDS_ABSOLUTE` | Max absolute timestamp. | +| 86 | `ASSERT_BEFORE_HEIGHT_RELATIVE` | Max blocks since coin creation. | +| 87 | `ASSERT_BEFORE_HEIGHT_ABSOLUTE` | Max absolute block height. | + +### Concurrency + +| Opcode | Name | Description | +|---|---|---| +| 64 | `ASSERT_CONCURRENT_SPEND` | Assert another specific coin is spent in same block. | +| 65 | `ASSERT_CONCURRENT_PUZZLE` | Assert a coin with specific puzzle hash is spent in same block. | + +### Other + +| Opcode | Name | Description | +|---|---|---| +| 1 | `REMARK` | No-op; always valid. | +| 90 | `SOFTFORK` | Soft-fork guard for future conditions. Cost is specified in ten-thousands. | + +## Puzzle composition + +Chia puzzles compose through **outer/inner puzzle wrapping**. An outer puzzle enforces protocol rules and delegates ownership decisions to an inner puzzle. The inner puzzle is typically the **standard transaction** at the bottom of the stack. + +When the inner puzzle emits `CREATE_COIN` conditions, the outer puzzle intercepts them and wraps the created coin's puzzle hash with itself, ensuring output coins maintain the same wrapper structure. + +### Standard transaction + +The innermost puzzle for most coins. Curries in a `SYNTHETIC_PUBLIC_KEY`. Spending requires either a delegated puzzle signed by the synthetic key, or revealing a hidden puzzle. This is what `xch…` addresses resolve to. + +### CAT (Chia Asset Token) + +Fungible token wrapper. Curries in `MOD_HASH`, `TAIL_PROGRAM_HASH`, and `INNER_PUZZLE`. Enforces supply conservation via a ring of coin announcements across all CAT spends in a bundle. The **TAIL** (Token and Asset Issuance Limitations) program controls minting and melting rules. The `asset_id` is the TAIL program hash. + +Amounts: 1 CAT = 1,000 mojos of the underlying coin. + +### Singleton + +Guarantees a unique, traceable on-chain identity via a `launcher_id`. Always produces exactly one odd-valued output coin (the continuation), or an output of -113 to self-destruct. Used as the foundation for NFTs, DIDs, plot NFTs, and vaults. + +### NFT + +An NFT is a singleton containing multiple nested layers: +- `singleton_layer` (uniqueness) +- `nft_state_layer` (metadata: URIs, hash, updater puzzle) +- `nft_ownership_layer` (current owner DID, transfer program) +- `royalty_transfer_layer` (royalty enforcement on trades) +- Inner puzzle (typically standard transaction) + +### DID (Decentralized Identity) + +A singleton-based identity. The `did_layer` curries in a `launcher_id`, optional recovery list, and metadata. Used as an ownership identity for NFTs and other assets. + +### Offers + +Offers enable peer-to-peer, trustless, atomic asset exchange on Chia. A **maker** creates a partial spend bundle describing what they're offering and what they want in return. A **taker** completes the spend bundle with their side and pushes it to the network. Both sides execute in the same block or not at all -- there is no counterparty risk. + +**Settlement payments puzzle**: Offers work through the `settlement_payments` puzzle (recognized by `--inspect` as `settlement_layer`). Its solution is a list of `notarized_payments`, each structured as `(nonce . ((puzzle_hash amount ...memos) ...))`. For each entry, the puzzle creates a `CREATE_PUZZLE_ANNOUNCEMENT` of the tree hash of the notarized payment, and a `CREATE_COIN` for each payment within the entry. The maker's coins assert these puzzle announcements, binding the two sides together -- if either side is altered, the assertions fail and the entire bundle is invalid. + +The settlement puzzle is versatile: it can be used as an inner puzzle inside a CAT or NFT, or standalone for XCH, enabling cross-asset trades. + +**Nonce**: The nonce is the tree hash of a sorted list of the coin IDs being offered. This binds the settlement to specific coins and prevents a maker from creating two offers that could both be fulfilled with a single payment. + +**Offer file**: An offer file is a bech32-encoded incomplete spend bundle containing only the maker's side. It can be shared freely -- anyone who sees it can only accept or ignore it. No private keys are exposed. Any alteration to the file invalidates the offer. + +**Offer lifecycle**: +- **PENDING_ACCEPT** -- maker created the offer; not yet taken +- **PENDING_CONFIRM** -- taker completed the bundle and pushed to mempool +- **CONFIRMED** -- included in a block; trade complete +- **PENDING_CANCEL** -- maker is spending the offered coins to invalidate +- **CANCELLED** -- maker's coins were spent, invalidating the offer +- **FAILED** -- taker's acceptance failed (maker cancelled or another taker was first) + +**Cancellation**: An offer is cancelled by spending the coins it references, which invalidates the partial spend bundle. If the offer was never published, simply deleting the file is sufficient. + +**Aggregation**: Multiple offers can be aggregated into a single settlement by an Automated Market Maker (AMM). Each asset type gets one `settlement_payments` ephemeral coin, and multiple offers' notarized payments are combined. This is relevant when investigating complex multi-party swaps on-chain. + +## Known puzzle mod hashes + +Reference table of well-known puzzle template hashes. Use these with `coinset clvm curry` and `coinset clvm uncurry` to identify and compose puzzles. Full serialized mod bytes are available at `https://raw.githubusercontent.com/Chia-Network/chia_puzzles/main/src/programs.rs`. + +### Core puzzles + +**Standard transaction** (`P2_DELEGATED_PUZZLE_OR_HIDDEN_PUZZLE`) +`0xe9aaa49f45bad5c889b86ee3341550c155cfdd10c3a6757de618d20612fffd52` +Args: `SYNTHETIC_PUBLIC_KEY` (48-byte atom) + +**CAT v2** (`CAT_PUZZLE`) +`0x37bef360ee858133b69d595a906dc45d01af50379dad515eb9518abb7c1d2a7a` +Args: `MOD_HASH` (atom — the CAT mod hash itself), `TAIL_PROGRAM_HASH` (atom — the asset ID), `INNER_PUZZLE` (tree) + +**Singleton v1.1** (`SINGLETON_TOP_LAYER_V1_1`) +`0x7faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9f` +Args: `SINGLETON_STRUCT` (tree — `(MOD_HASH . (LAUNCHER_ID . LAUNCHER_PUZZLE_HASH))`), `INNER_PUZZLE` (tree) + +**Singleton launcher** (`SINGLETON_LAUNCHER`) +`0xeff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9` +No curried args. The launcher coin ID becomes the `launcher_id`. + +**Settlement payments** (`SETTLEMENT_PAYMENT`) +`0xcfbfdeed5c4ca2de3d0bf520b9cb4bb7743a359bd2e6a188d19ce7dffc21d3e7` +No curried args. Used as inner puzzle in offers. + +**DID inner puzzle** (`DID_INNERPUZ`) +`0x33143d2bef64f14036742673afd158126b94284b4530a28c354fac202b0c910e` +Args: `INNER_PUZZLE` (tree), `RECOVERY_DID_LIST_HASH` (atom), `NUM_VERIFICATIONS_REQUIRED` (atom), `SINGLETON_STRUCT` (tree), `METADATA` (tree) + +**NFT state layer** (`NFT_STATE_LAYER`) +`0xa04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2` +Args: `MOD_HASH` (atom), `METADATA` (tree), `METADATA_UPDATER_PUZZLE_HASH` (atom), `INNER_PUZZLE` (tree) + +**NFT ownership layer** (`NFT_OWNERSHIP_LAYER`) +`0xc5abea79afaa001b5427dfa0c8cf42ca6f38f5841b78f9b3c252733eb2de2726` +Args: `MOD_HASH` (atom), `CURRENT_OWNER` (atom), `TRANSFER_PROGRAM` (tree), `INNER_PUZZLE` (tree) + +**Royalty transfer program** (`NFT_OWNERSHIP_TRANSFER_PROGRAM_ONE_WAY_CLAIM_WITH_ROYALTIES`) +`0x025dee0fb1e9fa110302a7e9bfb6e381ca09618e2778b0184fa5c6b275cfce1f` +Args: `SINGLETON_STRUCT` (tree), `ROYALTY_ADDRESS` (atom), `TRADE_PRICE_PERCENTAGE` (atom) + +**NFT metadata updater** (`NFT_METADATA_UPDATER_DEFAULT`) +`0xfe8a4b4e27a2e29a4d3fc7ce9d527adbcaccbab6ada3903ccf3ba9a769d2d78b` +No curried args. + +### Commonly encountered puzzles + +**Pay-to-singleton** (`P2_SINGLETON`) +`0x40f828d8dd55603f4ff9fbf6b73271e904e69406982f4fbefae2c8dcceaf9834` +Used as pool farming reward target. + +**Notification** (`NOTIFICATION`) +`0xb8b9d8ffca6d5cba5422ead7f477ecfc8f6aaaa1c024b8c3aeb1956b24a0ab1e` + +**Pool member inner puzzle** (`POOL_MEMBER_INNERPUZ`) +`0xa8490702e333ddd831a3ac9c22d0fa26d2bfeaf2d33608deb22f0e0123eb0494` + +**Pool waiting room** (`POOL_WAITINGROOM_INNERPUZ`) +`0xa317541a765bf8375e1c6e7c13503d0d2cbf56cacad5182befe947e78e2c0307` + +### Common TAIL programs + +**Everything with signature** (`EVERYTHING_WITH_SIGNATURE`) +`0xf26fe751e5a9b13e87e4c19e4c383e713cec8c854cf8e591053e179e60e3b856` +Args: `PUBLIC_KEY` (48-byte atom). Most common TAIL for standard CAT issuance. + +**Genesis by coin ID** (`GENESIS_BY_COIN_ID`) +`0x493afb89eed93ab86741b2aa61b8f5de495d33ff9b781dfc8919e602b2571571` +Args: `GENESIS_COIN_ID` (atom). Single-issuance tokens. + +## Agent operating rules + +1. **Always capture identifiers**: coin ID, puzzle hash, height, header hash, tx ID. These are needed for follow-up queries. +2. **Check `success` field**: all Full Node RPC responses include `"success": true|false`. Always verify. +3. **Use `--inspect` first** for spend analysis. Drop to raw CLVM disassembly only when `--inspect` doesn't cover what you need. +4. **If `--inspect` errors with "not supported for this endpoint/output shape"**: re-run the command without `--inspect`, or try a different endpoint that returns spend bundle data. +5. **Use `--include-spent-coins`** on coin record queries when tracing history. Without it, you only see unspent coins. +6. **Prefer confirmed data**: block spends are final; mempool items can disappear. +7. **Explain from conditions outward**: start with `evaluation.conditions` and `evaluation.cost`, then `puzzle_recognition.wrappers`, then raw CLVM if needed. +8. **If recognition is ambiguous** (multiple candidates or `parse_error`): report the ambiguity, don't make definitive wrapper claims. Fall back to conditions and additions/removals. +9. **Don't infer transaction intent from incomplete data**: always ground explanations in the actual conditions, additions, removals, and fee. +10. **Use `-q` for targeted extraction** instead of parsing large JSON responses manually. diff --git a/crates/coinset-clvm-core/Cargo.toml b/crates/coinset-clvm-core/Cargo.toml index ab78e88..cd4e275 100644 --- a/crates/coinset-clvm-core/Cargo.toml +++ b/crates/coinset-clvm-core/Cargo.toml @@ -12,3 +12,5 @@ serde_json.workspace = true clvmr.workspace = true chialisp.workspace = true +clvm-utils.workspace = true +clvm-traits.workspace = true diff --git a/crates/coinset-clvm-core/src/lib.rs b/crates/coinset-clvm-core/src/lib.rs index 502028a..0ec986d 100644 --- a/crates/coinset-clvm-core/src/lib.rs +++ b/crates/coinset-clvm-core/src/lib.rs @@ -1,12 +1,15 @@ use anyhow::{Context, Result, bail}; use chialisp::classic::clvm::OPERATORS_LATEST_VERSION; use chialisp::classic::clvm_tools::binutils::{assemble, disassemble}; -use clvmr::allocator::Allocator; +use clvm_traits::{FromClvm, ToClvm, Raw}; +use clvm_utils::{CurriedProgram, TreeHash, curry_tree_hash, tree_hash_atom}; +use clvm_utils::tree_hash; +use clvmr::allocator::{Allocator, NodePtr, SExp}; use clvmr::chia_dialect::{ChiaDialect, MEMPOOL_MODE}; use clvmr::cost::Cost; use clvmr::run_program::run_program; use clvmr::serde::{node_from_bytes_backrefs, node_to_bytes}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize)] pub struct CompileOutput { @@ -24,6 +27,71 @@ pub struct RunOutput { pub cost: Option, } +#[derive(Debug, Clone, Serialize)] +pub struct TreeHashOutput { + pub tree_hash: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct CurryOutput { + pub curried: CurriedInfo, + pub mod_: ModInfo, + pub args: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct UncurryOutput { + pub curried: bool, + pub mod_: Option, + pub args: Option>, +} + +#[derive(Debug, Clone, Serialize)] +pub struct CurriedInfo { + #[serde(skip_serializing_if = "Option::is_none")] + pub bytes: Option, + pub tree_hash: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ModInfo { + pub tree_hash: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ArgInfo { + pub tree_hash: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ModBytes { + pub bytes: String, + pub tree_hash: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct UncurriedArg { + pub bytes: String, + pub tree_hash: String, + pub kind: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct CurryArg { + pub kind: CurryArgKind, + pub value: String, +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum CurryArgKind { + Atom, + TreeHash, + Program, +} + pub fn compile_to_hex(program: &str) -> Result { let mut allocator = Allocator::new(); let node = assemble(&mut allocator, program) @@ -70,6 +138,169 @@ pub fn run_program_text( }) } +pub fn tree_hash_input(input: &str) -> Result { + let mut allocator = Allocator::new(); + let node = parse_program_input(&mut allocator, input)?; + let h = tree_hash(&allocator, node); + Ok(TreeHashOutput { + tree_hash: encode_hex_prefixed(h.as_ref()), + }) +} + +pub fn curry_typed(mod_input: &str, mod_is_hash: bool, args: &[CurryArg]) -> Result { + let hash_mode = mod_is_hash || args.iter().any(|a| a.kind == CurryArgKind::TreeHash); + + if hash_mode { + return curry_hash_mode(mod_input, mod_is_hash, args); + } + + let mut allocator = Allocator::new(); + let mod_node = parse_program_input(&mut allocator, mod_input)?; + + let mut arg_nodes = Vec::::with_capacity(args.len()); + for a in args { + let node = match a.kind { + CurryArgKind::Atom => { + let bytes = decode_hex_prefixed(&a.value)?; + allocator.new_atom(&bytes)? + } + CurryArgKind::Program => parse_program_input(&mut allocator, &a.value)?, + CurryArgKind::TreeHash => unreachable!(), + }; + arg_nodes.push(node); + } + + let curried_args = build_curried_args(&mut allocator, &arg_nodes)?; + let curried = CurriedProgram { + program: Raw(mod_node), + args: Raw(curried_args), + } + .to_clvm(&mut allocator)?; + + let bytes = node_to_bytes(&allocator, curried)?; + let curried_tree = tree_hash(&allocator, curried); + + Ok(CurryOutput { + curried: CurriedInfo { + bytes: Some(encode_hex_prefixed(&bytes)), + tree_hash: encode_hex_prefixed(curried_tree.as_ref()), + }, + mod_: ModInfo { + tree_hash: encode_hex_prefixed(tree_hash(&allocator, mod_node).as_ref()), + }, + args: arg_nodes + .iter() + .map(|n| ArgInfo { + tree_hash: encode_hex_prefixed(tree_hash(&allocator, *n).as_ref()), + }) + .collect(), + }) +} + +fn curry_hash_mode(mod_input: &str, mod_is_hash: bool, args: &[CurryArg]) -> Result { + let mod_hash = if mod_is_hash { + let bytes = decode_hex_prefixed(mod_input)?; + if bytes.len() != 32 { + bail!("mod hash must be exactly 32 bytes"); + } + TreeHash::from(<[u8; 32]>::try_from(bytes.as_slice()).unwrap()) + } else { + let mut allocator = Allocator::new(); + let mod_node = parse_program_input(&mut allocator, mod_input)?; + tree_hash(&allocator, mod_node) + }; + + let mut arg_hashes = Vec::::with_capacity(args.len()); + let mut allocator = Allocator::new(); + for a in args { + let h = match a.kind { + CurryArgKind::Atom => { + let bytes = decode_hex_prefixed(&a.value)?; + tree_hash_atom(&bytes) + } + CurryArgKind::TreeHash => { + let bytes = decode_hex_prefixed(&a.value)?; + if bytes.len() != 32 { + bail!("tree hash arg must be exactly 32 bytes"); + } + TreeHash::from(<[u8; 32]>::try_from(bytes.as_slice()).unwrap()) + } + CurryArgKind::Program => { + let node = parse_program_input(&mut allocator, &a.value)?; + tree_hash(&allocator, node) + } + }; + arg_hashes.push(h); + } + + let curried_hash = curry_tree_hash(mod_hash, &arg_hashes); + + Ok(CurryOutput { + curried: CurriedInfo { + bytes: None, + tree_hash: encode_hex_prefixed(curried_hash.as_ref()), + }, + mod_: ModInfo { + tree_hash: encode_hex_prefixed(mod_hash.as_ref()), + }, + args: arg_hashes + .iter() + .map(|h| ArgInfo { + tree_hash: encode_hex_prefixed(h.as_ref()), + }) + .collect(), + }) +} + +pub fn uncurry(program: &str) -> Result { + let mut allocator = Allocator::new(); + let node = parse_program_input(&mut allocator, program)?; + + let parsed = CurriedProgram::, Raw>::from_clvm(&allocator, node); + let Ok(curried) = parsed else { + return Ok(UncurryOutput { + curried: false, + mod_: None, + args: None, + }); + }; + + let mod_node = curried.program.0; + let args_node = curried.args.0; + let args_list = parse_curried_args(&allocator, args_node)?; + + let mod_bytes = node_to_bytes(&allocator, mod_node)?; + let mod_tree = tree_hash(&allocator, mod_node); + + let mut args_out = Vec::with_capacity(args_list.len()); + for a in args_list { + let b = node_to_bytes(&allocator, a)?; + let h = tree_hash(&allocator, a); + let (kind, value) = match allocator.sexp(a) { + SExp::Atom => { + let raw = allocator.atom(a); + ("atom".to_string(), Some(encode_hex_prefixed(raw.as_ref()))) + } + SExp::Pair(..) => ("tree".to_string(), None), + }; + args_out.push(UncurriedArg { + bytes: encode_hex_prefixed(&b), + tree_hash: encode_hex_prefixed(h.as_ref()), + kind, + value, + }); + } + + Ok(UncurryOutput { + curried: true, + mod_: Some(ModBytes { + bytes: encode_hex_prefixed(&mod_bytes), + tree_hash: encode_hex_prefixed(mod_tree.as_ref()), + }), + args: Some(args_out), + }) +} + fn parse_program_input(allocator: &mut Allocator, input: &str) -> Result { if looks_like_hex(input) { let bytes = decode_hex_prefixed(input)?; @@ -80,6 +311,63 @@ fn parse_program_input(allocator: &mut Allocator, input: &str) -> Result Result { + let mut rest = allocator.new_small_number(1)?; + let nil = allocator.nil(); + for arg in args.iter().rev() { + let q = allocator.new_small_number(1)?; + let quote = allocator.new_pair(q, *arg)?; + let tail = allocator.new_pair(rest, nil)?; + let inner = allocator.new_pair(quote, tail)?; + let op = allocator.new_small_number(4)?; + rest = allocator.new_pair(op, inner)?; + } + Ok(rest) +} + +fn parse_curried_args(allocator: &Allocator, mut node: NodePtr) -> Result> { + let mut out = Vec::::new(); + loop { + match allocator.sexp(node) { + clvmr::allocator::SExp::Atom => { + // termination is atom 1 + if allocator.atom(node).as_ref() == [1_u8] { + break; + } + break; + } + clvmr::allocator::SExp::Pair(op, rest) => { + // op must be atom 4 + if !matches!(allocator.sexp(op), clvmr::allocator::SExp::Atom) + || allocator.atom(op).as_ref() != [4_u8] + { + break; + } + let clvmr::allocator::SExp::Pair(quoted_arg, rest_pair) = allocator.sexp(rest) else { + break; + }; + // quoted_arg is (1 . arg) + let clvmr::allocator::SExp::Pair(q, arg) = allocator.sexp(quoted_arg) else { + break; + }; + if !matches!(allocator.sexp(q), clvmr::allocator::SExp::Atom) || allocator.atom(q).as_ref() != [1_u8] { + break; + } + out.push(arg); + + let clvmr::allocator::SExp::Pair(next, nil) = allocator.sexp(rest_pair) else { + break; + }; + if !matches!(allocator.sexp(nil), clvmr::allocator::SExp::Atom) || !allocator.atom(nil).as_ref().is_empty() { + break; + } + node = next; + } + } + } + Ok(out) +} + fn decode_hex_prefixed(input: &str) -> Result> { let raw = input .strip_prefix("0x") @@ -99,6 +387,26 @@ fn encode_hex_prefixed(bytes: &[u8]) -> String { format!("0x{}", hex::encode(bytes)) } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_curry_hash_mode_cat() { + let result = curry_typed( + "37bef360ee858133b69d595a906dc45d01af50379dad515eb9518abb7c1d2a7a", + true, + &[ + CurryArg { kind: CurryArgKind::Atom, value: "37bef360ee858133b69d595a906dc45d01af50379dad515eb9518abb7c1d2a7a".to_string() }, + CurryArg { kind: CurryArgKind::Atom, value: "e1c77e2f2bc1758cfd4f3f875adc9b96db322a558072a5b0b8e697d0931bd7fc".to_string() }, + CurryArg { kind: CurryArgKind::Program, value: "ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0b4301d5383702bb6824712a96425d343aaa4448e288de4d2b243425cdce9fd5fd19e0d4ddaacac858216644c05bac4d0ff018080".to_string() }, + ], + ); + let out = result.expect("curry_typed should succeed"); + println!("curried tree_hash: {}", out.curried.tree_hash); + } +} + fn looks_like_hex(input: &str) -> bool { let raw = input .strip_prefix("0x") diff --git a/crates/coinset-ffi/Cargo.toml b/crates/coinset-ffi/Cargo.toml index 822214c..e27d033 100644 --- a/crates/coinset-ffi/Cargo.toml +++ b/crates/coinset-ffi/Cargo.toml @@ -10,7 +10,12 @@ crate-type = ["staticlib"] [dependencies] anyhow.workspace = true +serde.workspace = true serde_json.workspace = true coinset-inspect-core = { path = "../coinset-inspect-core" } coinset-clvm-core = { path = "../coinset-clvm-core" } + +[dev-dependencies] +chia-protocol.workspace = true +clvm-utils.workspace = true diff --git a/crates/coinset-ffi/src/lib.rs b/crates/coinset-ffi/src/lib.rs index 9146d4c..74ecac6 100644 --- a/crates/coinset-ffi/src/lib.rs +++ b/crates/coinset-ffi/src/lib.rs @@ -1,9 +1,19 @@ use std::ffi::{CStr, CString, c_char}; -use coinset_clvm_core::{compile_to_hex, decompile_from_hex, run_program_text}; +use coinset_clvm_core::{ + CurryArg, compile_to_hex, curry_typed, decompile_from_hex, run_program_text, tree_hash_input, + uncurry, +}; +use serde::Deserialize; use coinset_inspect_core::{ExplainLevel, inspect_json_string}; use serde_json::json; +#[derive(Deserialize)] +struct CurryPayload { + mod_is_hash: bool, + args: Vec, +} + const FLAG_PRETTY: u32 = 1 << 0; const FLAG_CONDITIONS_ONLY: u32 = 1 << 1; const FLAG_INCLUDE_COST: u32 = 1 << 2; @@ -97,6 +107,81 @@ pub extern "C" fn coinset_clvm_run( .into_raw() } +#[unsafe(no_mangle)] +pub extern "C" fn coinset_clvm_tree_hash(input_ptr: *const u8, input_len: usize, flags: u32) -> *mut c_char { + let pretty = (flags & FLAG_PRETTY) != 0; + match with_utf8_input(input_ptr, input_len, |s| { + let out = tree_hash_input(s)?; + if pretty { + Ok(serde_json::to_string_pretty(&out)?) + } else { + Ok(serde_json::to_string(&out)?) + } + }) { + Ok(s) => CString::new(s).unwrap_or_else(|_| render_error("invalid_output", "output contained NUL")), + Err(e) => render_error("clvm_tree_hash_failed", &format!("{e:#}")), + } + .into_raw() +} + +#[unsafe(no_mangle)] +pub extern "C" fn coinset_clvm_uncurry(input_ptr: *const u8, input_len: usize, flags: u32) -> *mut c_char { + let pretty = (flags & FLAG_PRETTY) != 0; + match with_utf8_input(input_ptr, input_len, |s| { + let out = uncurry(s)?; + if pretty { + Ok(serde_json::to_string_pretty(&out)?) + } else { + Ok(serde_json::to_string(&out)?) + } + }) { + Ok(s) => CString::new(s).unwrap_or_else(|_| render_error("invalid_output", "output contained NUL")), + Err(e) => render_error("clvm_uncurry_failed", &format!("{e:#}")), + } + .into_raw() +} + +#[unsafe(no_mangle)] +pub extern "C" fn coinset_clvm_curry( + mod_ptr: *const u8, + mod_len: usize, + payload_ptr: *const u8, + payload_len: usize, + flags: u32, +) -> *mut c_char { + let pretty = (flags & FLAG_PRETTY) != 0; + + let mod_str = match unsafe_bytes_to_str(mod_ptr, mod_len) { + Ok(s) => s, + Err(e) => return render_error("invalid_utf8", &format!("{e:#}")).into_raw(), + }; + let payload_json = match unsafe_bytes_to_str(payload_ptr, payload_len) { + Ok(s) => s, + Err(e) => return render_error("invalid_utf8", &format!("{e:#}")).into_raw(), + }; + + let payload: CurryPayload = match serde_json::from_str(payload_json) { + Ok(v) => v, + Err(e) => return render_error("invalid_args", &format!("{e}")).into_raw(), + }; + + match curry_typed(mod_str, payload.mod_is_hash, &payload.args) { + Ok(out) => { + let s = if pretty { + serde_json::to_string_pretty(&out) + } else { + serde_json::to_string(&out) + }; + match s { + Ok(s) => CString::new(s).unwrap_or_else(|_| render_error("invalid_output", "output contained NUL")), + Err(e) => render_error("invalid_output", &format!("{e:#}")), + } + } + Err(e) => render_error("clvm_curry_failed", &format!("{e:#}")), + } + .into_raw() +} + #[unsafe(no_mangle)] pub extern "C" fn coinset_free(s: *mut c_char) { if s.is_null() { @@ -146,3 +231,58 @@ fn render_error(kind: &str, message: &str) -> CString { let s = serde_json::to_string(&obj).unwrap_or_else(|_| "{\"status\":\"failed\"}".to_string()); CString::new(s).unwrap_or_else(|_| CString::new("{\"status\":\"failed\"}").expect("cstr")) } + +#[cfg(test)] +mod tests { + use super::*; + use chia_protocol::{Coin, CoinSpend, Program, SpendBundle}; + use clvm_utils::tree_hash_from_bytes; + use serde_json::json; + + #[test] + fn ffi_inspect_includes_puzzle_recognition_key() { + let parent = [0x11_u8; 32]; + let puzzle = Program::from(vec![0x01_u8]); + let puzzle_hash = tree_hash_from_bytes(puzzle.as_ref()).expect("tree hash"); + let coin = Coin::new(parent.into(), puzzle_hash.into(), 1); + let solution = Program::from(vec![0x80]); + let spend = CoinSpend::new(coin, puzzle, solution); + let bundle = SpendBundle::new(vec![spend], Default::default()); + + let input = json!({ "spend_bundle": bundle }); + let input_str = serde_json::to_string(&input).expect("json"); + let ptr = input_str.as_ptr(); + let len = input_str.len(); + + let out_ptr = coinset_inspect(ptr, len, 0); + assert!(!out_ptr.is_null()); + let out = unsafe { CStr::from_ptr(out_ptr) }.to_string_lossy().to_string(); + coinset_free(out_ptr); + + let v: serde_json::Value = serde_json::from_str(&out).expect("json parse"); + let spend0 = &v["result"]["spends"][0]; + assert!(spend0.get("puzzle_recognition").is_some(), "missing puzzle_recognition key"); + } + + #[test] + fn ffi_curry_hash_mode() { + let mod_input = "37bef360ee858133b69d595a906dc45d01af50379dad515eb9518abb7c1d2a7a"; + let payload = r#"{"mod_is_hash":true,"args":[{"kind":"atom","value":"37bef360ee858133b69d595a906dc45d01af50379dad515eb9518abb7c1d2a7a"},{"kind":"atom","value":"e1c77e2f2bc1758cfd4f3f875adc9b96db322a558072a5b0b8e697d0931bd7fc"},{"kind":"program","value":"ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0b4301d5383702bb6824712a96425d343aaa4448e288de4d2b243425cdce9fd5fd19e0d4ddaacac858216644c05bac4d0ff018080"}]}"#; + + let mod_ptr = mod_input.as_ptr(); + let mod_len = mod_input.len(); + let payload_ptr = payload.as_ptr(); + let payload_len = payload.len(); + + let out_ptr = coinset_clvm_curry(mod_ptr, mod_len, payload_ptr, payload_len, 0); + assert!(!out_ptr.is_null()); + let out = unsafe { CStr::from_ptr(out_ptr) }.to_string_lossy().to_string(); + coinset_free(out_ptr); + + let v: serde_json::Value = serde_json::from_str(&out).expect("json parse"); + assert_eq!( + v["curried"]["tree_hash"].as_str().unwrap(), + "0x46a759d7e88c9f5fd43e4e09956656ea4325c947e37b8730bbc23c3be9406985" + ); + } +} diff --git a/crates/coinset-inspect-core/Cargo.toml b/crates/coinset-inspect-core/Cargo.toml index 84d78e2..da8fadb 100644 --- a/crates/coinset-inspect-core/Cargo.toml +++ b/crates/coinset-inspect-core/Cargo.toml @@ -17,3 +17,6 @@ chia-traits.workspace = true clvm-utils.workspace = true clvmr.workspace = true chialisp.workspace = true +chia-puzzle-types.workspace = true +chia-sdk-driver.workspace = true +chia-sdk-types.workspace = true diff --git a/crates/coinset-inspect-core/src/lib.rs b/crates/coinset-inspect-core/src/lib.rs index a077dcf..b999ab6 100644 --- a/crates/coinset-inspect-core/src/lib.rs +++ b/crates/coinset-inspect-core/src/lib.rs @@ -17,6 +17,8 @@ use clvmr::LIMIT_HEAP; use serde::Serialize; use serde_json::{Map, Value, json}; +mod recognize; + const DEFAULT_MAX_COST: u64 = 11_000_000_000; const DEFAULT_PREV_TX_HEIGHT: u32 = 10_000_000; @@ -86,6 +88,7 @@ pub struct SpendAnalysis { pub coin_spend: CoinSpendView, pub evaluation: EvaluationInfo, pub clvm: Option, + pub puzzle_recognition: Option, } #[derive(Debug, Clone, Serialize)] @@ -295,6 +298,14 @@ fn build_error_output( } else { None }; + let puzzle_recognition = if explain_level == ExplainLevel::Deep { + Some(recognize::recognize_puzzle_and_solution( + spend.puzzle_reveal.as_ref(), + spend.solution.as_ref(), + )) + } else { + None + }; spends.push(SpendAnalysis { coin_spend: CoinSpendView { coin: coin_ref_from_coin(&spend.coin), @@ -312,6 +323,7 @@ fn build_error_output( }), }, clvm, + puzzle_recognition, }); } @@ -471,6 +483,14 @@ fn analyze_single_spend( } else { None }, + puzzle_recognition: if explain_level == ExplainLevel::Deep { + Some(recognize::recognize_puzzle_and_solution( + spend.puzzle_reveal.as_ref(), + spend.solution.as_ref(), + )) + } else { + None + }, } } @@ -555,7 +575,7 @@ fn coin_ref_from_coin(coin: &Coin) -> CoinRef { } } -fn encode_hex_prefixed(bytes: &[u8]) -> String { +pub(crate) fn encode_hex_prefixed(bytes: &[u8]) -> String { format!("0x{}", hex::encode(bytes)) } @@ -747,4 +767,19 @@ mod tests { assert_eq!(kind.as_str(), "block"); assert_eq!(parsed.coin_spends.len(), 1); } + + #[test] + fn inspect_output_includes_puzzle_recognition_field() { + let bundle = sample_spend_bundle(); + let blob = json!({ "spend_bundle": bundle }); + let out = inspect_json_string( + &serde_json::to_string(&blob).expect("json"), + false, + ExplainLevel::Deep, + ) + .expect("inspect"); + let v: serde_json::Value = serde_json::from_str(&out).expect("json parse"); + let spend0 = &v["result"]["spends"][0]; + assert!(spend0.get("puzzle_recognition").is_some(), "missing puzzle_recognition key"); + } } diff --git a/crates/coinset-inspect-core/src/recognize.rs b/crates/coinset-inspect-core/src/recognize.rs new file mode 100644 index 0000000..04b47d0 --- /dev/null +++ b/crates/coinset-inspect-core/src/recognize.rs @@ -0,0 +1,811 @@ +use chialisp::classic::clvm::OPERATORS_LATEST_VERSION; +use chialisp::classic::clvm_tools::binutils::disassemble; +use chia_puzzle_types::did::DidSolution; +use chia_sdk_driver::{ + AugmentedConditionLayer, BulletinLayer, CatLayer, DidLayer, Layer, NftOwnershipLayer, + NftStateLayer, OptionContractLayer, P2CurriedLayer, P2DelegatedConditionsLayer, + P2OneOfManyLayer, P2SingletonLayer, Puzzle as DriverPuzzle, RevocationLayer, + RoyaltyTransferLayer, SettlementLayer, SingletonLayer, StandardLayer, StreamLayer, +}; +use clvm_utils::tree_hash; +use clvmr::allocator::NodePtr; +use clvmr::serde::node_from_bytes_backrefs; +use clvmr::Allocator; +use serde::Serialize; +use serde_json::{Value, json}; + +use crate::encode_hex_prefixed; + +const SOURCE_REPO: &str = "xch-dev/chia-wallet-sdk"; +const SOURCE_REF: &str = "0.33.0"; +const MAX_LAYER_DEPTH: usize = 32; + +#[derive(Debug, Clone, Serialize)] +pub struct PuzzleRecognition { + pub recognized: bool, + pub candidates: Vec, + pub wrappers: Vec, + pub parsed_solution: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct WrapperInfo { + pub name: String, + pub source_repo: String, + pub source_ref: String, + pub source_path: Option, + pub mod_hash: String, + pub curried_args_tree_hash: Option, + pub inner_puzzle_tree_hash: Option, + pub params: Value, + pub parse_error: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct PuzzleCandidate { + pub name: String, + pub confidence: f64, + pub source_repo: Option, + pub source_path: Option, + pub source_ref: Option, +} + +#[derive(Debug, Clone)] +struct LayerMatch { + name: &'static str, + source_path: &'static str, + params: Value, + next_puzzle: Option, + next_solution: Option, + solution: Value, + parse_error: Option, +} + +pub fn recognize_puzzle_and_solution(puzzle_reveal_bytes: &[u8], solution_bytes: &[u8]) -> PuzzleRecognition { + let mut allocator = Allocator::new(); + + let puzzle_ptr = match node_from_bytes_backrefs(&mut allocator, puzzle_reveal_bytes) { + Ok(ptr) => ptr, + Err(err) => { + return PuzzleRecognition { + recognized: false, + candidates: Vec::new(), + wrappers: Vec::new(), + parsed_solution: Some(json!({ + "layers": [], + "decode_error": format!("failed to decode puzzle_reveal bytes: {err}"), + })), + }; + } + }; + + let solution_ptr = node_from_bytes_backrefs(&mut allocator, solution_bytes).ok(); + let solution_decode_error = if solution_ptr.is_none() { + Some("failed to decode solution bytes".to_string()) + } else { + None + }; + + let mut current_puzzle = DriverPuzzle::parse(&allocator, puzzle_ptr); + let mut current_solution = solution_ptr; + let mut wrappers = Vec::::new(); + let mut candidates = Vec::::new(); + let mut solution_layers = Vec::::new(); + + for _ in 0..MAX_LAYER_DEPTH { + let matches = collect_matches(&allocator, current_puzzle, current_solution); + if matches.is_empty() { + break; + } + + if matches.len() > 1 { + for matched in &matches { + candidates.push(candidate_from_match(matched, 0.5)); + } + solution_layers.push(json!({ + "status": "ambiguous", + "options": matches.iter().map(|m| m.name).collect::>(), + })); + break; + } + + let matched = matches[0].clone(); + let current_hash = current_puzzle.curried_puzzle_hash(); + + wrappers.push(WrapperInfo { + name: matched.name.to_string(), + source_repo: SOURCE_REPO.to_string(), + source_ref: SOURCE_REF.to_string(), + source_path: Some(matched.source_path.to_string()), + mod_hash: encode_tree_hash(current_puzzle.mod_hash().as_ref()), + curried_args_tree_hash: current_puzzle + .as_curried() + .map(|curried| encode_tree_hash(tree_hash(&allocator, curried.args).as_ref())), + inner_puzzle_tree_hash: matched + .next_puzzle + .map(|p| encode_tree_hash(p.curried_puzzle_hash().as_ref())), + params: matched.params.clone(), + parse_error: matched.parse_error.clone(), + }); + + candidates.push(candidate_from_match( + &matched, + if matched.parse_error.is_some() { 0.8 } else { 1.0 }, + )); + + solution_layers.push(json!({ + "layer": matched.name, + "source_path": matched.source_path, + "result": matched.solution, + })); + + current_solution = matched.next_solution; + + let Some(next_puzzle) = matched.next_puzzle else { + break; + }; + + if next_puzzle.curried_puzzle_hash() == current_hash { + solution_layers.push(json!({ + "status": "stopped", + "reason": "next puzzle hash equals current hash", + })); + break; + } + + current_puzzle = next_puzzle; + } + + let parsed_solution = if solution_layers.is_empty() && solution_decode_error.is_none() { + None + } else { + Some(json!({ + "layers": solution_layers, + "remaining_solution": current_solution.map(|ptr| node_summary(&allocator, ptr)), + "decode_error": solution_decode_error, + })) + }; + + PuzzleRecognition { + recognized: !wrappers.is_empty(), + candidates, + wrappers, + parsed_solution, + } +} + +fn collect_matches( + allocator: &Allocator, + puzzle: DriverPuzzle, + solution: Option, +) -> Vec { + let mut matches = Vec::new(); + + if let Some(matched) = try_cat_layer(allocator, puzzle, solution) { + matches.push(matched); + } + if let Some(matched) = try_singleton_layer(allocator, puzzle, solution) { + matches.push(matched); + } + if let Some(matched) = try_did_layer(allocator, puzzle, solution) { + matches.push(matched); + } + if let Some(matched) = try_nft_state_layer(allocator, puzzle, solution) { + matches.push(matched); + } + if let Some(matched) = try_nft_ownership_layer(allocator, puzzle, solution) { + matches.push(matched); + } + if let Some(matched) = try_royalty_transfer_layer(allocator, puzzle) { + matches.push(matched); + } + if let Some(matched) = try_augmented_condition_layer(allocator, puzzle, solution) { + matches.push(matched); + } + if let Some(matched) = try_bulletin_layer(allocator, puzzle, solution) { + matches.push(matched); + } + if let Some(matched) = try_option_contract_layer(allocator, puzzle, solution) { + matches.push(matched); + } + if let Some(matched) = try_revocation_layer(allocator, puzzle, solution) { + matches.push(matched); + } + if let Some(matched) = try_p2_singleton_layer(allocator, puzzle, solution) { + matches.push(matched); + } + if let Some(matched) = try_p2_curried_layer(allocator, puzzle, solution) { + matches.push(matched); + } + if let Some(matched) = try_p2_one_of_many_layer(allocator, puzzle, solution) { + matches.push(matched); + } + if let Some(matched) = try_p2_delegated_conditions_layer(allocator, puzzle, solution) { + matches.push(matched); + } + if let Some(matched) = try_settlement_layer(allocator, puzzle, solution) { + matches.push(matched); + } + if let Some(matched) = try_stream_layer(allocator, puzzle, solution) { + matches.push(matched); + } + if let Some(matched) = try_standard_layer(allocator, puzzle, solution) { + matches.push(matched); + } + + matches +} + +fn try_cat_layer(allocator: &Allocator, puzzle: DriverPuzzle, solution: Option) -> Option { + let layer = CatLayer::::parse_puzzle(allocator, puzzle).ok()??; + let mut parse_error = None; + let (next_solution, solution_json) = match solution { + Some(ptr) => match CatLayer::::parse_solution(allocator, ptr) { + Ok(parsed) => ( + Some(parsed.inner_puzzle_solution), + json!({ + "status": "ok", + "inner_solution_tree_hash": node_tree_hash_hex(allocator, parsed.inner_puzzle_solution), + }), + ), + Err(err) => { + parse_error = Some(format!("failed to parse CAT solution: {err}")); + (None, json!({ "status": "error", "message": err.to_string() })) + } + }, + None => (None, json!({ "status": "missing_solution" })), + }; + + Some(LayerMatch { + name: "cat_layer", + source_path: "crates/chia-sdk-driver/src/layers/cat_layer.rs", + params: json!({ "asset_id": encode_hex_prefixed(layer.asset_id.as_ref()) }), + next_puzzle: Some(layer.inner_puzzle), + next_solution, + solution: solution_json, + parse_error, + }) +} + +fn try_singleton_layer(allocator: &Allocator, puzzle: DriverPuzzle, solution: Option) -> Option { + let layer = SingletonLayer::::parse_puzzle(allocator, puzzle).ok().flatten()?; + let mut parse_error = None; + let (next_solution, solution_json) = match solution { + Some(ptr) => match SingletonLayer::::parse_solution(allocator, ptr) { + Ok(parsed) => ( + Some(parsed.inner_solution), + json!({ "status": "ok", "inner_solution_tree_hash": node_tree_hash_hex(allocator, parsed.inner_solution) }), + ), + Err(err) => { + parse_error = Some(format!("failed to parse singleton solution: {err}")); + (None, json!({ "status": "error", "message": err.to_string() })) + } + }, + None => (None, json!({ "status": "missing_solution" })), + }; + + Some(LayerMatch { + name: "singleton_layer", + source_path: "crates/chia-sdk-driver/src/layers/singleton_layer.rs", + params: json!({ "launcher_id": encode_hex_prefixed(layer.launcher_id.as_ref()) }), + next_puzzle: Some(layer.inner_puzzle), + next_solution, + solution: solution_json, + parse_error, + }) +} + +fn try_did_layer(allocator: &Allocator, puzzle: DriverPuzzle, solution: Option) -> Option { + let layer = DidLayer::::parse_puzzle(allocator, puzzle).ok().flatten()?; + let mut parse_error = None; + let (next_solution, solution_json) = match solution { + Some(ptr) => match DidLayer::::parse_solution(allocator, ptr) { + Ok(DidSolution::Spend(inner_solution)) => ( + Some(inner_solution), + json!({ "status": "ok", "kind": "spend", "inner_solution_tree_hash": node_tree_hash_hex(allocator, inner_solution) }), + ), + Ok(DidSolution::Recover(recovery)) => ( + None, + json!({ "status": "ok", "kind": "recover", "parsed_debug": format!("{recovery:?}") }), + ), + Err(err) => { + parse_error = Some(format!("failed to parse DID solution: {err}")); + (None, json!({ "status": "error", "message": err.to_string() })) + } + }, + None => (None, json!({ "status": "missing_solution" })), + }; + + Some(LayerMatch { + name: "did_layer", + source_path: "crates/chia-sdk-driver/src/layers/did_layer.rs", + params: json!({ + "launcher_id": encode_hex_prefixed(layer.launcher_id.as_ref()), + "recovery_list_hash": layer.recovery_list_hash.map(|h| encode_hex_prefixed(h.as_ref())), + "num_verifications_required": layer.num_verifications_required, + "metadata_tree_hash": node_tree_hash_hex(allocator, layer.metadata), + }), + next_puzzle: Some(layer.inner_puzzle), + next_solution, + solution: solution_json, + parse_error, + }) +} + +fn try_nft_state_layer(allocator: &Allocator, puzzle: DriverPuzzle, solution: Option) -> Option { + let layer = NftStateLayer::::parse_puzzle(allocator, puzzle).ok().flatten()?; + let mut parse_error = None; + let (next_solution, solution_json) = match solution { + Some(ptr) => match NftStateLayer::::parse_solution(allocator, ptr) { + Ok(parsed) => ( + Some(parsed.inner_solution), + json!({ "status": "ok", "inner_solution_tree_hash": node_tree_hash_hex(allocator, parsed.inner_solution) }), + ), + Err(err) => { + parse_error = Some(format!("failed to parse NFT state solution: {err}")); + (None, json!({ "status": "error", "message": err.to_string() })) + } + }, + None => (None, json!({ "status": "missing_solution" })), + }; + + Some(LayerMatch { + name: "nft_state_layer", + source_path: "crates/chia-sdk-driver/src/layers/nft_state_layer.rs", + params: json!({ + "metadata_updater_puzzle_hash": encode_hex_prefixed(layer.metadata_updater_puzzle_hash.as_ref()), + "metadata_tree_hash": node_tree_hash_hex(allocator, layer.metadata), + }), + next_puzzle: Some(layer.inner_puzzle), + next_solution, + solution: solution_json, + parse_error, + }) +} + +fn try_nft_ownership_layer( + allocator: &Allocator, + puzzle: DriverPuzzle, + solution: Option, +) -> Option { + let layer = NftOwnershipLayer::::parse_puzzle(allocator, puzzle).ok().flatten()?; + let mut parse_error = None; + let (next_solution, solution_json) = match solution { + Some(ptr) => match NftOwnershipLayer::::parse_solution(allocator, ptr) { + Ok(parsed) => ( + Some(parsed.inner_solution), + json!({ "status": "ok", "inner_solution_tree_hash": node_tree_hash_hex(allocator, parsed.inner_solution) }), + ), + Err(err) => { + parse_error = Some(format!("failed to parse NFT ownership solution: {err}")); + (None, json!({ "status": "error", "message": err.to_string() })) + } + }, + None => (None, json!({ "status": "missing_solution" })), + }; + + Some(LayerMatch { + name: "nft_ownership_layer", + source_path: "crates/chia-sdk-driver/src/layers/nft_ownership_layer.rs", + params: json!({ + "current_owner": layer.current_owner.map(|owner| encode_hex_prefixed(owner.as_ref())), + "transfer_layer_tree_hash": encode_tree_hash(layer.transfer_layer.curried_puzzle_hash().as_ref()), + }), + next_puzzle: Some(layer.inner_puzzle), + next_solution, + solution: solution_json, + parse_error, + }) +} + +fn try_royalty_transfer_layer(allocator: &Allocator, puzzle: DriverPuzzle) -> Option { + let layer = RoyaltyTransferLayer::parse_puzzle(allocator, puzzle).ok().flatten()?; + Some(LayerMatch { + name: "royalty_transfer_layer", + source_path: "crates/chia-sdk-driver/src/layers/royalty_transfer_layer.rs", + params: json!({ + "launcher_id": encode_hex_prefixed(layer.launcher_id.as_ref()), + "royalty_puzzle_hash": encode_hex_prefixed(layer.royalty_puzzle_hash.as_ref()), + "royalty_basis_points": layer.royalty_basis_points, + }), + next_puzzle: None, + next_solution: None, + solution: json!({ "status": "unsupported" }), + parse_error: None, + }) +} + +fn try_augmented_condition_layer( + allocator: &Allocator, + puzzle: DriverPuzzle, + solution: Option, +) -> Option { + let layer = AugmentedConditionLayer::::parse_puzzle(allocator, puzzle).ok().flatten()?; + let mut parse_error = None; + let (next_solution, solution_json) = match solution { + Some(ptr) => match AugmentedConditionLayer::::parse_solution(allocator, ptr) { + Ok(parsed) => ( + Some(parsed.inner_solution), + json!({ "status": "ok", "inner_solution_tree_hash": node_tree_hash_hex(allocator, parsed.inner_solution) }), + ), + Err(err) => { + parse_error = Some(format!("failed to parse augmented condition solution: {err}")); + (None, json!({ "status": "error", "message": err.to_string() })) + } + }, + None => (None, json!({ "status": "missing_solution" })), + }; + + Some(LayerMatch { + name: "augmented_condition_layer", + source_path: "crates/chia-sdk-driver/src/layers/augmented_condition_layer.rs", + params: json!({}), + next_puzzle: Some(layer.inner_puzzle), + next_solution, + solution: solution_json, + parse_error, + }) +} + +fn try_bulletin_layer(allocator: &Allocator, puzzle: DriverPuzzle, solution: Option) -> Option { + let layer = BulletinLayer::::parse_puzzle(allocator, puzzle).ok().flatten()?; + let mut parse_error = None; + let (next_solution, solution_json) = match solution { + Some(ptr) => match BulletinLayer::::parse_solution(allocator, ptr) { + Ok(inner_solution) => ( + Some(inner_solution), + json!({ "status": "ok", "inner_solution_tree_hash": node_tree_hash_hex(allocator, inner_solution) }), + ), + Err(err) => { + parse_error = Some(format!("failed to parse bulletin solution: {err}")); + (None, json!({ "status": "error", "message": err.to_string() })) + } + }, + None => (None, json!({ "status": "missing_solution" })), + }; + + Some(LayerMatch { + name: "bulletin_layer", + source_path: "crates/chia-sdk-driver/src/layers/bulletin_layer.rs", + params: json!({}), + next_puzzle: Some(layer.inner_puzzle), + next_solution, + solution: solution_json, + parse_error, + }) +} + +fn try_option_contract_layer( + allocator: &Allocator, + puzzle: DriverPuzzle, + solution: Option, +) -> Option { + let layer = OptionContractLayer::::parse_puzzle(allocator, puzzle).ok().flatten()?; + let mut parse_error = None; + let (next_solution, solution_json) = match solution { + Some(ptr) => match OptionContractLayer::::parse_solution(allocator, ptr) { + Ok(parsed) => ( + Some(parsed.inner_solution), + json!({ "status": "ok", "inner_solution_tree_hash": node_tree_hash_hex(allocator, parsed.inner_solution) }), + ), + Err(err) => { + parse_error = Some(format!("failed to parse option contract solution: {err}")); + (None, json!({ "status": "error", "message": err.to_string() })) + } + }, + None => (None, json!({ "status": "missing_solution" })), + }; + + Some(LayerMatch { + name: "option_contract_layer", + source_path: "crates/chia-sdk-driver/src/layers/option_contract_layer.rs", + params: json!({ + "underlying_coin_id": encode_hex_prefixed(layer.underlying_coin_id.as_ref()), + "underlying_delegated_puzzle_hash": encode_hex_prefixed(layer.underlying_delegated_puzzle_hash.as_ref()), + }), + next_puzzle: Some(layer.inner_puzzle), + next_solution, + solution: solution_json, + parse_error, + }) +} + +fn try_revocation_layer(allocator: &Allocator, puzzle: DriverPuzzle, solution: Option) -> Option { + let layer = RevocationLayer::parse_puzzle(allocator, puzzle).ok().flatten()?; + let mut parse_error = None; + let (next_solution, solution_json) = match solution { + Some(ptr) => match RevocationLayer::parse_solution(allocator, ptr) { + Ok(parsed) => ( + None, + json!({ + "status": "ok", + "hidden": parsed.hidden, + "puzzle_tree_hash": node_tree_hash_hex(allocator, parsed.puzzle), + "solution_tree_hash": node_tree_hash_hex(allocator, parsed.solution), + }), + ), + Err(err) => { + parse_error = Some(format!("failed to parse revocation solution: {err}")); + (None, json!({ "status": "error", "message": err.to_string() })) + } + }, + None => (None, json!({ "status": "missing_solution" })), + }; + + Some(LayerMatch { + name: "revocation_layer", + source_path: "crates/chia-sdk-driver/src/layers/revocation_layer.rs", + params: json!({ + "hidden_puzzle_hash": encode_hex_prefixed(layer.hidden_puzzle_hash.as_ref()), + "inner_puzzle_hash": encode_hex_prefixed(layer.inner_puzzle_hash.as_ref()), + }), + next_puzzle: None, + next_solution, + solution: solution_json, + parse_error, + }) +} + +fn try_p2_singleton_layer( + allocator: &Allocator, + puzzle: DriverPuzzle, + solution: Option, +) -> Option { + let layer = P2SingletonLayer::parse_puzzle(allocator, puzzle).ok().flatten()?; + let mut parse_error = None; + let (_next_solution, solution_json): (Option, Value) = match solution { + Some(ptr) => match P2SingletonLayer::parse_solution(allocator, ptr) { + Ok(parsed) => ( + None, + json!({ + "status": "ok", + "singleton_inner_puzzle_hash": encode_hex_prefixed(parsed.singleton_inner_puzzle_hash.as_ref()), + "my_id": encode_hex_prefixed(parsed.my_id.as_ref()), + }), + ), + Err(err) => { + parse_error = Some(format!("failed to parse p2_singleton solution: {err}")); + (None, json!({ "status": "error", "message": err.to_string() })) + } + }, + None => (None, json!({ "status": "missing_solution" })), + }; + + Some(LayerMatch { + name: "p2_singleton_layer", + source_path: "crates/chia-sdk-driver/src/layers/p2_singleton_layer.rs", + params: json!({ "launcher_id": encode_hex_prefixed(layer.launcher_id.as_ref()) }), + next_puzzle: None, + next_solution: None, + solution: solution_json, + parse_error, + }) +} + +fn try_p2_curried_layer( + allocator: &Allocator, + puzzle: DriverPuzzle, + solution: Option, +) -> Option { + let layer = P2CurriedLayer::parse_puzzle(allocator, puzzle).ok().flatten()?; + let mut parse_error = None; + let (_next_solution, solution_json): (Option, Value) = match solution { + Some(ptr) => match P2CurriedLayer::parse_solution(allocator, ptr) { + Ok(parsed) => ( + None, + json!({ + "status": "ok", + "puzzle_tree_hash": node_tree_hash_hex(allocator, parsed.puzzle), + "solution_tree_hash": node_tree_hash_hex(allocator, parsed.solution), + }), + ), + Err(err) => { + parse_error = Some(format!("failed to parse p2_curried solution: {err}")); + (None, json!({ "status": "error", "message": err.to_string() })) + } + }, + None => (None, json!({ "status": "missing_solution" })), + }; + + Some(LayerMatch { + name: "p2_curried_layer", + source_path: "crates/chia-sdk-driver/src/layers/p2_curried_layer.rs", + params: json!({ "puzzle_hash": encode_hex_prefixed(layer.puzzle_hash.as_ref()) }), + next_puzzle: None, + next_solution: None, + solution: solution_json, + parse_error, + }) +} + +fn try_p2_one_of_many_layer( + allocator: &Allocator, + puzzle: DriverPuzzle, + solution: Option, +) -> Option { + let layer = P2OneOfManyLayer::parse_puzzle(allocator, puzzle).ok().flatten()?; + let mut parse_error = None; + let (_next_solution, solution_json): (Option, Value) = match solution { + Some(ptr) => match P2OneOfManyLayer::parse_solution(allocator, ptr) { + Ok(parsed) => ( + None, + json!({ + "status": "ok", + "puzzle_tree_hash": node_tree_hash_hex(allocator, parsed.puzzle), + "solution_tree_hash": node_tree_hash_hex(allocator, parsed.solution), + }), + ), + Err(err) => { + parse_error = Some(format!("failed to parse p2_one_of_many solution: {err}")); + (None, json!({ "status": "error", "message": err.to_string() })) + } + }, + None => (None, json!({ "status": "missing_solution" })), + }; + + Some(LayerMatch { + name: "p2_one_of_many_layer", + source_path: "crates/chia-sdk-driver/src/layers/p2_one_of_many_layer.rs", + params: json!({ "merkle_root": encode_hex_prefixed(layer.merkle_root.as_ref()) }), + next_puzzle: None, + next_solution: None, + solution: solution_json, + parse_error, + }) +} + +fn try_p2_delegated_conditions_layer( + allocator: &Allocator, + puzzle: DriverPuzzle, + solution: Option, +) -> Option { + let layer = P2DelegatedConditionsLayer::parse_puzzle(allocator, puzzle).ok().flatten()?; + let mut parse_error = None; + let (_next_solution, solution_json): (Option, Value) = match solution { + Some(ptr) => match P2DelegatedConditionsLayer::parse_solution(allocator, ptr) { + Ok(parsed) => (None, json!({ "status": "ok", "conditions_len": parsed.conditions.len() })), + Err(err) => { + parse_error = Some(format!("failed to parse p2_delegated_conditions solution: {err}")); + (None, json!({ "status": "error", "message": err.to_string() })) + } + }, + None => (None, json!({ "status": "missing_solution" })), + }; + + Some(LayerMatch { + name: "p2_delegated_conditions_layer", + source_path: "crates/chia-sdk-driver/src/layers/p2_delegated_conditions_layer.rs", + params: json!({ "public_key": encode_hex_prefixed(&layer.public_key.to_bytes()) }), + next_puzzle: None, + next_solution: None, + solution: solution_json, + parse_error, + }) +} + +fn try_settlement_layer( + allocator: &Allocator, + puzzle: DriverPuzzle, + solution: Option, +) -> Option { + let _layer = SettlementLayer::parse_puzzle(allocator, puzzle).ok().flatten()?; + let mut parse_error = None; + let (_next_solution, solution_json): (Option, Value) = match solution { + Some(ptr) => match SettlementLayer::parse_solution(allocator, ptr) { + Ok(parsed) => (None, json!({ "status": "ok", "payments_len": parsed.notarized_payments.len() })), + Err(err) => { + parse_error = Some(format!("failed to parse settlement solution: {err}")); + (None, json!({ "status": "error", "message": err.to_string() })) + } + }, + None => (None, json!({ "status": "missing_solution" })), + }; + + Some(LayerMatch { + name: "settlement_layer", + source_path: "crates/chia-sdk-driver/src/layers/settlement_layer.rs", + params: json!({}), + next_puzzle: None, + next_solution: None, + solution: solution_json, + parse_error, + }) +} + +fn try_stream_layer(allocator: &Allocator, puzzle: DriverPuzzle, solution: Option) -> Option { + let layer = StreamLayer::parse_puzzle(allocator, puzzle).ok().flatten()?; + let mut parse_error = None; + let (_next_solution, solution_json): (Option, Value) = match solution { + Some(ptr) => match StreamLayer::parse_solution(allocator, ptr) { + Ok(parsed) => ( + None, + json!({ + "status": "ok", + "my_amount": parsed.my_amount, + "payment_time": parsed.payment_time, + "to_pay": parsed.to_pay, + "clawback": parsed.clawback, + }), + ), + Err(err) => { + parse_error = Some(format!("failed to parse stream solution: {err}")); + (None, json!({ "status": "error", "message": err.to_string() })) + } + }, + None => (None, json!({ "status": "missing_solution" })), + }; + + Some(LayerMatch { + name: "stream_layer", + source_path: "crates/chia-sdk-driver/src/layers/streaming_layer.rs", + params: json!({ + "recipient": encode_hex_prefixed(layer.recipient.as_ref()), + "clawback_ph": layer.clawback_ph.map(|value| encode_hex_prefixed(value.as_ref())), + "end_time": layer.end_time, + "last_payment_time": layer.last_payment_time, + }), + next_puzzle: None, + next_solution: None, + solution: solution_json, + parse_error, + }) +} + +fn try_standard_layer(allocator: &Allocator, puzzle: DriverPuzzle, solution: Option) -> Option { + let layer = StandardLayer::parse_puzzle(allocator, puzzle).ok().flatten()?; + let mut parse_error = None; + let (_next_solution, solution_json): (Option, Value) = match solution { + Some(ptr) => match StandardLayer::parse_solution(allocator, ptr) { + Ok(parsed) => ( + None, + json!({ + "status": "ok", + "has_original_public_key": parsed.original_public_key.is_some(), + "delegated_puzzle_tree_hash": node_tree_hash_hex(allocator, parsed.delegated_puzzle), + "delegated_solution_tree_hash": node_tree_hash_hex(allocator, parsed.solution), + }), + ), + Err(err) => { + parse_error = Some(format!("failed to parse standard solution: {err}")); + (None, json!({ "status": "error", "message": err.to_string() })) + } + }, + None => (None, json!({ "status": "missing_solution" })), + }; + + Some(LayerMatch { + name: "standard_layer", + source_path: "crates/chia-sdk-driver/src/layers/standard_layer.rs", + params: json!({ "synthetic_key": encode_hex_prefixed(&layer.synthetic_key.to_bytes()) }), + next_puzzle: None, + next_solution: None, + solution: solution_json, + parse_error, + }) +} + +fn candidate_from_match(matched: &LayerMatch, confidence: f64) -> PuzzleCandidate { + PuzzleCandidate { + name: matched.name.to_string(), + confidence, + source_repo: Some(SOURCE_REPO.to_string()), + source_path: Some(matched.source_path.to_string()), + source_ref: Some(SOURCE_REF.to_string()), + } +} + +fn node_summary(allocator: &Allocator, ptr: NodePtr) -> Value { + json!({ + "tree_hash": node_tree_hash_hex(allocator, ptr), + "disasm": disassemble(allocator, ptr, Some(OPERATORS_LATEST_VERSION)), + }) +} + +fn node_tree_hash_hex(allocator: &Allocator, ptr: NodePtr) -> String { + encode_tree_hash(tree_hash(allocator, ptr).as_ref()) +} + +fn encode_tree_hash(bytes: &[u8]) -> String { + encode_hex_prefixed(bytes) +} + diff --git a/internal/cmd/coinset/clvm.go b/internal/cmd/coinset/clvm.go index 9f94ef3..14ebf7a 100644 --- a/internal/cmd/coinset/clvm.go +++ b/internal/cmd/coinset/clvm.go @@ -1,8 +1,10 @@ package cmd import ( + "encoding/hex" "fmt" "log" + "strings" "github.com/coinset-org/cli/internal/coinsetffi" "github.com/spf13/cobra" @@ -10,7 +12,7 @@ import ( var clvmCmd = &cobra.Command{ Use: "clvm", - Short: "CLVM utilities (compile/decompile/run)", + Short: "CLVM utilities (compile/decompile/run/curry/uncurry/tree_hash)", } func init() { @@ -19,11 +21,18 @@ func init() { clvmCmd.AddCommand(clvmDecompileCmd) clvmCmd.AddCommand(clvmCompileCmd) clvmCmd.AddCommand(clvmRunCmd) + clvmCmd.AddCommand(clvmTreeHashCmd) + clvmCmd.AddCommand(clvmCurryCmd) + clvmCmd.AddCommand(clvmUncurryCmd) clvmRunCmd.Flags().String("program", "", "CLVM program (hex bytes or s-expression)") clvmRunCmd.Flags().String("env", "()", "CLVM environment (hex bytes or s-expression)") clvmRunCmd.Flags().Uint64("max-cost", 0, "Maximum cost (0 = unlimited)") clvmRunCmd.Flags().Bool("cost", false, "Include cost in output") + + clvmCurryCmd.Flags().StringArray("atom", nil, "Curry arg as raw atom bytes (repeatable)") + clvmCurryCmd.Flags().StringArray("tree-hash", nil, "Curry arg as pre-computed tree hash (repeatable)") + clvmCurryCmd.Flags().StringArray("program", nil, "Curry arg as serialized CLVM program (repeatable)") } var clvmDecompileCmd = &cobra.Command{ @@ -89,3 +98,79 @@ var clvmRunCmd = &cobra.Command{ printJson(out) }, } + +var clvmTreeHashCmd = &cobra.Command{ + Use: "tree_hash ", + Short: "Compute CLVM tree hash (hex bytes or s-expression)", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + out, err := coinsetffi.ClvmTreeHash(args[0], false) + if err != nil { + log.Fatal(err.Error()) + } + printJson(out) + }, +} + +var clvmCurryCmd = &cobra.Command{ + Use: "curry [arg1] [arg2] ... [--atom val] [--tree-hash val] [--program val]", + Short: "Curry arguments into a CLVM program (inputs are hex bytes or s-expressions)", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + modInput := args[0] + modIsHash := isThirtyTwoByteHex(modInput) + + var curryArgs []coinsetffi.CurryArg + + for _, a := range args[1:] { + kind := "program" + if isThirtyTwoByteHex(a) { + kind = "atom" + } + curryArgs = append(curryArgs, coinsetffi.CurryArg{Kind: kind, Value: a}) + } + + atomArgs, _ := cmd.Flags().GetStringArray("atom") + for _, v := range atomArgs { + curryArgs = append(curryArgs, coinsetffi.CurryArg{Kind: "atom", Value: v}) + } + + treeHashArgs, _ := cmd.Flags().GetStringArray("tree-hash") + for _, v := range treeHashArgs { + curryArgs = append(curryArgs, coinsetffi.CurryArg{Kind: "tree_hash", Value: v}) + } + + programArgs, _ := cmd.Flags().GetStringArray("program") + for _, v := range programArgs { + curryArgs = append(curryArgs, coinsetffi.CurryArg{Kind: "program", Value: v}) + } + + out, err := coinsetffi.ClvmCurry(modInput, modIsHash, curryArgs, false) + if err != nil { + log.Fatal(err.Error()) + } + printJson(out) + }, +} + +func isThirtyTwoByteHex(s string) bool { + raw := strings.TrimPrefix(strings.TrimPrefix(s, "0x"), "0X") + if len(raw) != 64 { + return false + } + _, err := hex.DecodeString(raw) + return err == nil +} + +var clvmUncurryCmd = &cobra.Command{ + Use: "uncurry ", + Short: "Uncurry a CLVM program (hex bytes or s-expression)", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + out, err := coinsetffi.ClvmUncurry(args[0], false) + if err != nil { + log.Fatal(err.Error()) + } + printJson(out) + }, +} diff --git a/internal/cmd/coinset/inspect_test.go b/internal/cmd/coinset/inspect_test.go new file mode 100644 index 0000000..5df027d --- /dev/null +++ b/internal/cmd/coinset/inspect_test.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "encoding/json" + "testing" +) + +func TestInspectRpcOutputAddsPuzzleRecognition(t *testing.T) { + // coin_spends array shape (like block spends); inspectRpcOutput should transform it. + input := map[string]any{ + "coin_spends": []any{ + map[string]any{ + "coin": map[string]any{ + "parent_coin_info": "0x" + repeat("11", 32), + "puzzle_hash": "0x" + repeat("22", 32), + "amount": 1, + }, + "puzzle_reveal": "0x01", + "solution": "0x80", + }, + }, + } + b, _ := json.Marshal(input) + out, err := inspectRpcOutput(b) + if err != nil { + t.Fatalf("inspectRpcOutput failed: %v", err) + } + var v map[string]any + if err := json.Unmarshal(out, &v); err != nil { + t.Fatalf("output not JSON: %v", err) + } + result := v["result"].(map[string]any) + spends := result["spends"].([]any) + spend0 := spends[0].(map[string]any) + if _, ok := spend0["puzzle_recognition"]; !ok { + t.Fatalf("missing puzzle_recognition key; keys=%v", keys(spend0)) + } +} + +func repeat(s string, n int) string { + out := "" + for i := 0; i < n; i++ { + out += s + } + return out +} + +func keys(m map[string]any) []string { + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + return out +} diff --git a/internal/coinsetffi/coinsetffi.go b/internal/coinsetffi/coinsetffi.go index 3778587..eda73b8 100644 --- a/internal/coinsetffi/coinsetffi.go +++ b/internal/coinsetffi/coinsetffi.go @@ -11,17 +11,22 @@ package coinsetffi #cgo windows,amd64 LDFLAGS: -L${SRCDIR}/../../cgo-lib/x86_64-pc-windows-gnu #cgo LDFLAGS: -lcoinset #cgo linux LDFLAGS: -lm +#cgo windows LDFLAGS: -lbcrypt -lntdll -lws2_32 char* coinset_inspect(const uint8_t* input_ptr, size_t input_len, uint32_t flags); char* coinset_clvm_decompile(const uint8_t* input_ptr, size_t input_len, uint32_t flags); char* coinset_clvm_compile(const uint8_t* input_ptr, size_t input_len, uint32_t flags); char* coinset_clvm_run(const uint8_t* program_ptr, size_t program_len, const uint8_t* env_ptr, size_t env_len, uint64_t max_cost, uint32_t flags); +char* coinset_clvm_tree_hash(const uint8_t* input_ptr, size_t input_len, uint32_t flags); +char* coinset_clvm_uncurry(const uint8_t* input_ptr, size_t input_len, uint32_t flags); +char* coinset_clvm_curry(const uint8_t* mod_ptr, size_t mod_len, const uint8_t* args_json_ptr, size_t args_json_len, uint32_t flags); void coinset_free(char* s); const char* coinset_version(void); */ import "C" import ( + "encoding/json" "errors" "unsafe" ) @@ -100,6 +105,64 @@ func ClvmRun(program string, env string, maxCost uint64, includeCost bool, prett return readAndFree(out) } +func ClvmTreeHash(input string, pretty bool) ([]byte, error) { + in := []byte(input) + if len(in) == 0 { + return nil, errors.New("empty input") + } + var flags C.uint32_t + if pretty { + flags |= flagPretty + } + ptr := (*C.uint8_t)(unsafe.Pointer(&in[0])) + out := C.coinset_clvm_tree_hash(ptr, C.size_t(len(in)), flags) + return readAndFree(out) +} + +func ClvmUncurry(input string, pretty bool) ([]byte, error) { + in := []byte(input) + if len(in) == 0 { + return nil, errors.New("empty input") + } + var flags C.uint32_t + if pretty { + flags |= flagPretty + } + ptr := (*C.uint8_t)(unsafe.Pointer(&in[0])) + out := C.coinset_clvm_uncurry(ptr, C.size_t(len(in)), flags) + return readAndFree(out) +} + +type CurryArg struct { + Kind string `json:"kind"` + Value string `json:"value"` +} + +type curryPayload struct { + ModIsHash bool `json:"mod_is_hash"` + Args []CurryArg `json:"args"` +} + +func ClvmCurry(modInput string, modIsHash bool, args []CurryArg, pretty bool) ([]byte, error) { + m := []byte(modInput) + if len(m) == 0 { + return nil, errors.New("empty mod") + } + payload := curryPayload{ModIsHash: modIsHash, Args: args} + payloadJSON, err := json.Marshal(payload) + if err != nil { + return nil, err + } + var flags C.uint32_t + if pretty { + flags |= flagPretty + } + mPtr := (*C.uint8_t)(unsafe.Pointer(&m[0])) + pPtr := (*C.uint8_t)(unsafe.Pointer(&payloadJSON[0])) + out := C.coinset_clvm_curry(mPtr, C.size_t(len(m)), pPtr, C.size_t(len(payloadJSON)), flags) + return readAndFree(out) +} + func readAndFree(out *C.char) ([]byte, error) { if out == nil { return nil, errors.New("ffi returned null") diff --git a/internal/coinsetffi/coinsetffi_nocgo.go b/internal/coinsetffi/coinsetffi_nocgo.go index c7dc6ca..48164d0 100644 --- a/internal/coinsetffi/coinsetffi_nocgo.go +++ b/internal/coinsetffi/coinsetffi_nocgo.go @@ -19,3 +19,15 @@ func ClvmCompile(_ string, _ bool) ([]byte, error) { func ClvmRun(_ string, _ string, _ uint64, _ bool, _ bool) ([]byte, error) { return nil, errors.New("coinset ffi requires cgo-enabled build") } + +func ClvmTreeHash(_ string, _ bool) ([]byte, error) { + return nil, errors.New("coinset ffi requires cgo-enabled build") +} + +func ClvmUncurry(_ string, _ bool) ([]byte, error) { + return nil, errors.New("coinset ffi requires cgo-enabled build") +} + +func ClvmCurry(_ string, _ []string, _ bool) ([]byte, error) { + return nil, errors.New("coinset ffi requires cgo-enabled build") +} diff --git a/internal/coinsetffi/coinsetffi_test.go b/internal/coinsetffi/coinsetffi_test.go new file mode 100644 index 0000000..92e09cf --- /dev/null +++ b/internal/coinsetffi/coinsetffi_test.go @@ -0,0 +1,70 @@ +//go:build cgo + +package coinsetffi + +import ( + "encoding/json" + "testing" +) + +func TestInspectIncludesPuzzleRecognitionKey(t *testing.T) { + // Minimal spend bundle shape (bytes are toy, recognition may be empty, but key must exist). + input := map[string]any{ + "spend_bundle": map[string]any{ + "coin_spends": []any{ + map[string]any{ + "coin": map[string]any{ + "parent_coin_info": "0x" + repeat("11", 32), + "puzzle_hash": "0x" + repeat("22", 32), + "amount": 1, + }, + "puzzle_reveal": "0x01", + "solution": "0x80", + }, + }, + // Valid G2 point encoding for "infinity": 0xc0 followed by 95 zero bytes. + "aggregated_signature": "0xc0" + repeat("00", 95), + }, + } + b, _ := json.Marshal(input) + out, err := Inspect(b, false, false) + if err != nil { + t.Fatalf("Inspect failed: %v", err) + } + var v map[string]any + if err := json.Unmarshal(out, &v); err != nil { + t.Fatalf("output not JSON: %v", err) + } + result, ok := v["result"].(map[string]any) + if !ok { + t.Fatalf("missing result object; out=%s", string(out)) + } + spendsAny, ok := result["spends"] + if !ok { + t.Fatalf("missing spends; out=%s", string(out)) + } + spends, ok := spendsAny.([]any) + if !ok || len(spends) == 0 { + t.Fatalf("spends not array/non-empty; out=%s", string(out)) + } + spend0 := spends[0].(map[string]any) + if _, ok := spend0["puzzle_recognition"]; !ok { + t.Fatalf("missing puzzle_recognition key; keys=%v", keys(spend0)) + } +} + +func repeat(s string, n int) string { + out := "" + for i := 0; i < n; i++ { + out += s + } + return out +} + +func keys(m map[string]any) []string { + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + return out +} From 9aaebbdf42295ee183c5acb2dfda6f8010a9a4f1 Mon Sep 17 00:00:00 2001 From: Cameron Cooper Date: Wed, 4 Mar 2026 15:49:03 -0600 Subject: [PATCH 8/9] add -luserenv for win --- internal/coinsetffi/coinsetffi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/coinsetffi/coinsetffi.go b/internal/coinsetffi/coinsetffi.go index eda73b8..f5174f7 100644 --- a/internal/coinsetffi/coinsetffi.go +++ b/internal/coinsetffi/coinsetffi.go @@ -11,7 +11,7 @@ package coinsetffi #cgo windows,amd64 LDFLAGS: -L${SRCDIR}/../../cgo-lib/x86_64-pc-windows-gnu #cgo LDFLAGS: -lcoinset #cgo linux LDFLAGS: -lm -#cgo windows LDFLAGS: -lbcrypt -lntdll -lws2_32 +#cgo windows LDFLAGS: -lbcrypt -lntdll -lws2_32 -luserenv char* coinset_inspect(const uint8_t* input_ptr, size_t input_len, uint32_t flags); char* coinset_clvm_decompile(const uint8_t* input_ptr, size_t input_len, uint32_t flags); From 5f59f60635ecbe352fa97b9e5ee5637c5ec70dea Mon Sep 17 00:00:00 2001 From: Cameron Cooper Date: Wed, 8 Jul 2026 17:57:46 -0500 Subject: [PATCH 9/9] add coinset api gap commands Co-authored-by: Cursor --- internal/cmd/coinset/events.go | 84 +++++++++++++++++- internal/cmd/coinset/get_reorgs.go | 47 ++++++++++ internal/cmd/coinset/pretty_common.go | 34 ++++++++ internal/cmd/coinset/push_offer.go | 94 ++++++++++++++++++++ internal/cmd/coinset/reorg_pretty.go | 99 ++++++++++++++++++++++ internal/cmd/coinset/transaction_pretty.go | 27 +----- 6 files changed, 356 insertions(+), 29 deletions(-) create mode 100644 internal/cmd/coinset/get_reorgs.go create mode 100644 internal/cmd/coinset/pretty_common.go create mode 100644 internal/cmd/coinset/push_offer.go create mode 100644 internal/cmd/coinset/reorg_pretty.go diff --git a/internal/cmd/coinset/events.go b/internal/cmd/coinset/events.go index 04a0228..96165c1 100644 --- a/internal/cmd/coinset/events.go +++ b/internal/cmd/coinset/events.go @@ -15,9 +15,14 @@ import ( ) var ( - eventsFilter string - eventsTxIDs []string - eventsTxStatus string + eventsFilter string + eventsTxIDs []string + eventsTxStatus string + eventsIncoming bool + eventsOutgoing bool + eventsOfferIDs []string + eventsOfferStatus string + eventsP2 []string ) var eventsCmd = &cobra.Command{ @@ -51,6 +56,18 @@ If no type is specified, all events will be displayed.`, if err := validateTxStatusFilter(eventsTxStatus); err != nil { return err } + if err := validateOfferStatusFilter(eventsOfferStatus); err != nil { + return err + } + for _, p2 := range eventsP2 { + trimmed := strings.TrimSpace(p2) + if trimmed == "" { + continue + } + if _, err := convertAddressOrPuzzleHash(trimmed); err != nil { + return err + } + } return nil }, @@ -61,6 +78,11 @@ func init() { eventsCmd.Flags().StringVar(&eventsFilter, "events", "", "Comma-separated event types to subscribe to") eventsCmd.Flags().StringArrayVar(&eventsTxIDs, "tx-id", []string{}, "Transaction id filter (repeatable)") eventsCmd.Flags().StringVar(&eventsTxStatus, "tx-status", "", "Comma-separated tx statuses: pending,confirmed,removed") + eventsCmd.Flags().BoolVar(&eventsIncoming, "incoming", false, "Filter transaction events to incoming flow (optionally combined with --p2)") + eventsCmd.Flags().BoolVar(&eventsOutgoing, "outgoing", false, "Filter transaction events to outgoing flow (optionally combined with --p2)") + eventsCmd.Flags().StringArrayVar(&eventsOfferIDs, "offer-id", []string{}, "Offer id filter (repeatable)") + eventsCmd.Flags().StringVar(&eventsOfferStatus, "offer-status", "", "Comma-separated offer lifecycle statuses") + eventsCmd.Flags().StringArrayVar(&eventsP2, "p2", []string{}, "Puzzle hash or address filter for transaction/offer participants (repeatable)") rootCmd.AddCommand(eventsCmd) } @@ -122,6 +144,33 @@ func validateTxStatusFilter(value string) error { return nil } +func isValidWSOfferStatus(status string) bool { + if isValidOfferStatus(status) { + return true + } + + // WebSocket streams expose parse failures even though indexed offer queries do not. + return status == "parse_failed" +} + +func validateOfferStatusFilter(value string) error { + if value == "" { + return nil + } + + for _, item := range strings.Split(value, ",") { + status := strings.TrimSpace(item) + if status == "" { + continue + } + if !isValidWSOfferStatus(status) { + return fmt.Errorf("invalid offer status in --offer-status: %s", status) + } + } + + return nil +} + func websocketEndpoint(singleEventType string) (string, error) { baseUrl, err := url.Parse(apiRoot()) if err != nil { @@ -169,6 +218,35 @@ func websocketEndpoint(singleEventType string) (string, error) { queryValues.Set("tx_status", eventsTxStatus) } + if eventsIncoming && !eventsOutgoing { + queryValues.Set("tx_direction", "incoming") + } else if eventsOutgoing && !eventsIncoming { + queryValues.Set("tx_direction", "outgoing") + } + + for _, offerID := range eventsOfferIDs { + trimmed := strings.TrimSpace(offerID) + if trimmed != "" { + queryValues.Add("offer_id", trimmed) + } + } + + if eventsOfferStatus != "" { + queryValues.Set("offer_status", eventsOfferStatus) + } + + for _, p2 := range eventsP2 { + trimmed := strings.TrimSpace(p2) + if trimmed == "" { + continue + } + normalized, err := convertAddressOrPuzzleHash(trimmed) + if err != nil { + return "", err + } + queryValues.Add("p2", normalized) + } + wsURL.RawQuery = queryValues.Encode() return wsURL.String(), nil } diff --git a/internal/cmd/coinset/get_reorgs.go b/internal/cmd/coinset/get_reorgs.go new file mode 100644 index 0000000..d8b2d5d --- /dev/null +++ b/internal/cmd/coinset/get_reorgs.go @@ -0,0 +1,47 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var ( + getReorgsLimit int + getReorgsCursor string + getReorgsPretty bool +) + +func init() { + getReorgsCmd.Flags().IntVar(&getReorgsLimit, "limit", 0, "Maximum number of reorg events to return (1-200)") + getReorgsCmd.Flags().StringVar(&getReorgsCursor, "cursor", "", "Pagination cursor from a previous truncated response") + getReorgsCmd.Flags().BoolVarP(&getReorgsPretty, "pretty", "p", false, "display formatted reorg output") + rootCmd.AddCommand(getReorgsCmd) +} + +var getReorgsCmd = &cobra.Command{ + Use: "get_reorgs", + Short: "List recent chain reorg events", + Long: "Returns persisted chain reorg events in reverse chronological order.", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.NoArgs(cmd, args); err != nil { + return err + } + if getReorgsLimit != 0 && (getReorgsLimit < 1 || getReorgsLimit > 200) { + return fmt.Errorf("limit must be between 1 and 200") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + jsonData := map[string]interface{}{} + + if getReorgsLimit != 0 { + jsonData["limit"] = getReorgsLimit + } + if getReorgsCursor != "" { + jsonData["cursor"] = getReorgsCursor + } + + makeReorgRequest("get_reorgs", jsonData, getReorgsPretty) + }, +} diff --git a/internal/cmd/coinset/pretty_common.go b/internal/cmd/coinset/pretty_common.go new file mode 100644 index 0000000..9bd8005 --- /dev/null +++ b/internal/cmd/coinset/pretty_common.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "encoding/json" + "log" +) + +func makePrettyRequest(path string, jsonData map[string]interface{}, printPretty func([]byte) error) { + if jq != "." || raw || describe { + log.Fatal("--pretty cannot be combined with --query, --raw, or --describe") + } + + jsonResponse, err := doRpc(path, jsonData) + if err != nil { + log.Fatal(err.Error()) + } + + if err := printPretty(jsonResponse); err != nil { + log.Printf("pretty format unavailable (%v), falling back to JSON output", err) + printJson(jsonResponse) + } +} + +func responseSucceeded(envelope map[string]json.RawMessage) bool { + rawSuccess, ok := envelope["success"] + if !ok { + return true + } + var success bool + if err := json.Unmarshal(rawSuccess, &success); err != nil { + return true + } + return success +} diff --git a/internal/cmd/coinset/push_offer.go b/internal/cmd/coinset/push_offer.go new file mode 100644 index 0000000..c3d3499 --- /dev/null +++ b/internal/cmd/coinset/push_offer.go @@ -0,0 +1,94 @@ +package cmd + +import ( + "fmt" + "log" + "os" + "regexp" + "strings" + + "github.com/spf13/cobra" +) + +var offerStringPattern = regexp.MustCompile(`^offer1[0-9a-z]+$`) + +func init() { + pushOfferCmd.Flags().StringP("file", "f", "", "Path to file containing the offer string") + rootCmd.AddCommand(pushOfferCmd) +} + +var pushOfferCmd = &cobra.Command{ + Use: "push_offer [offer]", + Short: "Push an offer (ingest + broadcast)", + Long: `Push an offer to Coinset for indexing and broadcast. The offer can be provided in three ways: +1. As a direct argument: push_offer offer1... +2. As a file path argument: push_offer ./my_offer.txt +3. Using the -f flag: push_offer -f ./my_offer.txt`, +} + +func init() { + var offer string + + pushOfferCmd.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 offer either as argument or with -f flag") + } + + if len(args) > 1 { + return fmt.Errorf("too many arguments provided") + } + + var rawOffer string + + if fileFlag != "" { + data, err := os.ReadFile(fileFlag) + if err != nil { + return fmt.Errorf("unable to read file %s: %v", fileFlag, err) + } + rawOffer = string(data) + } else if len(args) == 1 { + trimmedArg := strings.TrimSpace(args[0]) + if offerStringPattern.MatchString(trimmedArg) { + rawOffer = trimmedArg + } 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) + } + rawOffer = string(data) + } else { + rawOffer = args[0] + } + } + + offer = strings.TrimSpace(rawOffer) + if offer == "" { + return fmt.Errorf("offer must not be empty") + } + if !offerStringPattern.MatchString(offer) { + return fmt.Errorf("invalid offer string: must be a bech32 offer starting with offer1") + } + + return nil + } + + pushOfferCmd.Run = func(cmd *cobra.Command, args []string) { + if inspect { + log.Fatal("--inspect not supported for push_offer") + } + + jsonResponse, err := doRpc("push_offer", map[string]interface{}{ + "offer": offer, + }) + if err != nil { + log.Fatal(err.Error()) + } + printJson(jsonResponse) + } +} diff --git a/internal/cmd/coinset/reorg_pretty.go b/internal/cmd/coinset/reorg_pretty.go new file mode 100644 index 0000000..4ebf7e5 --- /dev/null +++ b/internal/cmd/coinset/reorg_pretty.go @@ -0,0 +1,99 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "strings" +) + +type reorgEvent struct { + ID string `json:"id"` + DetectedAtMS int64 `json:"detected_at_ms"` + OldPeakHeight int64 `json:"old_peak_height"` + OldPeakHash string `json:"old_peak_hash"` + NewPeakHeight int64 `json:"new_peak_height"` + NewPeakHash string `json:"new_peak_hash"` + ReorgDepth int64 `json:"reorg_depth"` +} + +type getReorgsEnvelope struct { + Reorgs []reorgEvent `json:"reorgs"` + Truncated *bool `json:"truncated"` + NextCursor string `json:"next_cursor"` + Success bool `json:"success"` +} + +func makeReorgRequest(path string, jsonData map[string]interface{}, pretty bool) { + if !pretty { + makeRequest(path, jsonData) + return + } + + makePrettyRequest(path, jsonData, printPrettyReorgResponse) +} + +func printPrettyReorgResponse(jsonBytes []byte) error { + var envelope map[string]json.RawMessage + if err := json.Unmarshal(jsonBytes, &envelope); err != nil { + return err + } + + if !responseSucceeded(envelope) { + printJson(jsonBytes) + return nil + } + + var payload getReorgsEnvelope + if err := json.Unmarshal(jsonBytes, &payload); err != nil { + return err + } + + printReorgList(payload, newANSIStyler()) + return nil +} + +func printReorgList(payload getReorgsEnvelope, styler ansiStyler) { + if len(payload.Reorgs) == 0 { + fmt.Println("No reorgs.") + return + } + + for idx, reorg := range payload.Reorgs { + if idx > 0 { + fmt.Println() + } + fmt.Println(formatReorgRecord(reorg, styler)) + } + + if payload.Truncated != nil && *payload.Truncated { + fmt.Printf("\nMore results available. Next cursor: %s\n", payload.NextCursor) + } +} + +func formatReorgRecord(reorg reorgEvent, styler ansiStyler) string { + var out strings.Builder + + oldHeight := formatSignedStringWithCommas(fmt.Sprintf("%d", reorg.OldPeakHeight)) + newHeight := formatSignedStringWithCommas(fmt.Sprintf("%d", reorg.NewPeakHeight)) + depth := formatSignedStringWithCommas(fmt.Sprintf("%d", reorg.ReorgDepth)) + age := formatAge(reorg.DetectedAtMS) + + fmt.Fprintf( + &out, + "%s %s %s %s -> %s %s %s\n", + styler.bold("REORG"), + styler.dim("depth"), + depth, + oldHeight, + newHeight, + styler.dim("age"), + age, + ) + + out.WriteString(formatSection("Peaks", []string{ + "old " + shortenHexIdentifier(reorg.OldPeakHash, 10, 8), + "new " + shortenHexIdentifier(reorg.NewPeakHash, 10, 8), + }, styler)) + + return strings.TrimRight(out.String(), "\n") +} diff --git a/internal/cmd/coinset/transaction_pretty.go b/internal/cmd/coinset/transaction_pretty.go index 7b76949..630bf5b 100644 --- a/internal/cmd/coinset/transaction_pretty.go +++ b/internal/cmd/coinset/transaction_pretty.go @@ -4,7 +4,6 @@ import ( "encoding/hex" "encoding/json" "fmt" - "log" "math/big" "os" "regexp" @@ -71,19 +70,7 @@ func makeTransactionRequest(path string, jsonData map[string]interface{}, pretty return } - if jq != "." || raw || describe { - log.Fatal("--pretty cannot be combined with --query, --raw, or --describe") - } - - jsonResponse, err := doRpc(path, jsonData) - if err != nil { - log.Fatal(err.Error()) - } - - if err := printPrettyTransactionResponse(jsonResponse); err != nil { - log.Printf("pretty format unavailable (%v), falling back to JSON output", err) - printJson(jsonResponse) - } + makePrettyRequest(path, jsonData, printPrettyTransactionResponse) } func printPrettyTransactionResponse(jsonBytes []byte) error { @@ -133,18 +120,6 @@ func printPrettyTransactionResponse(jsonBytes []byte) error { return fmt.Errorf("unsupported transaction response shape") } -func responseSucceeded(envelope map[string]json.RawMessage) bool { - rawSuccess, ok := envelope["success"] - if !ok { - return true - } - var success bool - if err := json.Unmarshal(rawSuccess, &success); err != nil { - return true - } - return success -} - func printTransactionList(payload transactionListEnvelope, styler ansiStyler) { if len(payload.Transactions) == 0 { fmt.Println("No transactions.")