From 43e4baf35c385c01d9641cada4670c2f4c6f30f3 Mon Sep 17 00:00:00 2001 From: Alexgodoroja Date: Fri, 25 Sep 2026 15:30:16 +0200 Subject: [PATCH 1/2] wallet.help discovery method Every app answers .help with its methods and params. The wallet didn't, so agents had no way to learn its surface from the store. wallet.help lists exactly the methods the dispatcher serves, including the EVM and settler sets when those are wired; a test keeps help and the dispatcher in step. Also released as 0.3.4 from release/0.3.x. Co-Authored-By: Claude Opus 5.5 (1M context) --- cmd/wallet/main.go | 1 + manifest.json | 3 +- pkg/walletipc/api.go | 1 + pkg/walletipc/dispatcher.go | 1 + pkg/walletipc/help.go | 76 +++++++++++++++++++++++++++++++++++++ pkg/walletipc/help_test.go | 39 +++++++++++++++++++ 6 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 pkg/walletipc/help.go create mode 100644 pkg/walletipc/help_test.go diff --git a/cmd/wallet/main.go b/cmd/wallet/main.go index 4ef77fe..a6fe698 100644 --- a/cmd/wallet/main.go +++ b/cmd/wallet/main.go @@ -251,6 +251,7 @@ func run(ctx context.Context, args []string) error { logger.Printf("settler: endpoint=%s anchor=%v", settlerEP, anchor != nil) } + walletipc.Version = Version dispatcher := walletipc.NewDispatcher(w) // If a stale socket exists from a previous crash, drop it. unix sockets diff --git a/manifest.json b/manifest.json index 5821d71..9837ecc 100644 --- a/manifest.json +++ b/manifest.json @@ -28,7 +28,8 @@ "wallet.settler.history", "wallet.settler.transfer", "wallet.hookPreSendMessage", - "wallet.hookPostRecvMessage" + "wallet.hookPostRecvMessage", + "wallet.help" ], "grants": [ {"cap": "fs.write", "target": "$APP/data.db"}, diff --git a/pkg/walletipc/api.go b/pkg/walletipc/api.go index 79875ca..ab4c517 100644 --- a/pkg/walletipc/api.go +++ b/pkg/walletipc/api.go @@ -48,6 +48,7 @@ const ( var AllMethods = []string{ MethodBalance, MethodBalances, MethodAddress, MethodRequest, MethodPay, MethodVerify, MethodSettle, MethodTopup, MethodHistory, MethodSpendCaps, + MethodHelp, } // AllEVMMethods is the extra set the dispatcher registers when the diff --git a/pkg/walletipc/dispatcher.go b/pkg/walletipc/dispatcher.go index 388f51f..d58bd2b 100644 --- a/pkg/walletipc/dispatcher.go +++ b/pkg/walletipc/dispatcher.go @@ -28,6 +28,7 @@ func NewDispatcher(w *wallet.Wallet) *ipc.Dispatcher { d.Register(MethodSettle, settleHandler(w)) d.Register(MethodTopup, topupHandler(w)) d.Register(MethodHistory, historyHandler(w)) + d.Register(MethodHelp, helpHandler(w)) RegisterEVM(d, w) RegisterSettler(d, w) return d diff --git a/pkg/walletipc/help.go b/pkg/walletipc/help.go new file mode 100644 index 0000000..4331742 --- /dev/null +++ b/pkg/walletipc/help.go @@ -0,0 +1,76 @@ +package walletipc + +import ( + "context" + "encoding/json" + + "github.com/pilot-protocol/app-store/pkg/ipc" + "github.com/pilot-protocol/wallet/pkg/wallet" +) + +// MethodHelp is the app-store discovery contract: every app answers +// .help with its methods and their params, so an agent can call the +// wallet correctly without reading the source. +const MethodHelp = "wallet.help" + +// Version is the app version wallet.help reports. cmd/wallet sets it from +// its own Version, which a test pins to manifest.json's app_version. +var Version = "dev" + +type helpMethod struct { + Method string `json:"method"` + Summary string `json:"summary"` + Params map[string]string `json:"params,omitempty"` + Duration string `json:"duration"` +} + +var helpCore = []helpMethod{ + {MethodAddress, "This wallet's Pilot payment address.", nil, "fast"}, + {MethodBalance, "Balance of one asset.", map[string]string{"asset": "string (required) — asset code, e.g. \"PILOT\""}, "fast"}, + {MethodBalances, "Balances of every asset this wallet holds.", nil, "fast"}, + {MethodSpendCaps, "Configured spend caps with their live rolling-window usage.", nil, "fast"}, + {MethodHistory, "Transaction history, newest first.", map[string]string{ + "limit": "int — page size", "before_unix_nano": "int — cursor: the oldest timestamp from the prior page", + "since_unix_nano": "int — only newer transactions", "asset": "string — filter by asset", "kind": "string — filter by transaction kind"}, "fast"}, + {MethodRequest, "Create a payment challenge for a peer to pay.", map[string]string{ + "amount": "string (required) — amount in the asset's smallest unit", "asset": "string (required)", + "expires_in_seconds": "int (required) — challenge lifetime", "memo": "string — shown to the payer"}, "fast"}, + {MethodPay, "Sign a payment authorization for a challenge. Enforced against spend caps.", map[string]string{"challenge": "object (required) — from the payee's wallet.request"}, "fast"}, + {MethodVerify, "Check a signed authorization against its challenge without settling.", map[string]string{"challenge": "object (required)", "signed_auth": "object (required) — from wallet.pay"}, "fast"}, + {MethodSettle, "Verify and settle a signed authorization into this wallet.", map[string]string{"challenge": "object (required)", "signed_auth": "object (required)"}, "fast"}, + {MethodTopup, "Credit this wallet from an external source.", map[string]string{"asset": "string (required)", "amount": "string (required)", "source": "string (required) — where the funds came from"}, "fast"}, +} + +var helpEVM = []helpMethod{ + {MethodEVMChains, "EVM chains this wallet is configured for (USDC on Base, Ethereum, Polygon by default).", nil, "fast"}, + {MethodEVMAddress, "This wallet's EVM address on a chain.", map[string]string{"chain_id": "int — omit for the primary chain"}, "fast"}, + {MethodEVMBalance, "USDC balance on a chain.", map[string]string{"chain_id": "int — omit for the primary chain"}, "med"}, + {MethodEVMSatisfy, "Pay an x402 payment contract with an EIP-3009 authorization. Enforced against spend caps.", map[string]string{"contract": "object (required) — the x402 payment contract", "chain_id": "int"}, "med"}, + {MethodEVMVerify, "Verify an x402 receipt against its contract.", map[string]string{"contract": "object (required)", "receipt": "object (required)", "chain_id": "int"}, "med"}, +} + +var helpSettler = []helpMethod{ + {MethodSettlerIdentity, "The settler this wallet is wired to, and its identity.", nil, "fast"}, + {MethodSettlerBalance, "This wallet's balance held at the settler.", map[string]string{"asset": "string — asset symbol"}, "fast"}, + {MethodSettlerHistory, "This wallet's settler transaction history.", map[string]string{"limit": "int — page size"}, "fast"}, + {MethodSettlerTransfer, "Transfer through the settler. Enforced against spend caps.", map[string]string{"to": "string (required) — recipient's hex ed25519 pubkey", "asset": "string (required), e.g. \"USDC\"", "amount": "int (required) — smallest unit", "memo": "string", "expires_in_seconds": "int — default 300"}, "med"}, +} + +func helpHandler(w *wallet.Wallet) ipc.Handler { + return func(_ context.Context, _ *ipc.Envelope) (json.RawMessage, error) { + methods := append([]helpMethod{}, helpCore...) + if w.HasEVM() { + methods = append(methods, helpEVM...) + } + if w.HasSettler() { + methods = append(methods, helpSettler...) + } + methods = append(methods, helpMethod{MethodHelp, "This list.", nil, "fast"}) + return encode(map[string]any{ + "app": "io.pilot.wallet", + "version": Version, + "description": "Pilot's reference wallet: pay and get paid between agents (Pilot challenge/authorization flow) and pay x402 USDC contracts on EVM chains, all under per-asset spend caps.", + "methods": methods, + }) + } +} diff --git a/pkg/walletipc/help_test.go b/pkg/walletipc/help_test.go new file mode 100644 index 0000000..eaa7ca8 --- /dev/null +++ b/pkg/walletipc/help_test.go @@ -0,0 +1,39 @@ +package walletipc + +import ( + "testing" + + "github.com/pilot-protocol/app-store/pkg/ipc" +) + +// wallet.help is the discovery contract: it must describe every method the +// dispatcher actually serves, no more and no less. +func TestHelpListsEveryRegisteredMethod(t *testing.T) { + conn, w := servedWallet(t, addrAlice) + var resp struct { + App string `json:"app"` + Version string `json:"version"` + Methods []struct { + Method string `json:"method"` + } `json:"methods"` + } + if err := ipc.Call(conn, MethodHelp, nil, &resp); err != nil { + t.Fatal(err) + } + if resp.App != "io.pilot.wallet" || resp.Version != Version || len(resp.Methods) == 0 { + t.Errorf("app/version = %q/%q", resp.App, resp.Version) + } + listed := map[string]bool{} + for _, m := range resp.Methods { + listed[m.Method] = true + } + for _, m := range NewDispatcher(w).Methods() { + if !listed[m] { + t.Errorf("registered method %q missing from wallet.help", m) + } + delete(listed, m) + } + for m := range listed { + t.Errorf("wallet.help lists %q, which is not registered", m) + } +} From a10505101eeb234d4ff46ca4d7fe99ffd2ac43bc Mon Sep 17 00:00:00 2001 From: Teo Calin Date: Thu, 1 Oct 2026 17:26:16 +0300 Subject: [PATCH 2/2] wallet.help: amount is an integer for request and topup wallet.request and wallet.topup were documented as taking a string amount, but Amount is a uint64 and both handlers reject a JSON string. Document it as an int, as wallet.settler_transfer already does. Co-Authored-By: Claude Opus 5.5 --- pkg/walletipc/help.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/walletipc/help.go b/pkg/walletipc/help.go index 4331742..8b8cff3 100644 --- a/pkg/walletipc/help.go +++ b/pkg/walletipc/help.go @@ -33,12 +33,12 @@ var helpCore = []helpMethod{ "limit": "int — page size", "before_unix_nano": "int — cursor: the oldest timestamp from the prior page", "since_unix_nano": "int — only newer transactions", "asset": "string — filter by asset", "kind": "string — filter by transaction kind"}, "fast"}, {MethodRequest, "Create a payment challenge for a peer to pay.", map[string]string{ - "amount": "string (required) — amount in the asset's smallest unit", "asset": "string (required)", + "amount": "int (required) — amount in the asset's smallest unit", "asset": "string (required)", "expires_in_seconds": "int (required) — challenge lifetime", "memo": "string — shown to the payer"}, "fast"}, {MethodPay, "Sign a payment authorization for a challenge. Enforced against spend caps.", map[string]string{"challenge": "object (required) — from the payee's wallet.request"}, "fast"}, {MethodVerify, "Check a signed authorization against its challenge without settling.", map[string]string{"challenge": "object (required)", "signed_auth": "object (required) — from wallet.pay"}, "fast"}, {MethodSettle, "Verify and settle a signed authorization into this wallet.", map[string]string{"challenge": "object (required)", "signed_auth": "object (required)"}, "fast"}, - {MethodTopup, "Credit this wallet from an external source.", map[string]string{"asset": "string (required)", "amount": "string (required)", "source": "string (required) — where the funds came from"}, "fast"}, + {MethodTopup, "Credit this wallet from an external source.", map[string]string{"asset": "string (required)", "amount": "int (required) — amount in the asset's smallest unit", "source": "string (required) — where the funds came from"}, "fast"}, } var helpEVM = []helpMethod{