Skip to content

fix(DAN-854): recipient is REQUIRED per METHOD, not across the blockchain set - #8

Open
Danny-Devs wants to merge 1 commit into
mainfrom
agent-a/DAN-854-lightning-recipient-optionality
Open

fix(DAN-854): recipient is REQUIRED per METHOD, not across the blockchain set#8
Danny-Devs wants to merge 1 commit into
mainfrom
agent-a/DAN-854-lightning-recipient-optionality

Conversation

@Danny-Devs

Copy link
Copy Markdown
Contributor

Closes DAN-854.

The defect

fromMppChargeChallenge rejected every Charge request lacking recipient, and the error message cited an authority that says the opposite:

Blockchain Charge request missing "recipient" — required by charge-intent spec for blockchain methods

draft-payment-intent-charge-00 §Shared Fields lists recipient under Optional Fields. The line that message leaned on is a parenthetical — "Payment methods MAY elevate OPTIONAL fields to REQUIRED in their method specification (e.g. recipient and expires are REQUIRED for blockchain methods)" — which illustrates a mechanism, and that mechanism is per-method. Reading it as a blanket rule lets a prose example in the core document override the method specs that are actually normative.

Requirement levels, read from each method's own request schema

tempoxyz/mpp-specs @ f9506cd:

method recipient source
evm REQUIRED draft-evm-charge-00.md:264
tempo REQUIRED draft-tempo-charge-00.md:155
stellar REQUIRED draft-stellar-charge-00.md:258
solana REQUIRED draft-solana-charge-00.md
lightning OPTIONAL draft-lightning-charge-00.md:206

recipient : OPTIONAL. … Lightning implementations typically do not use this field; the invoice payee is implied by the BOLT11 invoice.

Lightning is the lone exception among the five, which is why the blanket rule survived review — it is correct four times out of five. The Lightning draft's own canonical example threw upstream of resolveNetwork, at the recipient check.

The design question: what does payTo become?

The BOLT11 invoice. draft-lightning-charge-00 §Method Details makes methodDetails.invoice REQUIRED and states "This field is authoritative; all other payment parameters are derived from it." The payee is implied by it, so it is the payment destination.

Every branch of resolvePayTo returns a payable destination or throws. There is deliberately no path that yields "" — requirements carrying an empty payTo look structurally valid and can never be settled, which is strictly worse than rejecting at the boundary. An explicit recipient still wins when Lightning supplies one, since OPTIONAL is not forbidden.

The write-path twin, fixed in the same change

toMppChargeRequest keyed off the identical blanket set, so it refused to emit a spec-legal Lightning charge — one carrying an authoritative invoice and no recipient. Same misreading, opposite direction. Both paths now share one table rather than growing a second copy.

Criterion 5 — confirmed, not changed

The same misreading did not reach expires. Every reference is to challenge.expires, the auth-param, never a request.expires — which is exactly what the charge spec requires, since request objects MUST NOT duplicate the expiry value.

Verification

pnpm vitest run test/compat-mpp-recipient-optionality.test.ts   # 9/9
pnpm typecheck                                                   # clean
pnpm vitest run                                                  # 1111 passed, 29 files

The assertion that failed before this change, and is the whole of the defect:

// draft-lightning-charge-00.md §Examples, "Decoded request", verbatim
expect(() => fromMppChargeChallenge(lightningSpecExample)).not.toThrow();

Watched failing (3 failed / 6 passed) before the fix, then passing. No Python MPP compat module exists, so there is no cross-language twin — checked, not assumed.

Deliberately not done here

Acceptance criterion 4 asks that the synthetic recipient workaround be removed from test/compat-mpp-network-resolution.test.ts. That file does not exist on main — it lives on PR #3's branch (et3rnald/dan-848-mpp-network-resolution), which is still open. It cannot be edited from here without stacking, and a stacked PR runs no CI in this repo (DAN-873): it would show two green Vercel checks while the whole suite sat out. This PR targets main so its checks are real. The workaround should be removed when #3 lands; the fix it worked around is now in place.

Noted in passing, not in scope

The Lightning draft's 402 example is internally inconsistent: the base64 request auth-param decodes to "currency":"BTC", while the "Decoded request" block directly beneath it reads "currency": "sat" and §Shared Fields states currency MUST be "sat". The base64 appears not to have been regenerated. Neither shape carries recipient, so this PR is unaffected. Worth an upstream report.

🤖 Generated with Claude Code

…hain set

fromMppChargeChallenge rejected every Charge request without "recipient",
citing the charge-intent spec as its authority. That spec lists recipient under
OPTIONAL Fields; the line the message leaned on is a parenthetical illustrating
that a method spec MAY elevate it. The mechanism is per-method, and Lightning
exercises it in the other direction.

Read from each method spec at mpp-specs f9506cd:
  evm REQUIRED · tempo REQUIRED · solana REQUIRED · stellar REQUIRED
  lightning OPTIONAL — "the invoice payee is implied by the BOLT11 invoice"

Lightning is the lone exception among the five, which is why a blanket rule
survived: it is correct four times out of five. The Lightning drafts own
canonical example threw before reaching resolveNetwork.

payTo for Lightning resolves to methodDetails.invoice, which the spec calls
authoritative and from which all other payment parameters derive. Every branch
of resolvePayTo returns a payable destination or throws — there is no path that
emits an empty string, because requirements carrying payTo:"" look valid and can
never settle.

Fixes the write-path twin in the same change and off the same table:
toMppChargeRequest used the identical blanket set, so it refused to EMIT a
spec-legal Lightning charge. Same misreading, opposite direction.

Error messages now name the method spec that actually requires the field. The
wrong reason is what the next agent acts on.

Criterion 5, confirmed not changed: the misreading did not reach "expires".
Every reference is to challenge.expires, the auth-param, never request.expires
— which is what the charge spec requires.

Gates: tsc --noEmit clean · 1111 tests / 29 files pass. No Python MPP compat
module exists, so there is no cross-language twin (checked, not assumed).
@vercel

vercel Bot commented Aug 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
s402-docs Ready Ready Preview Aug 16, 2026 11:05pm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant