Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
93 changes: 93 additions & 0 deletions internal/cmd/coinset/inspect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
61 changes: 60 additions & 1 deletion internal/cmd/coinset/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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)
}
}

Expand All @@ -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
}
Expand All @@ -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
}
Expand Down
Loading