From 18c298d3094d8eb71a1c340749ed23f33bc4ebde Mon Sep 17 00:00:00 2001 From: Pelle Braendgaard Date: Sun, 1 Mar 2026 14:39:59 +0100 Subject: [PATCH 1/4] Add TAP CLI tool with message creation and receive commands Add a `tap` CLI binary at cmd/tap/ that wraps all go-didcomm CLI commands (did, pack, unpack, send) and adds TAP-specific functionality: - `tap message ` creates all 20 TAP message types with --from, --to, --thid, and --body flags, outputting DIDComm JSON to stdout - `tap receive` unpacks a DIDComm envelope and parses the TAP body into a typed result with body type, envelope metadata, and parsed fields Also removes the local replace directive for go-didcomm in favor of importing from GitHub, and updates README and CLAUDE.md with CLI docs. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 41 ++- README.md | 69 ++++ cmd/tap/main.go | 101 ++++++ cmd/tap/message.go | 480 ++++++++++++++++++++++++++ cmd/tap/message_test.go | 734 ++++++++++++++++++++++++++++++++++++++++ cmd/tap/receive.go | 81 +++++ cmd/tap/receive_test.go | 261 ++++++++++++++ go.mod | 2 +- go.sum | 4 +- 9 files changed, 1768 insertions(+), 5 deletions(-) create mode 100644 cmd/tap/main.go create mode 100644 cmd/tap/message.go create mode 100644 cmd/tap/message_test.go create mode 100644 cmd/tap/receive.go create mode 100644 cmd/tap/receive_test.go diff --git a/CLAUDE.md b/CLAUDE.md index 54bc5f7..7371286 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,9 +17,12 @@ go test -race -coverprofile=coverage.out ./... # Run a single test go test -run TestNewTransferMessage ./... -# Build +# Build library go build ./... +# Build CLI +go build ./cmd/tap/ + # Vet go vet ./... ``` @@ -30,7 +33,7 @@ This is a Go library that provides typed wrappers for all 20 TAP (Transaction Au ### Package structure -Single flat `tap` package — all types and helpers in the root. One file per message type. +Single flat `tap` package — all types and helpers in the root. One file per message type. CLI binary at `cmd/tap/`. ### Key types @@ -107,3 +110,37 @@ The `Agent.For` field uses `ForField` type, which handles JSON marshaling of bot - **CHANGELOG.md** — Maintain a `CHANGELOG.md` in the project root using [Keep a Changelog](https://keepachangelog.com/) format. Update it with every user-facing change (new features, bug fixes, breaking changes, dependency updates). Group entries under `Added`, `Changed`, `Fixed`, `Removed` sections within version headings. - **README.md** — Update `README.md` whenever changes affect public API, usage examples, installation instructions, or project capabilities. - **CLAUDE.md** — Update this file whenever changes affect architecture, file layout, commands, dependencies, or development guidelines (e.g., new message types added to the file layout table, new commands, changed patterns). + +## CLI (`cmd/tap/`) + +The `tap` binary wraps all go-didcomm CLI commands and adds TAP-specific `message` and `receive` commands. + +### CLI file layout + +| File | Purpose | +|------|---------| +| `cmd/tap/main.go` | Entry point, command routing, usage text | +| `cmd/tap/message.go` | `message ` command — creates all 20 TAP message types | +| `cmd/tap/receive.go` | `receive` command — unpacks DIDComm envelope + parses TAP body | +| `cmd/tap/message_test.go` | Tests for message creation (all types, validation, file input) | +| `cmd/tap/receive_test.go` | Tests for receive (signed, authcrypt, pipe workflow) | + +### CLI commands + +``` +tap did generate-key [--output-dir ] +tap did generate-web --domain [--path

] [--service-endpoint ] [--output-dir

] +tap pack signed --key-file [--send] [--did-doc ] [--message ] +tap pack anoncrypt [--send] [--did-doc ] [--message ] +tap pack authcrypt --key-file [--send] [--did-doc ] [--message ] +tap unpack --key-file [--did-doc ] [--message ] +tap send --to [--message ] +tap message --from --to [--thid ] [--body ] +tap receive --key-file [--did-doc ] [--message ] +``` + +The `did`, `pack`, `unpack`, and `send` commands delegate to `go-didcomm/cli` package. The `message` and `receive` commands are TAP-specific. + +### go-didcomm/cli package + +Shared CLI utilities are exported from `go-didcomm/cli/`. Both the `didcomm` and `tap` binaries import this package. Key exports: `ReadMessageInput`, `BuildClient`, `BuildResolverWithOverrides`, `LoadKeyFile`, `MarshalDIDDoc`, `MarshalKeyPair`, `DetectContentType`, `ParseMessage`, `RunDID`, `RunPack`, `RunUnpack`, `RunSend`. diff --git a/README.md b/README.md index 3abae79..51943db 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,18 @@ Go library for the [Transaction Authorization Protocol (TAP)](https://tap.rsvp) ## Installation +### Library + ```bash go get github.com/TransactionAuthorizationProtocol/tap-go ``` +### CLI + +```bash +go install github.com/TransactionAuthorizationProtocol/tap-go/cmd/tap@latest +``` + ## How It Works `tap-go` sits on top of [go-didcomm](https://github.com/Notabene-id/go-didcomm), which handles DIDComm v2 message packing (signing, encryption) and unpacking. This library adds TAP-specific typed bodies, validation, and parsing. @@ -275,6 +283,67 @@ body, err := tap.ParseBody(msg) // (TAPBody, error) body.TAPType() // e.g. "https://tap.rsvp/schema/1.0#Transfer" ``` +## CLI + +The `tap` CLI wraps all [go-didcomm](https://github.com/Notabene-id/go-didcomm) CLI commands and adds TAP-specific message creation and receiving. + +### Commands + +``` +tap did generate-key [--output-dir ] +tap did generate-web --domain [--path

] [--service-endpoint ] [--output-dir

] +tap pack signed --key-file [--send] [--did-doc ] [--message ] +tap pack anoncrypt [--send] [--did-doc ] [--message ] +tap pack authcrypt --key-file [--send] [--did-doc ] [--message ] +tap unpack --key-file [--did-doc ] [--message ] +tap send --to [--message ] +tap message --from --to [--thid ] [--body ] +tap receive --key-file [--did-doc ] [--message ] +``` + +### Create and pack a TAP message + +```bash +# Generate identities +tap did generate-key --output-dir alice +tap did generate-key --output-dir bob + +ALICE=$(jq -r .id alice/did-doc.json) +BOB=$(jq -r .id bob/did-doc.json) + +# Create a TAP transfer message +tap message transfer --from $ALICE --to $BOB \ + --body '{"asset":"eip155:1/slip44:60","amount":"1.5","agents":[{"@id":"'$ALICE'","role":"OriginatingVASP"}]}' + +# Pipe: create → pack → send +tap message transfer --from $ALICE --to $BOB --body @body.json | \ + tap pack authcrypt --key-file alice/keys.json +``` + +### Receive a TAP message + +```bash +# Unpack a DIDComm envelope and parse the TAP body +echo '' | tap receive --key-file bob/keys.json +``` + +The `receive` command outputs JSON with the unpacked message, typed body, and envelope metadata (`encrypted`, `signed`, `anonymous`). + +### TAP message types + +**Initiating (no `--thid`):** `transfer`, `payment`, `exchange`, `escrow`, `connect` + +**Reply (require `--thid`):** `authorize`, `authorization-required`, `settle`, `reject`, `cancel`, `revert`, `capture`, `quote`, `add-agents`, `remove-agent`, `replace-agent`, `update-agent`, `update-party`, `update-policies`, `confirm-relationship` + +### Body input (`--body` flag) + +- `'{"json"}'` — inline JSON string +- `@file.json` — read from file +- `-` — read from stdin +- _(omitted)_ — defaults to `{}` + +The body JSON should contain only message-specific fields (e.g. `asset`, `amount`, `agents`). The CLI automatically sets `@context` and `@type`. + ## Error Handling ```go diff --git a/cmd/tap/main.go b/cmd/tap/main.go new file mode 100644 index 0000000..e843b33 --- /dev/null +++ b/cmd/tap/main.go @@ -0,0 +1,101 @@ +package main + +import ( + "fmt" + "os" + + "github.com/Notabene-id/go-didcomm/cli" +) + +const version = "0.1.0" + +const usage = `tap - TAP (Transaction Authorization Protocol) CLI + +Usage: + tap [options] + +Commands: + did generate-key Generate a did:key identity + did generate-web --domain [--path

] Generate a did:web identity + pack signed --key-file [--send] [--did-doc ] Sign a message (JWS) + pack anoncrypt [--send] [--did-doc ] [--message ] Anonymous encrypt (JWE) + pack authcrypt --key-file [--send] [--did-doc ] Sign-then-encrypt + unpack --key-file [--did-doc ] Unpack a message + send --to [--message ] HTTP POST pre-packed message + message --from --to [flags] Create a TAP message + receive --key-file [--did-doc ] Unpack + parse TAP body + version Print version + help Print this help + +TAP message types: + Initiating: transfer, payment, exchange, escrow, connect + Reply: authorize, authorization-required, settle, reject, cancel, + revert, capture, quote, add-agents, remove-agent, + replace-agent, update-agent, update-party, update-policies, + confirm-relationship + +Message flags: + --from Sender DID (required) + --to Recipient DID(s), comma-separated (required) + --thid Thread ID (required for reply messages) + --body Body JSON: inline, @file.json, or - for stdin (default: {}) + +Message input (--message flag): + - Read from stdin (default) + @file.json Read from file + '{"json"}' Inline JSON string + +Examples: + # Generate identities + tap did generate-key --output-dir alice + tap did generate-key --output-dir bob + + # Create a TAP transfer message + ALICE=$(jq -r .id alice/did-doc.json) + BOB=$(jq -r .id bob/did-doc.json) + tap message transfer --from $ALICE --to $BOB \ + --body '{"asset":"eip155:1/slip44:60","amount":"1.5","agents":[{"@id":"'$ALICE'","role":"OriginatingVASP"}]}' + + # Pipe: create message → pack → send + tap message transfer --from $ALICE --to $BOB --body @body.json | \ + tap pack authcrypt --key-file alice/keys.json + + # Receive: unpack + parse TAP body + echo '' | tap receive --key-file bob/keys.json +` + +func main() { + if len(os.Args) < 2 { + fmt.Fprint(os.Stderr, usage) + os.Exit(1) + } + + var err error + switch os.Args[1] { + case "did": + err = cli.RunDID(os.Args[2:]) + case "pack": + err = cli.RunPack(os.Args[2:]) + case "unpack": + err = cli.RunUnpack(os.Args[2:]) + case "send": + err = cli.RunSend(os.Args[2:]) + case "message": + err = runMessage(os.Args[2:]) + case "receive": + err = runReceive(os.Args[2:]) + case "version": + fmt.Println("tap " + version) + case "help", "--help", "-h": + fmt.Print(usage) + default: + fmt.Fprintln(os.Stderr, "unknown command: "+os.Args[1]+"\n") //nolint:gosec // CLI stderr output + fmt.Fprint(os.Stderr, usage) + os.Exit(1) + } + + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} diff --git a/cmd/tap/message.go b/cmd/tap/message.go new file mode 100644 index 0000000..4462e5e --- /dev/null +++ b/cmd/tap/message.go @@ -0,0 +1,480 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "strings" + + didcomm "github.com/Notabene-id/go-didcomm" + "github.com/Notabene-id/go-didcomm/cli" + + tap "github.com/TransactionAuthorizationProtocol/tap-go" +) + +const messageUsage = `Usage: tap message --from --to [--thid ] [--body ] + +Initiating types (no --thid): + transfer, payment, exchange, escrow, connect + +Reply types (require --thid): + authorize, authorization-required, settle, reject, cancel, revert, + capture, quote, add-agents, remove-agent, replace-agent, update-agent, + update-party, update-policies, confirm-relationship + +Body input (--body flag): + '{...}' Inline JSON + @file.json Read from file + - Read from stdin + (omitted) Defaults to {} +` + +// messageFlags holds the common flags for all message subcommands. +type messageFlags struct { + from string + to []string + thid string + body []byte +} + +func parseMessageFlags(name string, args []string, requireThid bool) (*messageFlags, error) { + fs := flag.NewFlagSet("message "+name, flag.ContinueOnError) + from := fs.String("from", "", "sender DID (required)") + to := fs.String("to", "", "recipient DID(s), comma-separated (required)") + thid := fs.String("thid", "", "thread ID (required for reply messages)") + bodyFlag := fs.String("body", "", "body JSON: inline, @file.json, or - for stdin") + if err := fs.Parse(args); err != nil { + return nil, err + } + + if *from == "" { + return nil, fmt.Errorf("--from is required") + } + if *to == "" { + return nil, fmt.Errorf("--to is required") + } + if requireThid && *thid == "" { + return nil, fmt.Errorf("--thid is required for %s messages", name) + } + + // Parse recipient list + var recipients []string + for _, r := range strings.Split(*to, ",") { + r = strings.TrimSpace(r) + if r != "" { + recipients = append(recipients, r) + } + } + + // Parse body + var body []byte + if *bodyFlag == "" { + body = []byte("{}") + } else { + var err error + body, err = cli.ReadMessageInput(*bodyFlag) + if err != nil { + return nil, fmt.Errorf("read body: %w", err) + } + } + + return &messageFlags{ + from: *from, + to: recipients, + thid: *thid, + body: body, + }, nil +} + +func writeMessage(msg *didcomm.Message) error { + data, err := json.MarshalIndent(msg, "", " ") + if err != nil { + return fmt.Errorf("marshal message: %w", err) + } + _, err = os.Stdout.Write(data) + return err +} + +func runMessage(args []string) error { + if len(args) == 0 { + fmt.Fprint(os.Stderr, messageUsage) + return fmt.Errorf("message type required") + } + + msgType := args[0] + msgArgs := args[1:] + + switch msgType { + // Initiating messages (no thid) + case "transfer": + return runMessageTransfer(msgArgs) + case "payment": + return runMessagePayment(msgArgs) + case "exchange": + return runMessageExchange(msgArgs) + case "escrow": + return runMessageEscrow(msgArgs) + case "connect": + return runMessageConnect(msgArgs) + + // Reply messages (require thid) + case "authorize": + return runMessageAuthorize(msgArgs) + case "authorization-required": + return runMessageAuthorizationRequired(msgArgs) + case "settle": + return runMessageSettle(msgArgs) + case "reject": + return runMessageReject(msgArgs) + case "cancel": + return runMessageCancel(msgArgs) + case "revert": + return runMessageRevert(msgArgs) + case "capture": + return runMessageCapture(msgArgs) + case "quote": + return runMessageQuote(msgArgs) + case "add-agents": + return runMessageAddAgents(msgArgs) + case "remove-agent": + return runMessageRemoveAgent(msgArgs) + case "replace-agent": + return runMessageReplaceAgent(msgArgs) + case "update-agent": + return runMessageUpdateAgent(msgArgs) + case "update-party": + return runMessageUpdateParty(msgArgs) + case "update-policies": + return runMessageUpdatePolicies(msgArgs) + case "confirm-relationship": + return runMessageConfirmRelationship(msgArgs) + + default: + return fmt.Errorf("unknown message type: %s", msgType) + } +} + +// --- Initiating messages --- + +func runMessageTransfer(args []string) error { + f, err := parseMessageFlags("transfer", args, false) + if err != nil { + return err + } + var body tap.TransferBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewTransferMessage(f.from, f.to, &body) + if err != nil { + return err + } + return writeMessage(msg) +} + +func runMessagePayment(args []string) error { + f, err := parseMessageFlags("payment", args, false) + if err != nil { + return err + } + var body tap.PaymentBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewPaymentMessage(f.from, f.to, &body) + if err != nil { + return err + } + return writeMessage(msg) +} + +func runMessageExchange(args []string) error { + f, err := parseMessageFlags("exchange", args, false) + if err != nil { + return err + } + var body tap.ExchangeBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewExchangeMessage(f.from, f.to, &body) + if err != nil { + return err + } + return writeMessage(msg) +} + +func runMessageEscrow(args []string) error { + f, err := parseMessageFlags("escrow", args, false) + if err != nil { + return err + } + var body tap.EscrowBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewEscrowMessage(f.from, f.to, &body) + if err != nil { + return err + } + return writeMessage(msg) +} + +func runMessageConnect(args []string) error { + f, err := parseMessageFlags("connect", args, false) + if err != nil { + return err + } + var body tap.ConnectBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewConnectMessage(f.from, f.to, &body) + if err != nil { + return err + } + return writeMessage(msg) +} + +// --- Reply messages --- + +func runMessageAuthorize(args []string) error { + f, err := parseMessageFlags("authorize", args, true) + if err != nil { + return err + } + var body tap.AuthorizeBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewAuthorizeMessage(f.from, f.to, f.thid, &body) + if err != nil { + return err + } + return writeMessage(msg) +} + +func runMessageAuthorizationRequired(args []string) error { + f, err := parseMessageFlags("authorization-required", args, true) + if err != nil { + return err + } + var body tap.AuthorizationRequiredBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewAuthorizationRequiredMessage(f.from, f.to, f.thid, &body) + if err != nil { + return err + } + return writeMessage(msg) +} + +func runMessageSettle(args []string) error { + f, err := parseMessageFlags("settle", args, true) + if err != nil { + return err + } + var body tap.SettleBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewSettleMessage(f.from, f.to, f.thid, &body) + if err != nil { + return err + } + return writeMessage(msg) +} + +func runMessageReject(args []string) error { + f, err := parseMessageFlags("reject", args, true) + if err != nil { + return err + } + var body tap.RejectBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewRejectMessage(f.from, f.to, f.thid, &body) + if err != nil { + return err + } + return writeMessage(msg) +} + +func runMessageCancel(args []string) error { + f, err := parseMessageFlags("cancel", args, true) + if err != nil { + return err + } + var body tap.CancelBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewCancelMessage(f.from, f.to, f.thid, &body) + if err != nil { + return err + } + return writeMessage(msg) +} + +func runMessageRevert(args []string) error { + f, err := parseMessageFlags("revert", args, true) + if err != nil { + return err + } + var body tap.RevertBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewRevertMessage(f.from, f.to, f.thid, &body) + if err != nil { + return err + } + return writeMessage(msg) +} + +func runMessageCapture(args []string) error { + f, err := parseMessageFlags("capture", args, true) + if err != nil { + return err + } + var body tap.CaptureBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewCaptureMessage(f.from, f.to, f.thid, &body) + if err != nil { + return err + } + return writeMessage(msg) +} + +func runMessageQuote(args []string) error { + f, err := parseMessageFlags("quote", args, true) + if err != nil { + return err + } + var body tap.QuoteBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewQuoteMessage(f.from, f.to, f.thid, &body) + if err != nil { + return err + } + return writeMessage(msg) +} + +func runMessageAddAgents(args []string) error { + f, err := parseMessageFlags("add-agents", args, true) + if err != nil { + return err + } + var body tap.AddAgentsBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewAddAgentsMessage(f.from, f.to, f.thid, &body) + if err != nil { + return err + } + return writeMessage(msg) +} + +func runMessageRemoveAgent(args []string) error { + f, err := parseMessageFlags("remove-agent", args, true) + if err != nil { + return err + } + var body tap.RemoveAgentBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewRemoveAgentMessage(f.from, f.to, f.thid, &body) + if err != nil { + return err + } + return writeMessage(msg) +} + +func runMessageReplaceAgent(args []string) error { + f, err := parseMessageFlags("replace-agent", args, true) + if err != nil { + return err + } + var body tap.ReplaceAgentBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewReplaceAgentMessage(f.from, f.to, f.thid, &body) + if err != nil { + return err + } + return writeMessage(msg) +} + +func runMessageUpdateAgent(args []string) error { + f, err := parseMessageFlags("update-agent", args, true) + if err != nil { + return err + } + var body tap.UpdateAgentBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewUpdateAgentMessage(f.from, f.to, f.thid, &body) + if err != nil { + return err + } + return writeMessage(msg) +} + +func runMessageUpdateParty(args []string) error { + f, err := parseMessageFlags("update-party", args, true) + if err != nil { + return err + } + var body tap.UpdatePartyBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewUpdatePartyMessage(f.from, f.to, f.thid, &body) + if err != nil { + return err + } + return writeMessage(msg) +} + +func runMessageUpdatePolicies(args []string) error { + f, err := parseMessageFlags("update-policies", args, true) + if err != nil { + return err + } + var body tap.UpdatePoliciesBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewUpdatePoliciesMessage(f.from, f.to, f.thid, &body) + if err != nil { + return err + } + return writeMessage(msg) +} + +func runMessageConfirmRelationship(args []string) error { + f, err := parseMessageFlags("confirm-relationship", args, true) + if err != nil { + return err + } + var body tap.ConfirmRelationshipBody + if err := json.Unmarshal(f.body, &body); err != nil { + return fmt.Errorf("parse body JSON: %w", err) + } + msg, err := tap.NewConfirmRelationshipMessage(f.from, f.to, f.thid, &body) + if err != nil { + return err + } + return writeMessage(msg) +} diff --git a/cmd/tap/message_test.go b/cmd/tap/message_test.go new file mode 100644 index 0000000..57920d6 --- /dev/null +++ b/cmd/tap/message_test.go @@ -0,0 +1,734 @@ +package main + +import ( + "encoding/json" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + didcomm "github.com/Notabene-id/go-didcomm" + tap "github.com/TransactionAuthorizationProtocol/tap-go" +) + +func buildBinary(t *testing.T) string { + t.Helper() + binary := filepath.Join(t.TempDir(), "tap") + cmd := exec.Command("go", "build", "-o", binary, ".") + cmd.Dir = "." + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("build failed: %s\n%s", err, out) + } + return binary +} + +func TestCLI_Help(t *testing.T) { + bin := buildBinary(t) + cmd := exec.Command(bin, "help") + out, err := cmd.Output() + if err != nil { + t.Fatalf("help failed: %s", err) + } + if !strings.Contains(string(out), "tap") { + t.Fatal("help output missing 'tap'") + } + if !strings.Contains(string(out), "message") { + t.Fatal("help output missing 'message'") + } + if !strings.Contains(string(out), "receive") { + t.Fatal("help output missing 'receive'") + } +} + +func TestCLI_Version(t *testing.T) { + bin := buildBinary(t) + cmd := exec.Command(bin, "version") + out, err := cmd.Output() + if err != nil { + t.Fatalf("version failed: %s", err) + } + if !strings.Contains(string(out), "tap") { + t.Fatal("version output missing 'tap'") + } +} + +func TestCLI_UnknownCommand(t *testing.T) { + bin := buildBinary(t) + cmd := exec.Command(bin, "foobar") + out, err := cmd.CombinedOutput() + if err == nil { + t.Fatal("expected error for unknown command") + } + if !strings.Contains(string(out), "unknown command") { + t.Fatalf("expected 'unknown command' error, got: %s", out) + } +} + +func TestCLI_NoArgs(t *testing.T) { + bin := buildBinary(t) + cmd := exec.Command(bin) + err := cmd.Run() + if err == nil { + t.Fatal("expected error with no args") + } +} + +func TestCLI_MessageNoType(t *testing.T) { + bin := buildBinary(t) + cmd := exec.Command(bin, "message") + out, err := cmd.CombinedOutput() + if err == nil { + t.Fatal("expected error with no message type") + } + if !strings.Contains(string(out), "message type required") { + t.Fatalf("expected 'message type required' error, got: %s", out) + } +} + +func TestCLI_MessageUnknownType(t *testing.T) { + bin := buildBinary(t) + cmd := exec.Command(bin, "message", "foobar", "--from", "did:key:z1", "--to", "did:key:z2") + out, err := cmd.CombinedOutput() + if err == nil { + t.Fatal("expected error for unknown message type") + } + if !strings.Contains(string(out), "unknown message type") { + t.Fatalf("expected 'unknown message type' error, got: %s", out) + } +} + +func TestCLI_MessageMissingFrom(t *testing.T) { + bin := buildBinary(t) + cmd := exec.Command(bin, "message", "transfer", "--to", "did:key:z2") + out, err := cmd.CombinedOutput() + if err == nil { + t.Fatal("expected error for missing --from") + } + if !strings.Contains(string(out), "--from is required") { + t.Fatalf("expected '--from is required' error, got: %s", out) + } +} + +func TestCLI_MessageMissingTo(t *testing.T) { + bin := buildBinary(t) + cmd := exec.Command(bin, "message", "transfer", "--from", "did:key:z1") + out, err := cmd.CombinedOutput() + if err == nil { + t.Fatal("expected error for missing --to") + } + if !strings.Contains(string(out), "--to is required") { + t.Fatalf("expected '--to is required' error, got: %s", out) + } +} + +func TestCLI_MessageMissingThid(t *testing.T) { + bin := buildBinary(t) + cmd := exec.Command(bin, "message", "authorize", "--from", "did:key:z1", "--to", "did:key:z2") + out, err := cmd.CombinedOutput() + if err == nil { + t.Fatal("expected error for missing --thid") + } + if !strings.Contains(string(out), "--thid is required") { + t.Fatalf("expected '--thid is required' error, got: %s", out) + } +} + +func TestCLI_MessageTransfer(t *testing.T) { + bin := buildBinary(t) + body := `{"asset":"eip155:1/slip44:60","amount":"1.5","agents":[{"@id":"did:key:z1","role":"OriginatingVASP"}]}` + + cmd := exec.Command(bin, "message", "transfer", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--body", body, + ) + out, err := cmd.Output() + if err != nil { + ee, ok := err.(*exec.ExitError) + stderr := "" + if ok { + stderr = string(ee.Stderr) + } + t.Fatalf("message transfer failed: %s\n%s", err, stderr) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON output: %s\noutput: %s", err, out) + } + + if msg.Type != tap.TypeTransfer { + t.Fatalf("expected type %s, got %s", tap.TypeTransfer, msg.Type) + } + if msg.From != "did:key:z1" { + t.Fatalf("expected from did:key:z1, got %s", msg.From) + } + if len(msg.To) != 1 || msg.To[0] != "did:key:z2" { + t.Fatalf("expected to [did:key:z2], got %v", msg.To) + } + if msg.ID == "" { + t.Fatal("expected non-empty message ID") + } + + // Verify body has @context and @type + var bodyMap map[string]any + if err := json.Unmarshal(msg.Body, &bodyMap); err != nil { + t.Fatal(err) + } + if bodyMap["@context"] != tap.TAPContext { + t.Fatalf("expected @context %s, got %v", tap.TAPContext, bodyMap["@context"]) + } + if bodyMap["@type"] != tap.TypeTransfer { + t.Fatalf("expected @type %s, got %v", tap.TypeTransfer, bodyMap["@type"]) + } +} + +func TestCLI_MessageTransfer_FileBody(t *testing.T) { + bin := buildBinary(t) + dir := t.TempDir() + + bodyFile := filepath.Join(dir, "body.json") + body := `{"asset":"eip155:1/slip44:60","amount":"2.0","agents":[{"@id":"did:key:z1","role":"OriginatingVASP"}]}` + if err := os.WriteFile(bodyFile, []byte(body), 0o644); err != nil { + t.Fatal(err) + } + + cmd := exec.Command(bin, "message", "transfer", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--body", "@"+bodyFile, + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message transfer with file body failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON output: %s", err) + } + if msg.Type != tap.TypeTransfer { + t.Fatalf("expected type %s, got %s", tap.TypeTransfer, msg.Type) + } +} + +func TestCLI_MessagePayment(t *testing.T) { + bin := buildBinary(t) + body := `{"amount":"100","currency":"USD","merchant":{"@id":"did:key:z2"},"agents":[{"@id":"did:key:z1","role":"OriginatingVASP"}]}` + + cmd := exec.Command(bin, "message", "payment", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--body", body, + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message payment failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if msg.Type != tap.TypePayment { + t.Fatalf("expected type %s, got %s", tap.TypePayment, msg.Type) + } +} + +func TestCLI_MessageExchange(t *testing.T) { + bin := buildBinary(t) + body := `{"fromAssets":["eip155:1/slip44:60"],"toAssets":["eip155:1/slip44:0"],"fromAmount":"1.0","requester":{"@id":"did:key:z1"},"agents":[{"@id":"did:key:z1","role":"OriginatingVASP"}]}` + + cmd := exec.Command(bin, "message", "exchange", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--body", body, + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message exchange failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if msg.Type != tap.TypeExchange { + t.Fatalf("expected type %s, got %s", tap.TypeExchange, msg.Type) + } +} + +func TestCLI_MessageEscrow(t *testing.T) { + bin := buildBinary(t) + body := `{"asset":"eip155:1/slip44:60","amount":"5.0","originator":{"@id":"did:key:z1"},"beneficiary":{"@id":"did:key:z2"},"expiry":"2025-12-31T23:59:59Z","agents":[{"@id":"did:key:z1","role":"OriginatingVASP"}]}` + + cmd := exec.Command(bin, "message", "escrow", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--body", body, + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message escrow failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if msg.Type != tap.TypeEscrow { + t.Fatalf("expected type %s, got %s", tap.TypeEscrow, msg.Type) + } +} + +func TestCLI_MessageConnect(t *testing.T) { + bin := buildBinary(t) + body := `{"requester":{"@id":"did:key:z1"},"principal":{"@id":"did:key:z1"},"agents":[{"@id":"did:key:z1","role":"OriginatingVASP"}],"constraints":{"purposes":["travel"]}}` + + cmd := exec.Command(bin, "message", "connect", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--body", body, + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message connect failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if msg.Type != tap.TypeConnect { + t.Fatalf("expected type %s, got %s", tap.TypeConnect, msg.Type) + } +} + +func TestCLI_MessageAuthorize(t *testing.T) { + bin := buildBinary(t) + + cmd := exec.Command(bin, "message", "authorize", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--thid", "thread-123", + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message authorize failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if msg.Type != tap.TypeAuthorize { + t.Fatalf("expected type %s, got %s", tap.TypeAuthorize, msg.Type) + } + if msg.Thid != "thread-123" { + t.Fatalf("expected thid thread-123, got %s", msg.Thid) + } +} + +func TestCLI_MessageAuthorizationRequired(t *testing.T) { + bin := buildBinary(t) + body := `{"authorizationUrl":"https://example.com/auth","expires":"2025-12-31T23:59:59Z"}` + + cmd := exec.Command(bin, "message", "authorization-required", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--thid", "thread-123", + "--body", body, + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message authorization-required failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if msg.Type != tap.TypeAuthorizationRequired { + t.Fatalf("expected type %s, got %s", tap.TypeAuthorizationRequired, msg.Type) + } +} + +func TestCLI_MessageSettle(t *testing.T) { + bin := buildBinary(t) + body := `{"settlementAddress":"0x1234567890abcdef"}` + + cmd := exec.Command(bin, "message", "settle", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--thid", "thread-123", + "--body", body, + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message settle failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if msg.Type != tap.TypeSettle { + t.Fatalf("expected type %s, got %s", tap.TypeSettle, msg.Type) + } +} + +func TestCLI_MessageReject(t *testing.T) { + bin := buildBinary(t) + body := `{"reason":"compliance"}` + + cmd := exec.Command(bin, "message", "reject", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--thid", "thread-123", + "--body", body, + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message reject failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if msg.Type != tap.TypeReject { + t.Fatalf("expected type %s, got %s", tap.TypeReject, msg.Type) + } +} + +func TestCLI_MessageCancel(t *testing.T) { + bin := buildBinary(t) + body := `{"by":"did:key:z1"}` + + cmd := exec.Command(bin, "message", "cancel", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--thid", "thread-123", + "--body", body, + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message cancel failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if msg.Type != tap.TypeCancel { + t.Fatalf("expected type %s, got %s", tap.TypeCancel, msg.Type) + } +} + +func TestCLI_MessageRevert(t *testing.T) { + bin := buildBinary(t) + body := `{"settlementAddress":"0x1234","reason":"fraud"}` + + cmd := exec.Command(bin, "message", "revert", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--thid", "thread-123", + "--body", body, + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message revert failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if msg.Type != tap.TypeRevert { + t.Fatalf("expected type %s, got %s", tap.TypeRevert, msg.Type) + } +} + +func TestCLI_MessageCapture(t *testing.T) { + bin := buildBinary(t) + + cmd := exec.Command(bin, "message", "capture", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--thid", "thread-123", + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message capture failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if msg.Type != tap.TypeCapture { + t.Fatalf("expected type %s, got %s", tap.TypeCapture, msg.Type) + } +} + +func TestCLI_MessageQuote(t *testing.T) { + bin := buildBinary(t) + body := `{"fromAsset":"eip155:1/slip44:60","toAsset":"eip155:1/slip44:0","fromAmount":"1.0","toAmount":"2000.0","provider":{"@id":"did:key:z1"},"agents":[{"@id":"did:key:z1","role":"Provider"}],"expiresAt":"2025-12-31T23:59:59Z"}` + + cmd := exec.Command(bin, "message", "quote", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--thid", "thread-123", + "--body", body, + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message quote failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if msg.Type != tap.TypeQuote { + t.Fatalf("expected type %s, got %s", tap.TypeQuote, msg.Type) + } +} + +func TestCLI_MessageAddAgents(t *testing.T) { + bin := buildBinary(t) + body := `{"agents":[{"@id":"did:key:z3","role":"IntermediaryVASP"}]}` + + cmd := exec.Command(bin, "message", "add-agents", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--thid", "thread-123", + "--body", body, + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message add-agents failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if msg.Type != tap.TypeAddAgents { + t.Fatalf("expected type %s, got %s", tap.TypeAddAgents, msg.Type) + } +} + +func TestCLI_MessageRemoveAgent(t *testing.T) { + bin := buildBinary(t) + body := `{"agent":"did:key:z3"}` + + cmd := exec.Command(bin, "message", "remove-agent", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--thid", "thread-123", + "--body", body, + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message remove-agent failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if msg.Type != tap.TypeRemoveAgent { + t.Fatalf("expected type %s, got %s", tap.TypeRemoveAgent, msg.Type) + } +} + +func TestCLI_MessageReplaceAgent(t *testing.T) { + bin := buildBinary(t) + body := `{"original":"did:key:z3","replacement":{"@id":"did:key:z4","role":"IntermediaryVASP"}}` + + cmd := exec.Command(bin, "message", "replace-agent", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--thid", "thread-123", + "--body", body, + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message replace-agent failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if msg.Type != tap.TypeReplaceAgent { + t.Fatalf("expected type %s, got %s", tap.TypeReplaceAgent, msg.Type) + } +} + +func TestCLI_MessageUpdateAgent(t *testing.T) { + bin := buildBinary(t) + body := `{"agent":{"@id":"did:key:z3","role":"IntermediaryVASP"}}` + + cmd := exec.Command(bin, "message", "update-agent", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--thid", "thread-123", + "--body", body, + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message update-agent failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if msg.Type != tap.TypeUpdateAgent { + t.Fatalf("expected type %s, got %s", tap.TypeUpdateAgent, msg.Type) + } +} + +func TestCLI_MessageUpdateParty(t *testing.T) { + bin := buildBinary(t) + body := `{"party":{"@id":"did:key:z3","name":"Alice"},"role":"originator"}` + + cmd := exec.Command(bin, "message", "update-party", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--thid", "thread-123", + "--body", body, + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message update-party failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if msg.Type != tap.TypeUpdateParty { + t.Fatalf("expected type %s, got %s", tap.TypeUpdateParty, msg.Type) + } +} + +func TestCLI_MessageUpdatePolicies(t *testing.T) { + bin := buildBinary(t) + body := `{"policies":[{"@type":"RequirePresentation","purpose":"compliance"}]}` + + cmd := exec.Command(bin, "message", "update-policies", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--thid", "thread-123", + "--body", body, + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message update-policies failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if msg.Type != tap.TypeUpdatePolicies { + t.Fatalf("expected type %s, got %s", tap.TypeUpdatePolicies, msg.Type) + } +} + +func TestCLI_MessageConfirmRelationship(t *testing.T) { + bin := buildBinary(t) + body := `{"relationship":{"type":"customer","parties":[{"@id":"did:key:z1"},{"@id":"did:key:z2"}]},"status":"confirmed"}` + + cmd := exec.Command(bin, "message", "confirm-relationship", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--thid", "thread-123", + "--body", body, + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message confirm-relationship failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if msg.Type != tap.TypeConfirmRelationship { + t.Fatalf("expected type %s, got %s", tap.TypeConfirmRelationship, msg.Type) + } +} + +// TestCLI_MessageTransfer_MultipleRecipients tests comma-separated --to flag. +func TestCLI_MessageTransfer_MultipleRecipients(t *testing.T) { + bin := buildBinary(t) + body := `{"asset":"eip155:1/slip44:60","amount":"1.0","agents":[{"@id":"did:key:z1","role":"OriginatingVASP"}]}` + + cmd := exec.Command(bin, "message", "transfer", + "--from", "did:key:z1", + "--to", "did:key:z2,did:key:z3", + "--body", body, + ) + out, err := cmd.Output() + if err != nil { + t.Fatalf("message transfer with multiple recipients failed: %s", err) + } + + var msg didcomm.Message + if err := json.Unmarshal(out, &msg); err != nil { + t.Fatalf("invalid JSON: %s", err) + } + if len(msg.To) != 2 { + t.Fatalf("expected 2 recipients, got %d", len(msg.To)) + } + if msg.To[0] != "did:key:z2" || msg.To[1] != "did:key:z3" { + t.Fatalf("unexpected recipients: %v", msg.To) + } +} + +// TestCLI_DIDGenerateKey tests that DID commands work through the tap binary. +func TestCLI_DIDGenerateKey(t *testing.T) { + bin := buildBinary(t) + dir := filepath.Join(t.TempDir(), "out") + + cmd := exec.Command(bin, "did", "generate-key", "--output-dir", dir) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("did generate-key failed: %s\n%s", err, out) + } + + _, statErr := os.Stat(filepath.Join(dir, "did-doc.json")) + if statErr != nil { + t.Fatal("did-doc.json not created") + } + _, statErr = os.Stat(filepath.Join(dir, "keys.json")) + if statErr != nil { + t.Fatal("keys.json not created") + } +} + +// TestCLI_MessageTransfer_BodyValidation tests that body validation errors propagate. +func TestCLI_MessageTransfer_BodyValidation(t *testing.T) { + bin := buildBinary(t) + // Missing required "asset" field + body := `{"amount":"1.0","agents":[{"@id":"did:key:z1"}]}` + + cmd := exec.Command(bin, "message", "transfer", + "--from", "did:key:z1", + "--to", "did:key:z2", + "--body", body, + ) + out, err := cmd.CombinedOutput() + if err == nil { + t.Fatal("expected validation error for missing asset") + } + if !strings.Contains(string(out), "asset") { + t.Fatalf("expected asset validation error, got: %s", out) + } +} diff --git a/cmd/tap/receive.go b/cmd/tap/receive.go new file mode 100644 index 0000000..1dc8ec6 --- /dev/null +++ b/cmd/tap/receive.go @@ -0,0 +1,81 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + + "github.com/Notabene-id/go-didcomm/cli" + + tap "github.com/TransactionAuthorizationProtocol/tap-go" +) + +// receiveOutput is the JSON output format for the receive command. +type receiveOutput struct { + Message json.RawMessage `json:"message"` + Body json.RawMessage `json:"body"` + BodyType string `json:"bodyType"` + Encrypted bool `json:"encrypted"` + Signed bool `json:"signed"` + Anonymous bool `json:"anonymous"` +} + +func runReceive(args []string) error { + fs := flag.NewFlagSet("receive", flag.ContinueOnError) + keyFile := fs.String("key-file", "", "path to JWK Set file with private keys (required)") + didDoc := fs.String("did-doc", "", "comma-separated DID document file paths") + message := fs.String("message", "-", "message input: - (stdin), @file, or inline JSON") + if err := fs.Parse(args); err != nil { + return err + } + + if *keyFile == "" { + return fmt.Errorf("--key-file is required") + } + + dcClient, err := cli.BuildClient(*keyFile, *didDoc) + if err != nil { + return err + } + + tapClient := tap.NewClient(dcClient) + + data, err := cli.ReadMessageInput(*message) + if err != nil { + return fmt.Errorf("read message: %w", err) + } + + result, err := tapClient.Receive(context.Background(), data) + if err != nil { + return fmt.Errorf("receive: %w", err) + } + + msgBytes, err := json.Marshal(result.Message) + if err != nil { + return fmt.Errorf("marshal message: %w", err) + } + + bodyBytes, err := json.Marshal(result.Body) + if err != nil { + return fmt.Errorf("marshal body: %w", err) + } + + out := receiveOutput{ + Message: msgBytes, + Body: bodyBytes, + BodyType: result.Body.TAPType(), + Encrypted: result.Encrypted, + Signed: result.Signed, + Anonymous: result.Anonymous, + } + + outBytes, err := json.MarshalIndent(out, "", " ") + if err != nil { + return fmt.Errorf("marshal output: %w", err) + } + + _, err = fmt.Fprintln(os.Stdout, string(outBytes)) + return err +} diff --git a/cmd/tap/receive_test.go b/cmd/tap/receive_test.go new file mode 100644 index 0000000..e08fa25 --- /dev/null +++ b/cmd/tap/receive_test.go @@ -0,0 +1,261 @@ +package main + +import ( + "context" + "encoding/json" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + didcomm "github.com/Notabene-id/go-didcomm" + "github.com/Notabene-id/go-didcomm/cli" + tap "github.com/TransactionAuthorizationProtocol/tap-go" +) + +// generateIdentity creates a DID identity and writes files to dir. +func generateIdentity(t *testing.T, dir string) *didcomm.DIDDocument { + t.Helper() + doc, kp, err := didcomm.GenerateDIDKey() + if err != nil { + t.Fatal(err) + } + + docBytes, err := cli.MarshalDIDDoc(doc) + if err != nil { + t.Fatal(err) + } + keyBytes, err := cli.MarshalKeyPair(kp) + if err != nil { + t.Fatal(err) + } + + if err := os.MkdirAll(dir, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "did-doc.json"), docBytes, 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "keys.json"), keyBytes, 0o600); err != nil { + t.Fatal(err) + } + return doc +} + +func TestCLI_ReceiveMissingKeyFile(t *testing.T) { + bin := buildBinary(t) + cmd := exec.Command(bin, "receive", "--message", "{}") + out, err := cmd.CombinedOutput() + if err == nil { + t.Fatal("expected error for missing key file") + } + if !strings.Contains(string(out), "--key-file is required") { + t.Fatalf("expected key-file error, got: %s", out) + } +} + +func TestCLI_Receive_SignedTransfer(t *testing.T) { + bin := buildBinary(t) + dir := t.TempDir() + + aliceDir := filepath.Join(dir, "alice") + aliceDoc := generateIdentity(t, aliceDir) + + // Create a TAP transfer message + transferBody := &tap.TransferBody{ + Asset: "eip155:1/slip44:60", + Amount: "1.5", + Agents: []tap.Agent{{ID: aliceDoc.ID, Role: "OriginatingVASP"}}, + } + msg, err := tap.NewTransferMessage(aliceDoc.ID, []string{aliceDoc.ID}, transferBody) + if err != nil { + t.Fatal(err) + } + + // Pack it signed + client, err := cli.BuildClient( + filepath.Join(aliceDir, "keys.json"), + filepath.Join(aliceDir, "did-doc.json"), + ) + if err != nil { + t.Fatal(err) + } + + packed, err := client.PackSigned(context.Background(), msg) + if err != nil { + t.Fatal(err) + } + + // Receive via CLI + receiveCmd := exec.Command(bin, "receive", + "--key-file", filepath.Join(aliceDir, "keys.json"), + "--did-doc", filepath.Join(aliceDir, "did-doc.json"), + "--message", string(packed), + ) + out, err := receiveCmd.Output() + if err != nil { + ee, ok := err.(*exec.ExitError) + stderr := "" + if ok { + stderr = string(ee.Stderr) + } + t.Fatalf("receive failed: %s\n%s", err, stderr) + } + + var result receiveOutput + if err := json.Unmarshal(out, &result); err != nil { + t.Fatalf("invalid receive output: %s\noutput: %s", err, out) + } + + if !result.Signed { + t.Fatal("expected signed=true") + } + if result.Encrypted { + t.Fatal("expected encrypted=false") + } + if result.BodyType != tap.TypeTransfer { + t.Fatalf("expected bodyType %s, got %s", tap.TypeTransfer, result.BodyType) + } + + // Verify body content + var body tap.TransferBody + if err := json.Unmarshal(result.Body, &body); err != nil { + t.Fatal(err) + } + if body.Asset != "eip155:1/slip44:60" { + t.Fatalf("expected asset eip155:1/slip44:60, got %s", body.Asset) + } + if body.Amount != "1.5" { + t.Fatalf("expected amount 1.5, got %s", body.Amount) + } +} + +func TestCLI_Receive_AuthcryptReject(t *testing.T) { + bin := buildBinary(t) + dir := t.TempDir() + + aliceDir := filepath.Join(dir, "alice") + aliceDoc := generateIdentity(t, aliceDir) + bobDir := filepath.Join(dir, "bob") + bobDoc := generateIdentity(t, bobDir) + + didDocs := filepath.Join(aliceDir, "did-doc.json") + "," + filepath.Join(bobDir, "did-doc.json") + + // Create a TAP reject message + rejectBody := &tap.RejectBody{ + Reason: "compliance failure", + } + msg, err := tap.NewRejectMessage(aliceDoc.ID, []string{bobDoc.ID}, "thread-456", rejectBody) + if err != nil { + t.Fatal(err) + } + + // Pack authcrypt + client, err := cli.BuildClient(filepath.Join(aliceDir, "keys.json"), didDocs) + if err != nil { + t.Fatal(err) + } + + packed, err := client.PackAuthcrypt(context.Background(), msg) + if err != nil { + t.Fatal(err) + } + + // Receive via CLI with bob's keys + receiveCmd := exec.Command(bin, "receive", + "--key-file", filepath.Join(bobDir, "keys.json"), + "--did-doc", didDocs, + "--message", string(packed), + ) + out, err := receiveCmd.Output() + if err != nil { + ee, ok := err.(*exec.ExitError) + stderr := "" + if ok { + stderr = string(ee.Stderr) + } + t.Fatalf("receive failed: %s\n%s", err, stderr) + } + + var result receiveOutput + if err := json.Unmarshal(out, &result); err != nil { + t.Fatalf("invalid receive output: %s", err) + } + + if !result.Encrypted { + t.Fatal("expected encrypted=true") + } + if !result.Signed { + t.Fatal("expected signed=true") + } + if result.Anonymous { + t.Fatal("expected anonymous=false") + } + if result.BodyType != tap.TypeReject { + t.Fatalf("expected bodyType %s, got %s", tap.TypeReject, result.BodyType) + } + + var body tap.RejectBody + if err := json.Unmarshal(result.Body, &body); err != nil { + t.Fatal(err) + } + if body.Reason != "compliance failure" { + t.Fatalf("expected reason 'compliance failure', got %s", body.Reason) + } +} + +// TestCLI_MessagePipe_Transfer tests creating a message and piping to pack/unpack/receive. +func TestCLI_MessagePipe_Transfer(t *testing.T) { + bin := buildBinary(t) + dir := t.TempDir() + + aliceDir := filepath.Join(dir, "alice") + aliceDoc := generateIdentity(t, aliceDir) + + // Step 1: Create TAP message via CLI + body := `{"asset":"eip155:1/slip44:60","amount":"3.0","agents":[{"@id":"` + aliceDoc.ID + `","role":"OriginatingVASP"}]}` + msgCmd := exec.Command(bin, "message", "transfer", + "--from", aliceDoc.ID, + "--to", aliceDoc.ID, + "--body", body, + ) + msgOut, err := msgCmd.Output() + if err != nil { + t.Fatalf("message creation failed: %s", err) + } + + // Step 2: Pack signed + packCmd := exec.Command(bin, "pack", "signed", + "--key-file", filepath.Join(aliceDir, "keys.json"), + "--did-doc", filepath.Join(aliceDir, "did-doc.json"), + "--message", string(msgOut), + ) + packed, err := packCmd.Output() + if err != nil { + t.Fatalf("pack failed: %s", err) + } + + // Step 3: Receive (unpack + TAP parse) + receiveCmd := exec.Command(bin, "receive", + "--key-file", filepath.Join(aliceDir, "keys.json"), + "--did-doc", filepath.Join(aliceDir, "did-doc.json"), + "--message", string(packed), + ) + receiveOut, err := receiveCmd.Output() + if err != nil { + t.Fatalf("receive failed: %s", err) + } + + var result receiveOutput + if err := json.Unmarshal(receiveOut, &result); err != nil { + t.Fatalf("invalid receive output: %s", err) + } + + if result.BodyType != tap.TypeTransfer { + t.Fatalf("expected bodyType %s, got %s", tap.TypeTransfer, result.BodyType) + } + if !result.Signed { + t.Fatal("expected signed=true") + } +} diff --git a/go.mod b/go.mod index 94b0f68..3f78d6b 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/TransactionAuthorizationProtocol/tap-go go 1.26.0 require ( - github.com/Notabene-id/go-didcomm v0.1.0 + github.com/Notabene-id/go-didcomm v0.2.0 github.com/google/uuid v1.6.0 ) diff --git a/go.sum b/go.sum index 504d17c..dfcf1d1 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= -github.com/Notabene-id/go-didcomm v0.1.0 h1:qouUDL3vXiJRu8c2pIfk2cZre7NtO9WN4TqAbqCqVnM= -github.com/Notabene-id/go-didcomm v0.1.0/go.mod h1:wIm3s9UCKYYLe2zIysHjTKj7SCU9g25HUWWWc8h9mho= +github.com/Notabene-id/go-didcomm v0.2.0 h1:sTpG8zayJWn3GW8L7iKqtE1dEm2Lvmv8Xmk5yj5I9Ek= +github.com/Notabene-id/go-didcomm v0.2.0/go.mod h1:wIm3s9UCKYYLe2zIysHjTKj7SCU9g25HUWWWc8h9mho= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From fe6fd932f1322a849c44eccc146b0a658626f401 Mon Sep 17 00:00:00 2001 From: Pelle Braendgaard Date: Sun, 1 Mar 2026 15:03:10 +0100 Subject: [PATCH 2/4] Add pre-push checklist to CLAUDE.md Require running linter and tests locally before pushing. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 7371286..be36c6a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -97,6 +97,17 @@ Test vectors from `TAIPs/test-vectors/` are loaded in tests. The `TAIPs` directo The `Agent.For` field uses `ForField` type, which handles JSON marshaling of both single DID strings and arrays of DIDs. Use `NewForField("did:eg:alice")` or `NewForField("did:eg:alice", "did:eg:bob")`. +## Pre-push checklist + +**Always run both the linter and tests locally before pushing:** + +```bash +golangci-lint run ./... +go test ./... +``` + +Fix any lint errors or test failures before pushing. CI runs both checks and will block the PR if either fails. + ## Development guidelines - Every new message type needs: body struct, `TAPType()`, `New*Message()` constructor, a case in `ParseBody()`, and a matching `_test.go` From b42fe21d0dd2461e8e5a686e8d048f537d7efd5e Mon Sep 17 00:00:00 2001 From: Pelle Braendgaard Date: Sun, 1 Mar 2026 15:04:47 +0100 Subject: [PATCH 3/4] Add go fmt to pre-push checklist in CLAUDE.md Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index be36c6a..d8c6750 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,14 +99,15 @@ The `Agent.For` field uses `ForField` type, which handles JSON marshaling of bot ## Pre-push checklist -**Always run both the linter and tests locally before pushing:** +**Always run formatting, linter, and tests locally before committing/pushing:** ```bash +go fmt ./... golangci-lint run ./... go test ./... ``` -Fix any lint errors or test failures before pushing. CI runs both checks and will block the PR if either fails. +Fix any issues before committing. CI runs linter and tests and will block the PR if either fails. ## Development guidelines From f3ee9404c9a89a5fd33bc791c77abb6036f1b9d3 Mon Sep 17 00:00:00 2001 From: Pelle Braendgaard Date: Sun, 1 Mar 2026 15:08:26 +0100 Subject: [PATCH 4/4] Run go fmt on all source files Co-Authored-By: Claude Opus 4.6 --- authorize.go | 12 ++++++------ confirm_relationship.go | 12 ++++++------ message_types.go | 10 +++++----- message_types_test.go | 38 +++++++++++++++++++------------------- payment.go | 24 ++++++++++++------------ transfer_test.go | 4 ++-- types.go | 38 +++++++++++++++++++------------------- types_test.go | 2 +- 8 files changed, 70 insertions(+), 70 deletions(-) diff --git a/authorize.go b/authorize.go index 86021e7..d68d7c2 100644 --- a/authorize.go +++ b/authorize.go @@ -10,12 +10,12 @@ import ( // AuthorizeBody represents the body of a TAP Authorize message (TAIP-4). type AuthorizeBody struct { - Context string `json:"@context"` - Type string `json:"@type"` - SettlementAddress string `json:"settlementAddress,omitempty"` - SettlementAsset string `json:"settlementAsset,omitempty"` - Amount string `json:"amount,omitempty"` - Expiry string `json:"expiry,omitempty"` + Context string `json:"@context"` + Type string `json:"@type"` + SettlementAddress string `json:"settlementAddress,omitempty"` + SettlementAsset string `json:"settlementAsset,omitempty"` + Amount string `json:"amount,omitempty"` + Expiry string `json:"expiry,omitempty"` } func (b *AuthorizeBody) TAPType() string { return TypeAuthorize } diff --git a/confirm_relationship.go b/confirm_relationship.go index c6d766b..702d47f 100644 --- a/confirm_relationship.go +++ b/confirm_relationship.go @@ -10,13 +10,13 @@ import ( // ConfirmRelationshipBody represents the body of a TAP ConfirmRelationship message (TAIP-9). type ConfirmRelationshipBody struct { - Context string `json:"@context"` - Type string `json:"@type"` + Context string `json:"@context"` + Type string `json:"@type"` Relationship *Relationship `json:"relationship"` - Status string `json:"status"` - ValidFrom string `json:"validFrom,omitempty"` - ValidUntil string `json:"validUntil,omitempty"` - Details any `json:"details,omitempty"` + Status string `json:"status"` + ValidFrom string `json:"validFrom,omitempty"` + ValidUntil string `json:"validUntil,omitempty"` + Details any `json:"details,omitempty"` } func (b *ConfirmRelationshipBody) TAPType() string { return TypeConfirmRelationship } diff --git a/message_types.go b/message_types.go index 862d4ec..be3f525 100644 --- a/message_types.go +++ b/message_types.go @@ -21,11 +21,11 @@ const ( TypeCapture = "https://tap.rsvp/schema/1.0#Capture" // Agent management message types - TypeUpdateAgent = "https://tap.rsvp/schema/1.0#UpdateAgent" - TypeUpdateParty = "https://tap.rsvp/schema/1.0#UpdateParty" - TypeAddAgents = "https://tap.rsvp/schema/1.0#AddAgents" - TypeReplaceAgent = "https://tap.rsvp/schema/1.0#ReplaceAgent" - TypeRemoveAgent = "https://tap.rsvp/schema/1.0#RemoveAgent" + TypeUpdateAgent = "https://tap.rsvp/schema/1.0#UpdateAgent" + TypeUpdateParty = "https://tap.rsvp/schema/1.0#UpdateParty" + TypeAddAgents = "https://tap.rsvp/schema/1.0#AddAgents" + TypeReplaceAgent = "https://tap.rsvp/schema/1.0#ReplaceAgent" + TypeRemoveAgent = "https://tap.rsvp/schema/1.0#RemoveAgent" // Relationship message types TypeConfirmRelationship = "https://tap.rsvp/schema/1.0#ConfirmRelationship" diff --git a/message_types_test.go b/message_types_test.go index e0d176e..9c57cff 100644 --- a/message_types_test.go +++ b/message_types_test.go @@ -7,26 +7,26 @@ import ( func TestTypeConstants(t *testing.T) { types := map[string]string{ - "Transfer": TypeTransfer, - "Payment": TypePayment, - "Exchange": TypeExchange, - "Quote": TypeQuote, - "Escrow": TypeEscrow, - "Authorize": TypeAuthorize, + "Transfer": TypeTransfer, + "Payment": TypePayment, + "Exchange": TypeExchange, + "Quote": TypeQuote, + "Escrow": TypeEscrow, + "Authorize": TypeAuthorize, "AuthorizationRequired": TypeAuthorizationRequired, - "Settle": TypeSettle, - "Reject": TypeReject, - "Cancel": TypeCancel, - "Revert": TypeRevert, - "Capture": TypeCapture, - "UpdateAgent": TypeUpdateAgent, - "UpdateParty": TypeUpdateParty, - "AddAgents": TypeAddAgents, - "ReplaceAgent": TypeReplaceAgent, - "RemoveAgent": TypeRemoveAgent, - "ConfirmRelationship": TypeConfirmRelationship, - "UpdatePolicies": TypeUpdatePolicies, - "Connect": TypeConnect, + "Settle": TypeSettle, + "Reject": TypeReject, + "Cancel": TypeCancel, + "Revert": TypeRevert, + "Capture": TypeCapture, + "UpdateAgent": TypeUpdateAgent, + "UpdateParty": TypeUpdateParty, + "AddAgents": TypeAddAgents, + "ReplaceAgent": TypeReplaceAgent, + "RemoveAgent": TypeRemoveAgent, + "ConfirmRelationship": TypeConfirmRelationship, + "UpdatePolicies": TypeUpdatePolicies, + "Connect": TypeConnect, } for name, typ := range types { diff --git a/payment.go b/payment.go index 07eff40..89b68e7 100644 --- a/payment.go +++ b/payment.go @@ -10,19 +10,19 @@ import ( // PaymentBody represents the body of a TAP Payment message (TAIP-14). type PaymentBody struct { - Context string `json:"@context"` - Type string `json:"@type"` - Amount string `json:"amount"` - Asset string `json:"asset,omitempty"` - Currency string `json:"currency,omitempty"` - Merchant *Party `json:"merchant"` - Customer *Party `json:"customer,omitempty"` - Agents []Agent `json:"agents"` - SupportedAssets []any `json:"supportedAssets,omitempty"` + Context string `json:"@context"` + Type string `json:"@type"` + Amount string `json:"amount"` + Asset string `json:"asset,omitempty"` + Currency string `json:"currency,omitempty"` + Merchant *Party `json:"merchant"` + Customer *Party `json:"customer,omitempty"` + Agents []Agent `json:"agents"` + SupportedAssets []any `json:"supportedAssets,omitempty"` FallbackSettlementAddresses []string `json:"fallbackSettlementAddresses,omitempty"` - Expiry string `json:"expiry,omitempty"` - Invoice any `json:"invoice,omitempty"` - Policies []Policy `json:"policies,omitempty"` + Expiry string `json:"expiry,omitempty"` + Invoice any `json:"invoice,omitempty"` + Policies []Policy `json:"policies,omitempty"` } func (b *PaymentBody) TAPType() string { return TypePayment } diff --git a/transfer_test.go b/transfer_test.go index 0834574..fab1c16 100644 --- a/transfer_test.go +++ b/transfer_test.go @@ -9,8 +9,8 @@ import ( func TestNewTransferMessage(t *testing.T) { body := &TransferBody{ - Asset: "eip155:1/slip44:60", - Amount: "1.23", + Asset: "eip155:1/slip44:60", + Amount: "1.23", Originator: &Party{ID: "did:eg:bob"}, Agents: []Agent{ {ID: "did:web:originator.vasp", For: NewForField("did:eg:bob")}, diff --git a/types.go b/types.go index ee9b3d1..96a0ff9 100644 --- a/types.go +++ b/types.go @@ -110,12 +110,12 @@ type Policy struct { // TransactionConstraints defines boundaries for transactions in a connection. type TransactionConstraints struct { - Purposes []string `json:"purposes,omitempty"` - CategoryPurposes []string `json:"categoryPurposes,omitempty"` - Limits *Limits `json:"limits,omitempty"` - AllowedBeneficiaries []Party `json:"allowedBeneficiaries,omitempty"` + Purposes []string `json:"purposes,omitempty"` + CategoryPurposes []string `json:"categoryPurposes,omitempty"` + Limits *Limits `json:"limits,omitempty"` + AllowedBeneficiaries []Party `json:"allowedBeneficiaries,omitempty"` AllowedSettlementAddresses []string `json:"allowedSettlementAddresses,omitempty"` - AllowedAssets []string `json:"allowedAssets,omitempty"` + AllowedAssets []string `json:"allowedAssets,omitempty"` } // Limits defines financial limits for transactions. @@ -130,19 +130,19 @@ type Limits struct { // Invoice represents a structured invoice for payment information. type Invoice struct { - ID string `json:"id"` - IssueDate string `json:"issueDate"` - CurrencyCode string `json:"currencyCode"` - LineItems []LineItem `json:"lineItems"` - Total float64 `json:"total"` - SubTotal *float64 `json:"subTotal,omitempty"` - TaxTotal *TaxTotal `json:"taxTotal,omitempty"` - DueDate string `json:"dueDate,omitempty"` - Note string `json:"note,omitempty"` - PaymentTerms string `json:"paymentTerms,omitempty"` - AccountingCost string `json:"accountingCost,omitempty"` - OrderReference *OrderReference `json:"orderReference,omitempty"` - AdditionalDocumentReference []DocumentReference `json:"additionalDocumentReference,omitempty"` + ID string `json:"id"` + IssueDate string `json:"issueDate"` + CurrencyCode string `json:"currencyCode"` + LineItems []LineItem `json:"lineItems"` + Total float64 `json:"total"` + SubTotal *float64 `json:"subTotal,omitempty"` + TaxTotal *TaxTotal `json:"taxTotal,omitempty"` + DueDate string `json:"dueDate,omitempty"` + Note string `json:"note,omitempty"` + PaymentTerms string `json:"paymentTerms,omitempty"` + AccountingCost string `json:"accountingCost,omitempty"` + OrderReference *OrderReference `json:"orderReference,omitempty"` + AdditionalDocumentReference []DocumentReference `json:"additionalDocumentReference,omitempty"` } // LineItem represents an individual item in an invoice. @@ -194,7 +194,7 @@ type DocumentReference struct { // Relationship represents a relationship between parties for ConfirmRelationship. type Relationship struct { - Type string `json:"type"` + Type string `json:"type"` Parties []Party `json:"parties"` EstablishedDate string `json:"establishedDate,omitempty"` Reference string `json:"reference,omitempty"` diff --git a/types_test.go b/types_test.go index 8f0cc30..07883c0 100644 --- a/types_test.go +++ b/types_test.go @@ -150,7 +150,7 @@ func TestTransactionConstraints_JSONRoundTrip(t *testing.T) { {ID: "did:example:vendor-1", Name: "Approved Vendor 1"}, }, AllowedSettlementAddresses: []string{"eip155:1:0x742d35Cc6e4dfE2eDFaD2C0b91A8b0780EDAEb58"}, - AllowedAssets: []string{"eip155:1/slip44:60"}, + AllowedAssets: []string{"eip155:1/slip44:60"}, } data, err := json.Marshal(tc)