Skip to content

fix(DAN-848): resolve the network each MPP method spec actually names - #3

Open
Danny-Devs wants to merge 1 commit into
mainfrom
et3rnald/dan-848-mpp-network-resolution
Open

fix(DAN-848): resolve the network each MPP method spec actually names#3
Danny-Devs wants to merge 1 commit into
mainfrom
et3rnald/dan-848-mpp-network-resolution

Conversation

@Danny-Devs

Copy link
Copy Markdown
Contributor

The defect

resolveNetwork read exactly one field — methodDetails.chainId — and branched for exactly two methods. Everything else fell through to `${method}:unknown`.

The Solana charge draft puts the cluster in methodDetails.network and makes it safety-critical:

Clients MUST reject challenges whose network does not match their configured cluster.

So a devnet challenge and a mainnet challenge both lifted into s402 requirements as solana:unknown. The one field the spec obliges a client to compare was discarded into a value that cannot fail the comparison, because it names no cluster.

This is a defect, not upstream drift. The field predates the module's declared spec baseline by six weeks — a baseline bump would have closed the sibling findings and left this one exactly as it is.

What changed

Resolution now reads whichever field the owning method spec names:

method field presence resolves to
solana methodDetails.network OPTIONAL, default mainnet solana:{mainnet|devnet|localnet}
lightning methodDetails.network OPTIONAL, default mainnet lightning:{mainnet|regtest|signet}
stellar methodDetails.network REQUIRED, already CAIP-2 validated, passed through
tempo methodDetails.chainId OPTIONAL, spec'd default 42431 tempo:{chainId}
evm methodDetails.chainId REQUIRED eip155:{chainId}

Lightning is Solana's twin. Its draft carries an identical enumerated network field with the same spec'd default and its own reject-on-mismatch clause (SHOULD rather than MUST). It was demoted to a prose footnote because its consequence is milder — the severity ranking hid a second instance of the same bug.

${method}:unknown is removed rather than forbidden. Every member of BLOCKCHAIN_CHARGE_METHODS now resolves or throws, so the sentinel has no producer left. That ordering is the point: deleting an unreachable branch cannot regress a caller, whereas turning a reachable one into an error can. Doing it the other way round would have converted Lightning from working-though-degraded into a hard failure — a regression introduced by the fix rather than found by it.

FromMppChargeOptions.expectedNetwork enforces the spec's reject-on-mismatch clause at the lift, so a caller that knows its configured network cannot forget the comparison downstream. The second parameter still accepts a bare number as the legacy now argument.

Two things this also corrects

The rejection message. hedera, usdc and nearintents are specified blockchain charge methods with real payTo fields, and the catch-all told callers they "have no payTo/asset exposed in the Charge request" — sending the reader to look for a problem that is not there. They are unmapped because their request shapes are nested (methodDetails.evm.chainId) or cross-chain, which is what the message now says.

The header's spec baseline, which was wrong in both directions: it declared one repo-wide commit while the module already implemented a change newer than that baseline and lagged the method drafts by 22 commits. A single baseline asserts uniform conformance a partial implementation never has, so citations are now per area, with the unsynced methods named.

Verification

cd typescript && pnpm vitest run test/compat-mpp-network-resolution.test.ts
  • 30 new tests, observed red against main before the fix (23 failed / 7 passed) by reverting src/ and re-running.
  • Full suite 1132 passed across 29 files · tsc --noEmit clean · pnpm build clean.
  • Every fixture is a method spec's own example, cited inline (tempoxyz/mpp-specs @ f9506cd).

The new tests live in their own file on purpose. compat-mpp.test.ts greps positive for solana — five hits — but every one is inside a parseMppAcceptPayment test and no Solana challenge had ever reached fromMppChargeChallenge. The gap was camouflaged rather than absent, which is how it survived review.

Deliberately not in this PR

  • DAN-854fromMppChargeChallenge rejects the Lightning draft's own charge example, because it requires recipient for the whole blockchain set while Lightning's spec declares it OPTIONAL ("the invoice payee is implied by the BOLT11 invoice"). Relaxing a public validation is a different risk class from resolving an identifier, and it carries a real design question — what payTo becomes for an invoice-authoritative method. Filed separately with the repro.
  • Mapping hedera / usdc / nearintents (DAN-848 criteria 3–4). usdc nests its chain id per chain family and nearintents is cross-chain with a CAIP-2 originNetwork plus a distinct destination asset, so a single network field is ambiguous for it. Neither fits the flat translation without a decision.

Refs: DAN-848

🤖 Generated with Claude Code

`resolveNetwork` read one field (`methodDetails.chainId`) and branched for two
methods, then fell back to `${method}:unknown` for everything else. Solana's
charge draft puts the cluster in `methodDetails.network` and obliges clients to
reject a challenge whose cluster differs from their configured one — so a
devnet challenge and a mainnet challenge both lifted as `solana:unknown`, and
the comparison the spec requires could not fail. The one field a client must
check was discarded into a value that names no network.

Resolution now follows the owning method spec:

- solana    `methodDetails.network` ∈ {mainnet, devnet, localnet}, default mainnet
- lightning `methodDetails.network` ∈ {mainnet, regtest, signet},  default mainnet
- stellar   `methodDetails.network` — REQUIRED, already CAIP-2, validated and passed through
- tempo     `methodDetails.chainId` — OPTIONAL, spec'd default 42431 now applied
- evm       `methodDetails.chainId` — REQUIRED, rejected when absent

Lightning is the same defect as Solana with a milder consequence, which is why
the severity ranking hid it. Its spec carries an identical enumerated `network`
field with a reject-on-mismatch clause.

`${method}:unknown` is removed rather than forbidden. Every member of
BLOCKCHAIN_CHARGE_METHODS now resolves or throws, so the sentinel has no
producer left — deleting an unreachable branch cannot regress a caller, whereas
turning a reachable one into an error can.

Adds `FromMppChargeOptions.expectedNetwork`, which enforces the spec's
reject-on-mismatch clause at the lift. The second parameter still accepts a
bare `number` as the legacy `now` argument.

Also: the catch-all rejection told callers that hedera, usdc and nearintents
"have no payTo/asset exposed in the Charge request". All three are specified
blockchain charge methods with real payTo fields; they are unmapped because
their request shapes are nested or cross-chain. The message now says that.

Header spec references are cited per area instead of as one repo-wide baseline.
The old single commit was wrong in both directions — the module already
implemented a change newer than its declared baseline while lagging the method
drafts by 22 commits.

Verification:
  cd typescript && pnpm vitest run test/compat-mpp-network-resolution.test.ts
  30 new tests, observed red against HEAD (23 failed / 7 passed) before the fix.
  Full suite 1132 passed (29 files) · tsc --noEmit clean · build clean.

Refs: DAN-848
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@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 12:25am

@Danny-Devs

Copy link
Copy Markdown
Contributor Author

⚠️ CI red here is inherited from main, not caused by this PR — DAN-860

Both open PRs fail identically: test_conformance.py unlock decode, S402Error: unlock payload requires encryptionId (string), got NoneType. Neither PR touches unlock or encryptionId, and they have nothing to do with each other.

main @ 7d50fa9 on a clean tree, with none of this branch present:

cd python && uv run --extra dev pytest -v  →  2 failed, 152 passed

main has been red since 2026-07-21 (feat(unlock): single-transaction pay-to-decrypt scheme). Nothing has merged since, so nothing caught it.

The underlying defect is larger than a failing test: the unlock scheme's Python and TypeScript field sets are disjointencryptionId/encryptedContentId/encryptionServiceId versus packageId/keyServers/threshold/contentDigest, with an empty intersection. A design change reached the conformance vectors and the TS runtime validator but not the Python implementation, docs/specification.md, or TS's own types.ts. Diagnosis and the decision it needs: DAN-860.

Deliberately not fixed inside this PR — a red-main repair riding in a feature branch is unreviewable, and it would mean neither PR's green could be trusted as evidence about itself.

This PR's own suites are green: pnpm typecheck clean, pnpm vitest run full pass, pnpm build clean, and its new tests were observed red against main before the fix.

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