From cdc9b58288068b2ba46a20484390764d3dc44313 Mon Sep 17 00:00:00 2001 From: Cameron Cooper Date: Wed, 8 Jul 2026 17:19:36 -0500 Subject: [PATCH] Expand --inspect to more spend-carrying RPC shapes. Normalize coin_solution, mempool_items arrays, and block_spends_with_conditions so puzzle/solution endpoints can use --inspect without changing the Rust inspector. Co-authored-by: Cursor --- README.md | 2 + SKILL.md | 4 +- internal/cmd/coinset/inspect_test.go | 93 ++++++++++++++++++++++++++++ internal/cmd/coinset/util.go | 61 +++++++++++++++++- 4 files changed, 158 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f1a97de..e4a4173 100644 --- a/README.md +++ b/README.md @@ -89,8 +89,10 @@ coinset get_block_spends 5000000 --inspect `--inspect` is supported when the returned JSON contains one of these shapes: - `mempool_item` +- `mempool_items` (array or map; items inspected individually) - `spend_bundle` / `spend_bundle_bytes` - `coin_spends` / `block_spends` +- `coin_solution` (coin spend wrapper, e.g. `get_puzzle_and_solution*`) - `coin_spend` (single spend) Notes: diff --git a/SKILL.md b/SKILL.md index 2968bb4..aa01331 100644 --- a/SKILL.md +++ b/SKILL.md @@ -213,7 +213,9 @@ When `--inspect` is used, the output follows `coinset.inspect.v1`: - `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) +- `mempool_items` (map of tx_id -> mempool_item, or array of mempool_item; each is inspected individually) +- `coin_solution` (coin spend wrapper returned by `get_puzzle_and_solution*`) +- `block_spends_with_conditions` (Coinset: wrapper around `coin_spend` entries; extracted and inspected) If the output shape doesn't match, `--inspect` returns an error. Fall back to the command without `--inspect`. diff --git a/internal/cmd/coinset/inspect_test.go b/internal/cmd/coinset/inspect_test.go index 5df027d..b8afcbd 100644 --- a/internal/cmd/coinset/inspect_test.go +++ b/internal/cmd/coinset/inspect_test.go @@ -25,6 +25,99 @@ func TestInspectRpcOutputAddsPuzzleRecognition(t *testing.T) { if err != nil { t.Fatalf("inspectRpcOutput failed: %v", err) } + assertHasPuzzleRecognition(t, out) +} + +func TestInspectRpcOutputCoinSolutionWrapper(t *testing.T) { + input := map[string]any{ + "coin_solution": 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) + } + assertHasPuzzleRecognition(t, out) +} + +func TestInspectRpcOutputMempoolItemsArray(t *testing.T) { + coinSpend := 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", + } + mempoolItem := map[string]any{ + "spend_bundle": map[string]any{ + "coin_spends": []any{coinSpend}, + // Valid G2 point encoding for "infinity": 0xc0 followed by 95 zero bytes. + "aggregated_signature": "0xc0" + repeat("00", 95), + }, + "fee": 0, + } + input := map[string]any{ + "mempool_items": []any{mempoolItem}, + } + b, _ := json.Marshal(input) + out, err := inspectRpcOutput(b) + if err != nil { + t.Fatalf("inspectRpcOutput failed: %v", err) + } + + var arr []any + if err := json.Unmarshal(out, &arr); err != nil { + t.Fatalf("output not JSON array: %v", err) + } + if len(arr) != 1 { + t.Fatalf("expected 1 inspected item, got %d", len(arr)) + } + blob, err := json.Marshal(arr[0]) + if err != nil { + t.Fatalf("failed to re-marshal inspected item: %v", err) + } + assertHasPuzzleRecognition(t, blob) +} + +func TestInspectRpcOutputBlockSpendsWithConditions(t *testing.T) { + coinSpend := 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", + } + input := map[string]any{ + "block_spends_with_conditions": []any{ + map[string]any{ + "coin_spend": coinSpend, + "conditions": []any{}, + "time_received": 0, + }, + }, + } + b, _ := json.Marshal(input) + out, err := inspectRpcOutput(b) + if err != nil { + t.Fatalf("inspectRpcOutput failed: %v", err) + } + assertHasPuzzleRecognition(t, out) +} + +func assertHasPuzzleRecognition(t *testing.T, out []byte) { + t.Helper() var v map[string]any if err := json.Unmarshal(out, &v); err != nil { t.Fatalf("output not JSON: %v", err) diff --git a/internal/cmd/coinset/util.go b/internal/cmd/coinset/util.go index 286c9ff..3b6c272 100644 --- a/internal/cmd/coinset/util.go +++ b/internal/cmd/coinset/util.go @@ -164,8 +164,18 @@ func inspectRpcOutput(jsonBytes []byte) ([]byte, error) { 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 { + // Coin endpoints may wrap a CoinSpend as coin_solution. + if cs, ok := m["coin_solution"]; ok { + wrapped := map[string]interface{}{"coin_spend": cs} + wrappedBytes, err := json.Marshal(wrapped) + if err != nil { + return nil, fmt.Errorf("failed to re-marshal coin_solution: %w", err) + } + return coinsetffi.Inspect(wrappedBytes, false, false) + } + + // Many full node RPCs return mempool items as a map keyed by tx id. if items, ok := m["mempool_items"]; ok { if itemsMap, ok := items.(map[string]interface{}); ok { out := make(map[string]json.RawMessage, len(itemsMap)) @@ -183,6 +193,49 @@ func inspectRpcOutput(jsonBytes []byte) ([]byte, error) { } return json.Marshal(out) } + + // Some endpoints return mempool items as an array. Inspect each entry. + if itemsArr, ok := items.([]interface{}); ok { + out := make([]json.RawMessage, 0, len(itemsArr)) + for i, item := range itemsArr { + 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[%d]: %w", i, err) + } + inspected, err := coinsetffi.Inspect(wrappedBytes, false, false) + if err != nil { + return nil, fmt.Errorf("inspect mempool_item[%d]: %w", i, err) + } + out = append(out, json.RawMessage(inspected)) + } + return json.Marshal(out) + } + } + + // Block spends with conditions wrap each coin spend entry. + if items, ok := m["block_spends_with_conditions"]; ok { + itemsArr, ok := items.([]interface{}) + if !ok { + return nil, fmt.Errorf("block_spends_with_conditions must be an array") + } + spends := make([]interface{}, 0, len(itemsArr)) + for i, entry := range itemsArr { + entryMap, ok := entry.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("block_spends_with_conditions[%d] must be an object", i) + } + coinSpend, ok := entryMap["coin_spend"] + if !ok { + return nil, fmt.Errorf("block_spends_with_conditions[%d] missing coin_spend", i) + } + spends = append(spends, coinSpend) + } + spendsBytes, err := json.Marshal(spends) + if err != nil { + return nil, fmt.Errorf("failed to re-marshal block_spends_with_conditions: %w", err) + } + return coinsetffi.Inspect(spendsBytes, false, false) } } @@ -204,6 +257,9 @@ func looksInspectable(v interface{}) bool { if _, ok := x["mempool_items"]; ok { return true } + if _, ok := x["coin_solution"]; ok { + return true + } if _, ok := x["spend_bundle"]; ok { return true } @@ -216,6 +272,9 @@ func looksInspectable(v interface{}) bool { if _, ok := x["block_spends"]; ok { return true } + if _, ok := x["block_spends_with_conditions"]; ok { + return true + } if _, ok := x["coin_spend"]; ok { return true }