payment

package
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 23, 2026 License: AGPL-3.0 Imports: 7 Imported by: 0

Documentation

Overview

Package payment is the platform's payment-capability protocol surface.

Three independent extension slots:

  • Method — produces and verifies proofs-of-payment (e.g. wallet signs a SignedAuth; an on-chain method submits a transaction and returns its hash; Stripe charges a card and returns its session id). Apps implementing Method are how a payer satisfies a Contract.

  • Escrow — holds the symmetric key behind a sealed message and releases it on a verified Receipt. Sender-self (the wallet) is one impl; a third-party notary is another.

  • Seal — encrypts the body of a sealed message. Default is chacha20-poly1305 (see pkg/payment/seal package). Other algos can be plugged in by registering a new ID.

The wire types — Contract, Receipt, EscrowRef, SealedEnvelope — are transport-agnostic JSON. They flow through hook chains (pkg/extend), over pilot's overlay, or through HTTP 402 shims, all the same shape.

Index

Constants

View Source
const DefaultSealID = "seal/chacha20-poly1305-v1"

Variables

View Source
var ErrCannotSatisfy = errors.New("payment: method cannot satisfy contract")

ErrCannotSatisfy signals a method declines a contract because it is out-of-scope (wrong asset, unsupported amount, etc.). Distinct from transient failures, so the daemon's broker can try another method without surfacing the error to the user.

View Source
var ErrEscrowConsumed = errors.New("payment: escrow already consumed")

ErrEscrowConsumed is returned by Redeem when the key has already been released. Stops replay against a single payment.

View Source
var ErrEscrowNotFound = errors.New("payment: escrow not found")

ErrEscrowNotFound is returned by Redeem when the EscrowRef does not resolve to a held key.

View Source
var ErrUnknownMethod = errors.New("payment: unknown method id")

ErrUnknownMethod is returned by VerifierFor when no Method matches.

Functions

func RandomKey

func RandomKey(seal Seal) ([]byte, error)

RandomKey returns a cryptographically random key of the right size for seal. Convenience for callers wrapping a body before sealing.

func RandomNonce

func RandomNonce(seal Seal) ([]byte, error)

RandomNonce returns a cryptographically random nonce of the right size for seal. For chacha20-poly1305 the 12-byte nonce is safe to pick randomly — collision probability is ~2^-32 after 2^32 messages under one key, but we use fresh keys per envelope, so reuse is effectively impossible.

Types

type Contract

type Contract struct {
	ID              string    `json:"id"`
	Amount          uint64    `json:"amount"`
	Asset           string    `json:"asset"`
	RecipientAddr   string    `json:"recipient_addr"`
	ExpiresAt       time.Time `json:"expires_at"`
	Nonce           string    `json:"nonce"`
	AcceptedMethods []string  `json:"accepted_methods,omitempty"`
	AcceptedEscrows []string  `json:"accepted_escrows,omitempty"`
	Memo            string    `json:"memo,omitempty"`
}

Contract is the abstract statement of what payment is required to unseal a message (or satisfy any other gated action).

AcceptedMethods and AcceptedEscrows scope what implementations may participate. If empty, defaults are configured by the daemon's installed-app policy. Recipients pick any of their installed methods whose ID is in AcceptedMethods.

type Escrow

type Escrow interface {
	ID() string
	Hold(ctx context.Context, c Contract, k []byte) (EscrowRef, error)
	Redeem(ctx context.Context, ref EscrowRef, r Receipt) ([]byte, error)
}

Escrow holds a symmetric key K bound to a Contract, releasing it when presented with a verified Receipt. The platform's send-side paywall flow uses Hold to stash K right after encrypting the body; the receive-side redeem flow uses Redeem to swap a Receipt for K.

Implementations: io.pilot.wallet/escrow-v1 (sender-self, the wallet holds K in its own memory + ledger); io.pilot.notary/v1 (always-on third-party); io.pilot.timelock/v1 (cryptographic, no online server).

type EscrowRef

type EscrowRef struct {
	EscrowID   string `json:"escrow_id"`
	Endpoint   string `json:"endpoint"`
	Token      string `json:"token"`
	ContractID string `json:"contract_id"`
}

EscrowRef tells a recipient how to redeem K for a contract. Endpoint is a pilot address (or, eventually, a URL) at which the escrow's Redeem RPC is callable. Token is a handle the escrow uses to look up K — opaque to everyone else.

type EscrowRegistry

type EscrowRegistry struct {
	// contains filtered or unexported fields
}

EscrowRegistry maps escrow ID → Escrow.

func NewEscrowRegistry

func NewEscrowRegistry() *EscrowRegistry

NewEscrowRegistry returns an empty registry.

func (*EscrowRegistry) Get

func (r *EscrowRegistry) Get(id string) Escrow

Get returns the Escrow for id, or nil.

func (*EscrowRegistry) IDs

func (r *EscrowRegistry) IDs() []string

IDs returns every registered escrow ID.

func (*EscrowRegistry) PickFor

func (r *EscrowRegistry) PickFor(c Contract) (Escrow, error)

PickFor returns an Escrow whose ID is in the contract's AcceptedEscrows (or any registered escrow when unconstrained). Used by the sender side to decide where to stash K. Returns the first match in map iteration order; callers wanting deterministic choice should constrain the contract.

func (*EscrowRegistry) Redeem

func (r *EscrowRegistry) Redeem(ctx context.Context, ref EscrowRef, rec Receipt) ([]byte, error)

Redeem dispatches to the Escrow named by ref.EscrowID. The receipt must verify against the contract via the matching Method registry — callers chain Verify then Redeem to ensure key release is contingent on a real payment proof.

func (*EscrowRegistry) Register

func (r *EscrowRegistry) Register(e Escrow) error

Register adds an Escrow. Replaces any prior entry with the same ID.

func (*EscrowRegistry) Unregister

func (r *EscrowRegistry) Unregister(id string)

Unregister removes an Escrow by ID. Idempotent.

type Method

type Method interface {
	// ID is a globally unique identifier for this payment rail and
	// receipt format. The receiver of a Receipt uses MethodID to find
	// the matching Method's Verify.
	ID() string

	// Satisfy attempts to produce a Receipt for the Contract. It may
	// debit a balance, submit an on-chain tx, charge a card —
	// implementation-specific. Returns ErrCannotSatisfy if the contract
	// names assets/amounts the method does not support; a different
	// error for failures during a satisfaction attempt that's
	// otherwise in-scope.
	Satisfy(ctx context.Context, c Contract) (Receipt, error)

	// Verify checks a Receipt against its Contract. Returns nil if the
	// Receipt is a valid proof of payment. Verifiers are pure functions
	// of (Contract, Receipt) + the method's stable verification keyset;
	// they do not need to talk to the payer.
	Verify(ctx context.Context, c Contract, r Receipt) error
}

Method produces and verifies proofs-of-payment for a Contract. Implementations are concrete payment rails — internal-ledger (io.pilot.wallet/v1), on-chain (io.lightning.bolt12/v1, etc.), off-chain (io.stripe.checkout/v1). Apps register their Methods at app-start with MethodRegistry.

Implementations MUST be safe for concurrent use — the daemon may dispatch many Satisfies in parallel.

type MethodRegistry

type MethodRegistry struct {
	// contains filtered or unexported fields
}

MethodRegistry maps method ID → Method. Populated at app-start, read during dispatch.

func NewMethodRegistry

func NewMethodRegistry() *MethodRegistry

NewMethodRegistry returns an empty registry.

func (*MethodRegistry) Get

func (r *MethodRegistry) Get(id string) Method

Get returns the Method for id, or nil if absent.

func (*MethodRegistry) Has

func (r *MethodRegistry) Has(id string) bool

Has reports whether id is registered.

func (*MethodRegistry) IDs

func (r *MethodRegistry) IDs() []string

IDs returns every registered method ID.

func (*MethodRegistry) Register

func (r *MethodRegistry) Register(m Method) error

Register adds a Method. Replaces any prior entry with the same ID (later install wins); callers wanting strict-no-replace should check Has first.

func (*MethodRegistry) Satisfy

func (r *MethodRegistry) Satisfy(ctx context.Context, c Contract) (Receipt, error)

Satisfy is the broker entry point on the payer side. Picks any registered Method whose ID is in c.AcceptedMethods (or any registered method, if the contract leaves it unconstrained) and asks it to Satisfy. Tries methods in registration order; returns the first Receipt produced. If every method returns ErrCannotSatisfy, returns the last such error.

func (*MethodRegistry) Unregister

func (r *MethodRegistry) Unregister(id string)

Unregister removes a Method by ID. Idempotent.

func (*MethodRegistry) Verify

func (r *MethodRegistry) Verify(ctx context.Context, c Contract, rec Receipt) error

Verify dispatches to the Method named by r.MethodID. The contract is passed so the method can re-derive canonical signing bytes, check the receipt is not replayed, etc.

type Receipt

type Receipt struct {
	ContractID string `json:"contract_id"`
	MethodID   string `json:"method_id"`
	Payload    []byte `json:"payload"`
}

Receipt is the method-tagged proof a Contract was satisfied. Payload is opaque to the platform — only the Method whose ID matches MethodID knows how to interpret it. For io.pilot.wallet/v1 the payload is the JSON-encoded SignedAuth.

type Seal

type Seal interface {
	ID() string
	KeySize() int
	NonceSize() int
	Encrypt(plaintext, key, nonce, ad []byte) ([]byte, error)
	Decrypt(ciphertext, key, nonce, ad []byte) ([]byte, error)
}

Seal is the symmetric encryption primitive for paywalled message bodies. The default impl (DefaultSeal) is chacha20-poly1305 with the standard 12-byte nonce; apps may register others (e.g. age x25519 for recipient-targeted encryption) and the contract names the algorithm via SealID so verification picks the right impl.

Encrypt/Decrypt both take associated data (AD) that's authenticated but not encrypted. Callers pass the canonical SealedEnvelope header (or a binding of it) as AD so a tampered Contract/EscrowRef triggers an AEAD failure instead of producing a valid plaintext under rewritten metadata.

func DefaultSeal

func DefaultSeal() Seal

DefaultSeal returns the chacha20-poly1305 implementation. ID is "seal/chacha20-poly1305-v1".

type SealRegistry

type SealRegistry struct {
	// contains filtered or unexported fields
}

SealRegistry maps seal ID → Seal.

func NewSealRegistry

func NewSealRegistry() *SealRegistry

NewSealRegistry returns a registry pre-populated with DefaultSeal. Apps that ship alternate seals call Register at app-start.

func (*SealRegistry) Get

func (r *SealRegistry) Get(id string) Seal

func (*SealRegistry) IDs

func (r *SealRegistry) IDs() []string

func (*SealRegistry) Register

func (r *SealRegistry) Register(s Seal) error

type SealedEnvelope

type SealedEnvelope struct {
	Contract   Contract  `json:"contract"`
	EscrowRef  EscrowRef `json:"escrow_ref"`
	SealID     string    `json:"seal_id"`
	Ciphertext []byte    `json:"ciphertext"`
	Nonce      []byte    `json:"nonce"`
	SenderAddr string    `json:"sender_addr"`
}

SealedEnvelope is what travels on the wire when an app paywalls a message body. The recipient holds the ciphertext locally; when they decide to pay, they fetch K from the EscrowRef using a Receipt, and decrypt with the named Seal.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL