From d8d7018acaaa946bd149d28cde2885c86175fb7a Mon Sep 17 00:00:00 2001 From: hannahhoward Date: Thu, 18 Dec 2025 14:24:12 -0800 Subject: [PATCH] feat(message): add utils for extract UCanto messages --- cmd/message/extract.go | 81 +++++++++++++++++++++++++++++++++++ cmd/message/parse.go | 75 ++++++++++++++++++++++++++++++++ cmd/message/root.go | 10 +++++ cmd/root.go | 2 + pkg/ipldfmt/formatter.go | 7 ++- pkg/ucanfmt/message.go | 45 ++++++++++++++++++++ pkg/ucanfmt/receipt.go | 92 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 cmd/message/extract.go create mode 100644 cmd/message/parse.go create mode 100644 cmd/message/root.go create mode 100644 pkg/ucanfmt/message.go create mode 100644 pkg/ucanfmt/receipt.go diff --git a/cmd/message/extract.go b/cmd/message/extract.go new file mode 100644 index 0000000..ff47b00 --- /dev/null +++ b/cmd/message/extract.go @@ -0,0 +1,81 @@ +package message + +import ( + "bytes" + "fmt" + "io" + "os" + + logging "github.com/ipfs/go-log/v2" + "github.com/spf13/cobra" + "github.com/storacha/debugger/pkg/ipldfmt" + "github.com/storacha/debugger/pkg/ucanfmt" + "github.com/storacha/go-ucanto/core/car" + "github.com/storacha/go-ucanto/core/dag/blockstore" + "github.com/storacha/go-ucanto/core/message" +) + +var extractCmd = &cobra.Command{ + Use: "extract [car-file]", + Short: "Extract a message from a CAR.", + Long: "Extract a message that has been archived to a CAR. You can pipe directly to this command.", + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + logging.SetLogLevel("*", "info") + + var archive []byte + var err error + if len(args) > 0 { + archive, err = os.ReadFile(args[0]) + } else { + archive, err = io.ReadAll(cmd.InOrStdin()) + } + if err != nil { + panic(err) + } + + // Decode CAR file + roots, blocks, err := car.Decode(bytes.NewReader(archive)) + if err != nil { + panic(fmt.Errorf("decoding CAR: %w", err)) + } + if len(roots) != 1 { + panic(fmt.Errorf("unexpected number of roots: %d, expected: 1", len(roots))) + } + + // Create blockstore from blocks + bstore, err := blockstore.NewBlockReader(blockstore.WithBlocksIterator(blocks)) + if err != nil { + panic(fmt.Errorf("creating blockstore: %w", err)) + } + + // Create message from root and blockstore + msg, err := message.NewMessage(roots[0], bstore) + if err != nil { + panic(fmt.Errorf("creating message: %w", err)) + } + + jsonOutput, _ := cmd.Flags().GetBool("json") + if jsonOutput { + for b, err := range msg.Blocks() { + if err != nil { + panic(fmt.Errorf("iterating message blocks: %w", err)) + } + cmd.Printf("%s\n", b.Link()) + s, err := ipldfmt.FormatDagCBOR(b.Bytes()) + if err != nil { + panic(fmt.Errorf("formatting block %s: %w", b.Link(), err)) + } + cmd.Println(s) + cmd.Println("") + } + } else { + ucanfmt.PrintMessage(msg) + } + }, +} + +func init() { + extractCmd.Flags().Bool("json", false, "Output DAG JSON") + Cmd.AddCommand(extractCmd) +} diff --git a/cmd/message/parse.go b/cmd/message/parse.go new file mode 100644 index 0000000..38a7264 --- /dev/null +++ b/cmd/message/parse.go @@ -0,0 +1,75 @@ +package message + +import ( + "bytes" + "fmt" + + logging "github.com/ipfs/go-log/v2" + "github.com/multiformats/go-multibase" + "github.com/spf13/cobra" + "github.com/storacha/debugger/pkg/ipldfmt" + "github.com/storacha/debugger/pkg/ucanfmt" + "github.com/storacha/go-ucanto/core/car" + "github.com/storacha/go-ucanto/core/dag/blockstore" + "github.com/storacha/go-ucanto/core/message" +) + +var parseCmd = &cobra.Command{ + Use: "parse ", + Short: "Parse a message.", + Long: `Parse a multibase encoded CID, with an identity multihash that contains message data in a CAR file.`, + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + logging.SetLogLevel("*", "info") + + // Decode the multibase encoded value + _, data, err := multibase.Decode(args[0]) + if err != nil { + panic(fmt.Errorf("decoding multibase: %w", err)) + } + + // Decode CAR file + roots, blocks, err := car.Decode(bytes.NewReader(data)) + if err != nil { + panic(fmt.Errorf("decoding CAR: %w", err)) + } + if len(roots) != 1 { + panic(fmt.Errorf("unexpected number of roots: %d, expected: 1", len(roots))) + } + + // Create blockstore from blocks + bstore, err := blockstore.NewBlockReader(blockstore.WithBlocksIterator(blocks)) + if err != nil { + panic(fmt.Errorf("creating blockstore: %w", err)) + } + + // Create message from root and blockstore + msg, err := message.NewMessage(roots[0], bstore) + if err != nil { + panic(fmt.Errorf("creating message: %w", err)) + } + + jsonOutput, _ := cmd.Flags().GetBool("json") + if jsonOutput { + for b, err := range msg.Blocks() { + if err != nil { + panic(fmt.Errorf("iterating message blocks: %w", err)) + } + cmd.Printf("%s\n", b.Link()) + s, err := ipldfmt.FormatDagCBOR(b.Bytes()) + if err != nil { + panic(fmt.Errorf("formatting block %s: %w", b.Link(), err)) + } + cmd.Println(s) + cmd.Println("") + } + } else { + ucanfmt.PrintMessage(msg) + } + }, +} + +func init() { + parseCmd.Flags().Bool("json", false, "Output DAG JSON") + Cmd.AddCommand(parseCmd) +} diff --git a/cmd/message/root.go b/cmd/message/root.go new file mode 100644 index 0000000..ff22583 --- /dev/null +++ b/cmd/message/root.go @@ -0,0 +1,10 @@ +package message + +import ( + "github.com/spf13/cobra" +) + +var Cmd = &cobra.Command{ + Use: "message", + Short: "Tools for debugging UCAN messages", +} diff --git a/cmd/root.go b/cmd/root.go index 43eaa28..2991807 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -10,6 +10,7 @@ import ( "github.com/storacha/debugger/cmd/delegation" "github.com/storacha/debugger/cmd/flatfs" "github.com/storacha/debugger/cmd/ipni" + "github.com/storacha/debugger/cmd/message" "github.com/storacha/debugger/cmd/xagentmessage" ) @@ -41,5 +42,6 @@ func init() { rootCmd.AddCommand(delegation.Cmd) rootCmd.AddCommand(flatfs.Cmd) rootCmd.AddCommand(ipni.Cmd) + rootCmd.AddCommand(message.Cmd) rootCmd.AddCommand(xagentmessage.Cmd) } diff --git a/pkg/ipldfmt/formatter.go b/pkg/ipldfmt/formatter.go index 44f63a8..ff9721a 100644 --- a/pkg/ipldfmt/formatter.go +++ b/pkg/ipldfmt/formatter.go @@ -19,12 +19,17 @@ func FormatDagCBOR(buf []byte) (string, error) { if err != nil { return "", fmt.Errorf("decoding CBOR: %w", err) } + return FormatNode(n, "") +} + +// FormatNode formats an ipld-node to a dag-json encoded string. +func FormatNode(n ipld.Node, prefix string) (string, error) { jsonData, err := ipld.Encode(n, dagjson.Encode) if err != nil { return "", fmt.Errorf("encoding JSON: %w", err) } var indentedJSON bytes.Buffer - err = json.Indent(&indentedJSON, jsonData, "", " ") + err = json.Indent(&indentedJSON, jsonData, prefix, " ") if err != nil { return "", fmt.Errorf("indenting JSON: %w", err) } diff --git a/pkg/ucanfmt/message.go b/pkg/ucanfmt/message.go new file mode 100644 index 0000000..3154bc8 --- /dev/null +++ b/pkg/ucanfmt/message.go @@ -0,0 +1,45 @@ +package ucanfmt + +import ( + "fmt" + + "github.com/storacha/go-ucanto/core/message" +) + +func PrintMessage(m message.AgentMessage) { + fmt.Printf("%s\n", m.Root().Link()) + + invocations := m.Invocations() + if len(invocations) > 0 { + fmt.Println(" Invocations:") + for _, invLink := range invocations { + inv, ok, err := m.Invocation(invLink) + if err != nil { + fmt.Printf(" Error getting invocation %s: %v\n", invLink, err) + continue + } + if !ok { + fmt.Printf(" Invocation not found: %s\n", invLink) + continue + } + doPrintDelegation(inv, 1) + } + } + + receipts := m.Receipts() + if len(receipts) > 0 { + fmt.Println(" Receipts:") + for _, rcptLink := range receipts { + rcpt, ok, err := m.Receipt(rcptLink) + if err != nil { + fmt.Printf(" Error getting receipt %s: %v\n", rcptLink, err) + continue + } + if !ok { + fmt.Printf(" Receipt not found: %s\n", rcptLink) + continue + } + doPrintReceipt(rcpt, 1) + } + } +} diff --git a/pkg/ucanfmt/receipt.go b/pkg/ucanfmt/receipt.go new file mode 100644 index 0000000..2257708 --- /dev/null +++ b/pkg/ucanfmt/receipt.go @@ -0,0 +1,92 @@ +package ucanfmt + +import ( + "fmt" + + "github.com/storacha/debugger/pkg/ipldfmt" + "github.com/storacha/go-ucanto/core/dag/blockstore" + "github.com/storacha/go-ucanto/core/delegation" + "github.com/storacha/go-ucanto/core/ipld" + "github.com/storacha/go-ucanto/core/receipt" + "github.com/storacha/go-ucanto/core/result" +) + +func PrintReceipt(r receipt.AnyReceipt) { + doPrintReceipt(r, 0) +} + +func doPrintReceipt(r receipt.AnyReceipt, level int) { + log := withIndent(level) + + log("%s", r.Root().Link()) + + if r.Issuer() != nil { + log(" Issuer: %s", r.Issuer().DID()) + } + + log(" Ran: %s", r.Ran().Link()) + + // Print the result (Out) + out := r.Out() + result.MatchResultR0(out, func(ok ipld.Node) { + log(" Out: Ok") + jsonString, err := ipldfmt.FormatNode(ok, " ") + if err != nil { + log(" Error formatting JSON: %v", err) + } else { + log("%s", jsonString) + } + }, func(err ipld.Node) { + log(" Out: Error") + jsonString, jsonErr := ipldfmt.FormatNode(err, " ") + if jsonErr != nil { + log(" Error formatting JSON: %v", jsonErr) + } else { + log("%s", jsonString) + } + }) + + // Print effects + fx := r.Fx() + if len(fx.Fork()) > 0 { + log(" Effects:") + log(" Fork:") + for _, f := range fx.Fork() { + log(" %s", f.Link()) + } + } + + if fx.Join().Link() != nil { + if len(fx.Fork()) == 0 { + log(" Effects:") + } + log(" Join: %s", fx.Join().Link()) + } + + // Print metadata + meta := r.Meta() + if len(meta) > 0 { + log(" Meta:") + for k, v := range meta { + log(" %s: %v", k, v) + } + } + + bs, err := blockstore.NewBlockReader(blockstore.WithBlocksIterator(r.Blocks())) + if err != nil { + panic(fmt.Errorf("creating blockstore: %w", err)) + } + + // Print proofs recursively + if len(r.Proofs()) > 0 { + log(" Proofs:") + for _, p := range r.Proofs() { + pd, err := delegation.NewDelegationView(p.Link(), bs) + if err != nil { + log(" %s", p.Link()) + continue + } + doPrintDelegation(pd, level+2) + } + } +}