Skip to content
Merged
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
1 change: 1 addition & 0 deletions cmd/wallet/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,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
Expand Down
3 changes: 2 additions & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
1 change: 1 addition & 0 deletions pkg/walletipc/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions pkg/walletipc/dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 76 additions & 0 deletions pkg/walletipc/help.go
Original file line number Diff line number Diff line change
@@ -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
// <ns>.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": "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": "int (required) — amount in the asset's smallest unit", "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,
})
}
}
39 changes: 39 additions & 0 deletions pkg/walletipc/help_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading