105-machine-readable-endpoint-documentation.rst (62290B)
1 DD 105: Machine-Readable Endpoint and Method Documentation 2 ########################################################## 3 4 :Design status: Draft 5 :Implementation status: Not started 6 :DD shepherd: TBD 7 :Historical contributors: TBD 8 :First published: 2026-10-06 9 :Last substantive change: 2026-10-06 10 :Normative references: :doc:`../core/endpoint-documentation-requirements` 11 12 .. contents:: Table of Contents 13 :depth: 2 14 15 Summary 16 ======= 17 18 Every HTTP endpoint and every wallet-core operation in the Taler 19 documentation is described in exactly one ``.rst`` file, and every semantic 20 claim in that file that tooling could need — versioning, maturity, 21 idempotency, actor visibility, error semantics, state observation, world 22 effects, identifier roles, timers — is expressed in a directive option or 23 typed block, never only in prose. A small number of registries (entities 24 with lifecycles, timers, identifiers; per-component notification channels) 25 hold the cross-method anchors. From this corpus, tooling derives per-entity 26 state tables, per-actor visibility projections, cross-component flows, and 27 sequence diagrams, and generates client types and validators, with drift 28 against implementations caught in CI. 29 30 The format is language-agnostic: the shape layer describes wire-level types 31 (JSON messages exchanged between client and server) via the historically 32 named ``ts:def`` / ``ts:op`` directives, and generated artifacts may target 33 any implementation language. 34 35 This document records the *intent*, the *diagnostic of the current state*, 36 the *design decisions*, and the *migration plan*. The normative 37 field-by-field format, controlled vocabularies, and lint rules are 38 specified in :doc:`../core/endpoint-documentation-requirements`; where the 39 two disagree during evolution, the requirements document wins for format 40 questions and this DD wins for *why*. 41 42 Motivation 43 ========== 44 45 The documentation corpus is large (~490 per-endpoint files across exchange, 46 merchant, bank-*, corebank, challenger, taldir, mailbox, donau, auditor, 47 plus 152 wallet-core operations) and already uniform in *shape*: identity 48 directive, request, response, prose details. But the semantics a reader 49 most needs are locked in prose or absent: 50 51 * **Error codes are overloaded across flows.** A 404 on an unfunded 52 reserve means "wait"; a 404 on an expired order means "gone"; a 402 on 53 the order status page is the *normal* unpaid state. Same codes, opposite 54 meanings, not inferable. 55 * **Retry safety is never stated.** ``POST /withdraw`` is replay-safe by 56 careful design; ``POST /private/orders`` without an ``order_id`` 57 duplicates. In a system built on at-least-once networking, this is the 58 most frequent operational question a client faces, and the corpus answers 59 it only occasionally, in prose. 60 * **State lives between methods.** No per-endpoint file can honestly 61 describe the machine the endpoints collectively implement — the order 62 lifecycle, the withdrawal operation, the TAN challenge — so those 63 machines exist only implicitly, and cross-component causality (the 64 exchange's wirewatch driving a bank-owned state; the merchant tracking 65 exchange wire arrivals) exists only as narrative. 66 * **Docs and code drift silently.** Doc-versus-code audits on this corpus 67 repeatedly find stale state names, missing error codes, and endpoints 68 whose real behavior has moved on. One-sided declarations rot; the fix is 69 two-sided, machine-checkable declarations. 70 * **Deriving anything requires reading prose.** Use-case walkthroughs, 71 persona-specific API surfaces ("what can a tax auditor see?"), state 72 tables, and sequence diagrams are today hand-authored and immediately 73 stale. 74 75 The goal is a documentation corpus that is the *authoritative source of 76 truth* in the spec-first sense: implementations conform to it, code is 77 generated from its shape layer, and derived artifacts are produced without 78 reading prose. 79 80 Current state of the corpus 81 =========================== 82 83 This section is the diagnostic: what the corpus provides today, and which 84 gaps the requirements document closes. 85 86 What files contain today 87 ------------------------ 88 89 The current corpus already provides, per file: 90 91 .. list-table:: 92 :header-rows: 1 93 94 * - ID 95 - Element 96 - Encoding today 97 * - E1 98 - Method identity (HTTP method + path / op) 99 - ``.. http:get:: /p`` / ``.. ts:op:: op`` 100 * - E2 101 - One-line + prose description 102 - directive body 103 * - E3 104 - Request parameters 105 - ``:query:``, body as ``.. ts:def::`` 106 * - E4 107 - Response per status code 108 - ``:http:statuscode:`` + ``ts:def`` 109 * - E5 110 - Error codes 111 - ``TALER_EC_*`` names in status prose 112 * - E6 113 - Field-level versioning 114 - ``@since **vN**`` / ``@deprecated`` comments 115 * - E7 116 - Chapter-level version history 117 - "Version History" section of ``api-*.rst`` 118 * - E8 119 - Chapter-level auth model 120 - prose section of ``api-*.rst`` 121 * - E9 122 - Long-poll parameters 123 - ``:query timeout_ms:`` + ``:ref:`` to convention 124 * - E10 125 - Idempotency 126 - *occasionally*, prose only (e.g. ``post-withdraw.rst``) 127 * - E11 128 - Read-only / deprecated flags (wallet ops) 129 - ``:read-only:``, ``:deprecated:`` on ``ts:op`` 130 * - E12 131 - Expected errors + side effects (wallet ops) 132 - prose lists in **Details:** 133 134 Verified against the corpus (2026-10): the only options the identity 135 directives accept today are ``deprecated``/``noindex``/``synopsis`` (HTTP 136 domain) and ``read-only``/``deprecated`` (``ts:op``). None of the new 137 header options, and none of the ``entity`` / ``lifecycle`` / ``timer`` / 138 ``identifier`` / ``notification-channel`` directives, exist anywhere 139 outside the specification documents themselves — all are new 140 ``_exts/`` work. ``core/entities.rst`` does not exist. 141 ``core/wallet-core/notifications.rst`` exists, but as a flat list of 142 notification types with payload definitions — no channel, delivery, or 143 event structure — and must be restructured into the channel form. 144 145 Gaps to close 146 ------------- 147 148 .. list-table:: 149 :header-rows: 1 150 151 * - ID 152 - Missing element 153 - Why derivation needs it 154 * - G1 155 - Method maturity (stable/draft/...) 156 - drafts must be excludable from derivation 157 * - G2 158 - Per-method actor visibility 159 - persona closures are computed from who may call what 160 * - G3 161 - Error semantics 162 - same status/code is transient in one flow, terminal in another, 163 and sometimes not an error at all (progress); cannot be inferred 164 * - G4 165 - Idempotency label 166 - retries are safe or not — never guessable 167 * - G5 168 - Lifecycles with triggers and guards 169 - the entity machine: what causes each transition, and when that 170 path exists at all (config/data forks such as KYC thresholds) 171 * - G6 172 - Timers 173 - expiry/deadline edges of state tables 174 * - G7 175 - Observation channels 176 - how an actor *learns* a state changed 177 * - G8 178 - Identifier joins 179 - which values are created/carried/read/consumed where — 180 the cross-component join keys 181 * - G9 182 - Effects (structured) 183 - what changed in the world beyond the response 184 185 Structural mismatches found by verification 186 ------------------------------------------- 187 188 * **~20 files violate one-method-per-file as-is**, some deliberately: 189 ``auditor/delete-monitoring-records.rst`` carries 24 identity directives 190 (one per monitoring-record type); KYC webhook and AML transfer files 191 carry 2–4; long-poll variants (e.g. ``get-purses-PURSE_PUB-merge.rst``) 192 are documented as two directives in one file. 193 * **~12 files are proxy stanzas**, not methods: ``.. http:any::`` wildcard 194 reverse-proxy delegation declarations (``merchant/any-star.rst``, seven 195 corebank ``any-...-star.rst`` files) with no request/response semantics. 196 * **sync and terminal have no per-endpoint files at all**: 3 and 10 197 endpoints respectively live inline in ``api-sync.rst`` / 198 ``api-terminal.rst``. 199 * **KYC state is cross-cutting**: 23+ KYC-related files across six 200 components; exchange, merchant, bank-wire, and wallet all hold 201 projections, so naive single-owner modeling fails. 202 * **Wallet transaction machines** (DD37) need four lifecycle-model 203 extensions; see D10. 204 205 Requirements 206 ============ 207 208 R1 Each endpoint/method is documented in exactly one ``.rst`` file whose 209 semantic content is completely machine-readable. 210 211 R2 From the corpus, without reading prose, tooling can derive: 212 per-entity state tables (states, transitions, timers, failure exits), 213 per-actor observation projections, cross-component identifier flows, 214 and sequence diagrams (messages from transitions, ``alt`` from guards, 215 ``loop`` from timers). 216 217 R3 The shape layer (``ts:def`` types, request/response schemas) is 218 complete enough to generate client/server types and validators; drift 219 is caught by CI in the implementation repositories. 220 221 R4 Documented *intent* (drafts, unimplemented proposals) is visibly and 222 mechanically distinguishable from documented *behavior*, and excluded 223 from derivation. 224 225 R5 The format must cover the existing corpus without exceptions that 226 swallow rules: HTTP APIs, wallet-core operations, long-poll dialects, 227 non-2xx progress signals, capability-gated access, config/data forks 228 (KYC thresholds), and wallet transaction lifecycles per DD37. 229 230 R6 Every cross-reference a lint can check must be resolvable: entity 231 names, state values, timers, identifiers, trigger targets, channel 232 events. Free strings are lint errors. 233 234 R7 Migration is incremental: the current corpus is mostly compliant in 235 shape, and backfill proceeds slice by slice (bank-integrated withdrawal 236 first), with the lint warn-only until a slice is done. 237 238 Proposed Solution 239 ================= 240 241 The full format is normatively specified in 242 :doc:`../core/endpoint-documentation-requirements`. This section states 243 the design as a sequence of decisions and their reasons. 244 245 D1: Derive edges, declare anchors 246 --------------------------------- 247 248 The organizing philosophy. Anything that is an *edge* — a transition, an 249 arming, an observation, a carriage of a value — is written exactly once, on 250 the edge itself (the lifecycle transition, the method header, the effect, 251 the identifier tag). Anything that acts as a *resolution anchor* — an 252 entity, a timer name, an identifier, a notification channel — is declared 253 once, minimally, so edges can be checked against it. If a fact can be 254 derived from edges it is never also declared; if a name must be resolvable 255 it is never left implicit. 256 257 This is what keeps the system from collapsing under its own bookkeeping: 258 timers are slim anchors (name + prose) because their armed-by, exposed-in, 259 fires-to, and gates facts are all derivable from timer-refs, error entries, 260 and lifecycle triggers; identifiers are name + ``:capability:`` + prose 261 because their created-by/carried-by/read-by/consumed-by story is the union 262 of method-side tags. The alternative — restating everything at the anchor — 263 guarantees drift. 264 265 D2: One file per method, machine-readable header 266 ------------------------------------------------ 267 268 Each method's identity card is its existing directive (``.. http:get::`` / 269 ``.. ts:op::``) extended with options: version (``:since:`` / 270 ``:deprecated:``), maturity, idempotency, actor visibility, observation 271 binding, timer relationships, and long-poll dialect. Every option exists 272 because a named consumer needs it (lint, generator, exclusion filter); none 273 is decorative. 274 275 **Decision: the maturity option is named ``:maturity:``, not 276 ``:status:``.** The vendored sphinxcontrib-httpdomain in ``_exts/`` 277 already accepts ``:status:`` as a field-name alias for status codes 278 (``names=('statuscode','status','code')``); a new header option with that 279 name collides. 280 281 **Decision: file-naming and one-directive-per-file rules get two explicit 282 exemptions, not silent violations.** 283 284 * *Variant groups*: a small number of files legitimately document several 285 directive instances of one logical method 286 (``auditor/delete-monitoring-records.rst`` with 24 record types, KYC 287 webhook provider variants, long-poll variants such as 288 ``exchange/get-purses-PURSE_PUB-merge.rst``). These are declared 289 variant groups and linted as one method with variants, rather than 290 being split into near-identical files. 291 * *Proxy stanzas*: ~12 files using ``.. http:any::`` with wildcard paths 292 (``merchant/any-star.rst``, the corebank ``any-...-star.rst`` reverse- 293 proxy delegation declarations) are not methods with request/response 294 semantics. They get their own minimal stanza class and are excluded 295 from request/response/idempotency requirements. 296 297 **Decision: sync and terminal are split.** Both APIs currently live inline 298 in their chapter files (3 and 10 endpoints). They are split into 299 per-endpoint files like every other API; chapter files keep only version 300 history and shared conventions. 301 302 D3: Errors carry temporality and recovery 303 ----------------------------------------- 304 305 Every non-2xx outcome — including codes that are *not failures* (202 306 pending, 304 unchanged, 402 unpaid) — gets a structured entry with 307 ``:temporality:`` (transient-state / terminal / fixable / rate-limited / 308 gate / progress) and ``:recovery:`` (retry-same / retry-modified / 309 retry-after / long-poll / poll-later / refresh-config / start-kyc / 310 user-action / abort / give-up / none). This pair is what makes the failure 311 and timer columns of derived state tables computable instead of guessed, 312 and what tells the generator when *not* to derive a failure edge 313 (``progress`` entries are state signals, not errors). 314 315 D4: Idempotency is a first-class label 316 -------------------------------------- 317 318 ``readonly`` / ``replay-safe`` / ``keyed:<field>`` / ``unsafe``. Retries 319 are the universal response to network failure, and a request that 320 arrived-but-lost-its-response is indistinguishable from one that never 321 arrived; whether a verbatim replay is safe decides whether the client's 322 failure branch exists at all. The label also flags design smells: an 323 ``unsafe`` mutating method in this ecosystem deserves review, and the 324 lint requires the field on every mutating method. 325 326 D5: Authorization is one fact: the actor 327 ---------------------------------------- 328 329 ``:visible-to:`` declares the actor surface (wallet-user, 330 merchant-staff[:scope], aml-officer[:readonly], exchange-operator, auditor, 331 tax-auditor, bank-customer, bank-admin, kyc-provider, any-unauthenticated, 332 external), and the credential mechanism is *implied* by a fixed table. 333 Declaring both actor and mechanism would duplicate one fact in two 334 drift-prone places. Capability-gated access (unguessable identifiers such 335 as ``wtid`` or ``withdrawal-id``) is annotated in parentheses and resolved 336 against the identifier registry, which is where the ``:capability:`` 337 property of the *value* lives. Persona projections are computed as 338 closures over this field. 339 340 D6: Methods bind to entity machines in exactly two ways 341 ------------------------------------------------------- 342 343 *Mutators* declare transitions in an **Effects** block (entity, exact edge, 344 observation endpoint). *Readers* declare an observation binding 345 (``:observes-state: <entity>.<field>``) stating that the response *is* a 346 view onto that entity's machine. Both are checked two-sided against the 347 entity registry: the lifecycle says "call X drives A→B", the method says 348 "I drive A→B on entity E", and disagreement is a lint error. One-sided 349 declarations silently rot; that is the lesson of every doc-versus-code 350 audit on this corpus. 351 352 D7: Identifiers are the join keys 353 --------------------------------- 354 355 Methods tag their role in each identifier's life: ``creates`` / ``carries`` 356 / ``reads`` / ``consumes``. The union over ~490 files yields the 357 cross-component value-flow graph and the lint's join checks (exactly one 358 creator per carried/read value, or an internal-creation note at the anchor 359 for values like ``wtid`` minted by the exchange's aggregator). The 360 registry holds only the anchor: name, ``:capability:``, prose. 361 362 D8: Entities, lifecycles, triggers, guards 363 ------------------------------------------ 364 365 The entity registry declares, per entity: ``:owner:`` (the component 366 holding *authoritative* state and executing transitions — not merely where 367 the entity is displayed), a transition list, a declared terminal set, timer 368 anchors, and identifier anchors. States are *inferred* from transitions; 369 terminal states are *declared* so that use-case success conditions are 370 anchored, the absorbing property is checkable, and truncated machines are 371 visible (a non-terminal state with no outgoing edges is a lint warning). 372 373 Every transition names its *trigger* — ``call <method>``, ``timer 374 <entity>.<timer>``, ``watch <component>.<observable>``, or ``external 375 <description>`` — and optionally a guard (``:if:``) for config/data forks 376 (KYC on/off, thresholds, feature flags). Triggers are the causality layer 377 no per-endpoint doc can see locally; guards keep one canonical machine with 378 guarded edges instead of a combinatorial family of near-identical machines. 379 380 D9: Notifications live on per-component pages 381 --------------------------------------------- 382 383 How an actor *learns* that state changed is declared once per component: 384 channel, registration, delivery semantics (``lossy-hint`` / 385 ``at-least-once`` / ``rendezvous`` / honestly-``unspecified``), events, and 386 — critically — the *authoritative read* backing each event family, per the 387 doctrine *signals are hints; reads are truth*. Method files never declare 388 push channels; the link from effect to signal is derived. Long-poll is not 389 a notification channel: it is a property of the read endpoint (``:wait:``), 390 since a long-poll response is authoritative state delivered late, not a 391 hint. Components with no push channels (the exchange) declare that 392 negative fact explicitly. 393 394 D10: Wallet transaction lifecycles need four model extensions 395 ------------------------------------------------------------- 396 397 The wallet-core variant maps HTTP concepts onto ``ts:op`` files, with 398 effects expressed as transitions on per-transaction-type entities 399 (``transaction-withdrawal``, ``transaction-payment``, ...) whose state 400 vocabulary is DD37's ``major[:minor][/working]``. Verification against 401 DD37 and the wallet-core implementation surfaced four places where the 402 base lifecycle model does not fit; each gets an explicit extension rather 403 than a workaround: 404 405 **D10.1 Final states may be reactivatable.** DD37 ``done`` is not 406 absorbing: a payment is reactivated by refund queries and 407 ``rebind-session``; a deposit in ``finalizing:track`` can fall back to 408 ``pending`` when new KYC requirements appear. The lifecycle model 409 therefore distinguishes ``:terminal:`` (absorbing) from 410 ``:final-reactivatable:`` (success anchor for use cases, but reactivation 411 edges are legal and must be listed). The absorbing lint applies only to 412 ``:terminal:``. 413 414 **D10.2 ``/working`` and the suspended shadow states are orthogonal 415 modifiers, not states.** DD37's public state value is a tuple 416 ``(major, minor, working)``; ``working`` is a transitional presentation 417 flag explicitly slated to become major states later, and nearly every 418 non-terminal state has paired ``suspended`` / ``suspended-aborting`` / 419 ``suspended-finalizing`` shadow states plus retry self-loops that DD37 420 deliberately omits from its diagrams. Encoding all of this as literal 421 states and transitions would explode every table. Instead, wallet 422 transaction entities declare: 423 424 * ``working`` as a lifecycle-level orthogonal flag (same mechanism as the 425 order entity's ``:modifiers:``); 426 * a ``:standard-edges: suspend-retry`` declaration that *implies* the 427 suspend/resume/retry edge family on every non-final state, so the 428 inferred state set and edge set include them without authoring them. 429 430 State values in ``:transition:`` fields use DD37 notation, and DD37's 431 state-*expression* language (``pending:*``, ``pending:-``, ``/idle``, 432 comma alternation) is accepted wherever a state set is matched. 433 434 **D10.3 Ownership is inside-out, and that is fine.** A wallet 435 transaction entity is owned by wallet-core (it holds the authoritative 436 record and executes transitions), but its triggers are almost entirely 437 ``watch <component>.<observable>`` (bank withdrawal operations, exchange 438 reserves/KYC, merchant refunds, *another wallet* in p2p) plus local user 439 actions and timers — the inverse of the tan-challenge reference example 440 where the owner executes on ``call``. This is not a violation of the 441 owner rule but its second mode, and the worked examples below include a 442 watch-driven wallet entity alongside call-driven bank entities. 443 Corollaries: one logical p2p transfer is two entities (debit and credit) 444 with no shared machine ("public states describe each wallet's local 445 progress", DD37), and hidden child transactions (``internal-withdrawal``) 446 are separate entities, not states of the parent. 447 448 **D10.4 Some truths are not derivable from the machine.** Three are 449 declared as explicit non-goals of derivation for wallet transactions: 450 451 * ``txActions`` are computed per state *and* context and are **not** 452 derivable from the lifecycle (DD37: frontends must use the returned 453 ``txActions``). No "possible actions" column is generated. 454 * ``deleted`` is notification-only, never a stored state; its observation 455 is the *absence* of the record (``WALLET_TRANSACTION_NOT_FOUND``). The 456 observation model gains the notion "absence as signal" for exactly this 457 case (and wallet-core's ``none`` pseudo-state appearing in transition 458 notifications). 459 * Internal states are intentionally collapsed on exposure (e.g. 460 redenomination phases report as ``pending:withdraw/working``). The 461 registry describes the *public* machine; the mapping from internal 462 status enums is implementation detail, checked by CI drift detection, 463 not authored in docs. 464 465 D11: DD37's tables are generated, not mirrored 466 ---------------------------------------------- 467 468 The registry entries for wallet transaction entities become the canonical 469 machine-readable form of DD37's machines, and DD37's per-type state tables 470 are *generated from the registry* (with DD37 remaining the human-oriented 471 prose and diagrams). Re-encoding DD37 by hand in the registry would create 472 the very two-sided drift problem this system exists to eliminate. The 473 lint's wallet rule resolves state *names* against the 474 ``get-transaction-by-id.rst`` enums and state *edges* against the registry. 475 476 D12: Lint as build checks, warn-only during migration 477 ----------------------------------------------------- 478 479 A doc lint in ``taler-docs/_exts/`` enforces the rules (identity, 480 vocabulary, resolvability, two-sided lifecycle agreement, identifier joins, 481 lifecycle integrity, filename/location) as Sphinx build warnings/errors. 482 New rules land warn-only per slice and flip to error when the slice is 483 backfilled. 484 485 D13: KYC/AML is one exchange-owned entity; decisions are data, not states 486 ------------------------------------------------------------------------- 487 488 KYC is the hardest entity to model because its state is genuinely 489 cross-cutting: the wallet blocks transactions on it, the merchant shows a 490 status page for it, the bank carries its wire transfers, the auditor 491 observes its holds, and AML officers act on it. Verification against the 492 exchange's actual schema (DD23) shows that the authoritative state lives 493 entirely in the exchange: per wire target (``h_payto``), the exchange holds 494 the access token, the currently triggered legitimization measures, the 495 active decision/rule-set ledger, and the external provider processes. 496 Everything else — the wallet's ``ExchangeWalletKycStatus`` and transaction 497 minors, the merchant's ``MerchantAccountKycRedirect.status`` enum, the 498 KYC SPA's requirement list, the auditor's AML-hold view — is a projection, 499 named as such under the owner (per the owner rule's projection clause). 500 501 **Decision: one entity, ``kyc-account``, owned by the exchange.** Its 502 observable surface is ``GET /kyc-check/$H_NORMALIZED_PAYTO`` (200 = no 503 mandatory action, 202 = redirect user to legitimization, 409 = account not 504 yet bound to a public key, i.e. KYC-auth wire transfer required) plus the 505 ``aml_review`` flag, the monotonic ``rule_gen`` counter (the long-poll 506 rendezvous), and the access token that authorizes the SPA. Its states are 507 client-meaningful, not DB-meaningful: ``clean``, ``auth-required``, 508 ``action-required`` (measures open), ``in-process`` (external provider 509 engaged), ``under-review`` (``aml_review``). There are no terminal states: 510 a bank account never stops existing; dormancy and closure are modifiers. 511 512 **Decision: officer decisions and rule sets are guards and modifiers, not 513 states.** An AML officer decision (``POST /aml/$OFFICER_PUB/decision``) 514 does four things: installs a new ``LegitimizationRuleSet`` (which *changes 515 which transitions exist* — hence ``:if:`` guards on threshold rules, not 516 new states), sets account properties (``is_frozen``, ``was_reported``, 517 ``pep``, ``high_risk`` — declared as ``:modifiers:`` on ``kyc-account``), 518 triggers immediate measures (a ``call`` trigger into ``action-required``), 519 and sets ``keep_investigating`` (the ``under-investigation`` modifier). 520 This is exactly how the implementation works (the active row of 521 ``legitimization_outcomes`` is superseded, not stacked): decisions rewrite 522 the machine's guard data. The concurrency guard of the API (409 523 ``AML_DECISION_MORE_RECENT_PRESENT``) is an ordinary ``fixable`` error with 524 ``retry-modified`` recovery (re-read, then re-decide). Officer 525 provisioning (``POST /management/aml-officers``, master-key signed) is the 526 ``exchange-operator`` actor acting on the officer registry — separate 527 entity, not part of ``kyc-account``. 528 529 **Decision: projections may be restricted or lossy, and that is 530 declared.** The KYC SPA's projection is *deliberately* restricted (the 531 law may forbid disclosing true status; ``aml_review`` is only a hint), so 532 ``:observes-state:`` on the SPA endpoints is annotated as restricted. The 533 merchant's status enum mixes entity states (``kyc-required``, 534 ``awaiting-aml-review``) with observation failures (``exchange-unreachable``, 535 ``logic-bug``); the lint resolves only the former against the lifecycle — 536 projection-local failure members are declared at the observing method, not 537 added to the entity's machine. The wallet deliberately conflates 538 "user must act" with "staff review in progress" in ``pending:kyc``; the 539 docs must not model it as user-actionable only. 540 541 **Decision: Challenger is a separate entity, plugged in by trigger.** An 542 address-validation process (Challenger or another provider) has its own 543 machine (``setup → challenged → solved/failed/expired``, OAuth-flavored), 544 owned by the provider component. It never decides KYC: its outcome enters 545 ``kyc-account`` via ``watch kyc-provider.webhook`` / ``call 546 GET /kyc-proof/$PROVIDER`` triggers. Provider processes have their own 547 expiration timers, distinct from the account's rule-set expiration. 548 549 Worked examples 550 =============== 551 552 These examples show the target format applied to real corpus content. 553 They illustrate the requirements document; they are not themselves 554 normative additions to it. 555 556 A compliant method file 557 ----------------------- 558 559 ``core/bank-integration/get-withdrawal-operation-WITHDRAWAL_ID.rst`` 560 brought into compliance — added structure marked with ``※``: 561 562 .. code-block:: rst 563 564 .. http:get:: /withdrawal-operation/$WITHDRAWAL_ID 565 :since: v1 566 :maturity: stable 567 :idempotency: readonly 568 :observes-state: withdrawal-operation.status ※ 569 :visible-to: wallet-user, bank-customer (capability withdrawal-id) 570 :wait: long-poll(timeout_ms, old_state) ※ 571 572 Query information about a withdrawal operation, identified by the 573 ``WITHDRAWAL_ID``. 574 575 **Request:** 576 577 :query timeout_ms: *Optional.* 578 Timeout in milliseconds, for :ref:`long-polling <long-polling>`, 579 to wait for operation state to be different from ``old_state``. 580 Since protocol **v3**. 581 :query old_state: 582 *Optional.* Defaults to "pending". 583 :query long_poll_ms: *Optional.* 584 Deprecated in protocol **v3**. Use *timeout_ms* instead. 585 586 **Response:** 587 588 :http:statuscode:`200 OK`: 589 :schema: BankWithdrawalOperationStatus ※ 590 The withdrawal operation is known to the bank, and details 591 are given in the `BankWithdrawalOperationStatus` response body. 592 593 **Errors:** ※ 594 595 :error 404 none: 596 :temporality: terminal 597 :recovery: give-up 598 The operation was not found. (A not-yet-propagated operation 599 id may also 404 briefly; clients that just received the id from 600 a ``taler://withdraw`` URI may retry briefly.) 601 602 **Identifiers:** ※ 603 604 :identifier withdrawal-id: 605 :role: reads 606 :registry: :ref:`identifier-withdrawal-id` 607 The lookup key of the operation, conveyed via 608 ``taler://withdraw``. 609 :identifier reserve_pub: 610 :role: reads 611 :registry: :ref:`identifier-reserve-pub` 612 Wallet-selected reserve public key, returned here as part of 613 the operation state. 614 615 **Details:** 616 617 .. ts:def:: BankWithdrawalOperationStatus 618 ... (unchanged) ... 619 620 Notes on the example: 621 622 * The 404 entry shows the G3 problem and its resolution: the *same* 623 code is terminal for a stale id and transient for a not-yet-visible 624 one — the condition is documented, the default classification is 625 explicit. 626 * ``:visible-to:`` carries the only authorization fact: wallet user and 627 bank customer, holding the unguessable ``withdrawal-id``. The 628 mechanism (no credentials, capability in the URL) follows from the 629 actor vocabulary and the registry entry — no separate auth field. 630 * ``:wait:`` acknowledges long-polling and names its parameters (the 631 lint checks them against the ``:query:`` list). There is no push 632 channel for this entity; had there been one, it would be declared in 633 the bank's notification page, not here. 634 * The withdrawal operation's missing expiry timer is recorded once, at 635 the entity (next example) — not here, and not scattered. 636 637 Entity, timer, and identifier anchors 638 ------------------------------------- 639 640 ``withdrawal-operation`` (owner: bank): 641 642 .. code-block:: rst 643 644 .. entity:: withdrawal-operation 645 :owner: bank (libeufin/corebank; exposed via the 646 bank-integration API and reused by the terminal API) 647 648 A bank-side operation coordinating one withdrawal: exchange and 649 reserve selection by the wallet, confirmation by the account 650 owner, wire transfer to the exchange. 651 652 .. lifecycle:: 653 :terminal: aborted, confirmed 654 655 :transition none -> pending: 656 :trigger: call POST /accounts/$USERNAME/withdrawals 657 :trigger: external bank customer creates the operation in 658 the bank UI 659 The operation exists; the ``withdrawal-id`` is distributed 660 via a ``taler://withdraw`` URI. 661 :transition pending -> selected: 662 :trigger: call POST /withdrawal-operation/$WITHDRAWAL_ID 663 The wallet submits its selection (exchange, ``reserve_pub``). 664 :transition selected -> confirmed: 665 :trigger: call POST .../withdrawals/$WITHDRAWAL_ID/confirm 666 :trigger: external account owner confirms (2FA) in the 667 bank UI 668 The bank registers the transfer; the exchange observes the 669 funded reserve through its incoming wire history 670 (``reserve_pub`` as wire subject). 671 :transition pending|selected -> aborted: 672 :trigger: call POST /withdrawal-operation/$WITHDRAWAL_ID/abort 673 :trigger: external bank-side abort 674 409 ``CONFIRM_ABORT_CONFLICT`` if already confirmed — the 675 absorbing property of ``confirmed`` in action. 676 677 .. timer:: operation-expiration 678 ⚠ not yet specified: no documented expiration exists today. 679 Recorded here as the single visible gap instead of silence 680 across all method files. 681 682 .. identifier:: withdrawal-id 683 :capability: yes (unguessable; possession authorizes) 684 Bank-generated operation id, distributed via 685 ``taler://withdraw`` URIs. Created by 686 ``POST /accounts/$U/withdrawals`` or the bank UI; carried and 687 read across bank, wallet and exchange endpoints (derived from 688 the method files' **Identifiers:** blocks). 689 690 .. identifier:: reserve_pub 691 :capability: yes 692 Wallet-generated reserve key; becomes the wire subject of the 693 funding transfer and names the reserve at the exchange. 694 695 ``tan-challenge`` (owner: corebank) is the reference example of a fully 696 specified entity: declared terminal set, trigger-typed transitions (call, 697 timer, guarded call), and two slim timer anchors whose structural facts 698 (fires-to, armed-by, exposed-in, gates) are all derived from transitions, 699 timer-refs and error entries — see the requirements document, §4. 700 701 ``order`` (owner: merchant backend) — implicit machine made explicit: 702 703 .. code-block:: rst 704 705 .. entity:: order 706 :owner: merchant backend 707 708 .. lifecycle:: 709 :terminal: wired 710 :modifiers: refunded (any state at or after paid) 711 712 :transition none -> unpaid: 713 :trigger: call POST /private/orders 714 :trigger: call POST /templates/$TEMPLATE_ID 715 Order created; a wallet can now claim it. 716 :transition unpaid -> claimed: 717 :trigger: call POST /orders/$ORDER_ID/claim 718 The wallet binds its nonce; contract terms signed. 719 :transition claimed -> unpaid: 720 :trigger: call POST /orders/$ORDER_ID/unclaim 721 The claim is released; another wallet may claim. 722 :transition claimed -> paid: 723 :trigger: call POST /orders/$ORDER_ID/pay 724 :if: coins cover the amount and any age/token requirements 725 Coins deposited; fulfillment unlocked. 726 :transition paid -> wired: 727 :trigger: watch exchange.outgoing-wire-arrivals 728 :trigger: call POST /private/transfers 729 The exchange aggregates and wires the funds; the merchant 730 confirms the bank credit and the backend reconciles. 731 732 .. timer:: pay-deadline 733 After the deadline the order can no longer be paid. 734 (Armed by order creation's ``pay_deadline`` and exposed in 735 order status responses — both derived from timer-refs.) 736 737 .. timer:: refund-deadline 738 Closes the refund window; wallet refund pickup returns 410 739 afterwards. (Arming, exposure and gating all derived.) 740 741 .. identifier:: order-id 742 :capability: yes 743 The public status/payment page is authorized by the 744 unguessable order id (plus claim token); management views 745 require bearer scopes instead — two different access 746 classes over the same value. 747 748 Note that ``:modifiers:`` records orthogonal boolean flags 749 (``refunded``, ``confirmed``) that the API exposes alongside the main 750 chain — they extend the state space without entering the transition 751 graph. This is the same mechanism wallet transaction entities use for 752 ``working`` and the suspended shadow states (D10.2). 753 754 Notification pages 755 ------------------ 756 757 Wallet (``core/wallet-core/notifications.rst``, restructured from today's 758 flat list into channel form): 759 760 .. code-block:: rst 761 762 .. notification-channel:: wallet-message 763 :component: wallet-core 764 :registration: implicit (clients of the wallet-core message 765 protocol receive notifications) 766 :delivery: lossy-hint 767 :events: transaction-state-transition, exchange-state-transition, 768 balance-change, withdrawal-transition, ... 769 770 All notifications are hints, not authoritative state 771 (``api-wallet-core.rst``): the authoritative read is the 772 corresponding getter — e.g. ``transaction-state-transition`` -> 773 ``getTransactionById``; ``balance-change`` -> ``getBalances``. 774 775 Merchant (to be extracted from ``api-merchant.rst``): 776 777 .. code-block:: rst 778 779 .. notification-channel:: webhook 780 :component: merchant 781 :registration: POST /private/webhooks 782 :delivery: unspecified ⚠ retry/ordering/duplication semantics 783 are not documented today 784 :events: order_created, pay, refund, order_settled, 785 category_created, category_update, category_delete, 786 inventory_update, inventory_product_created, ... 787 788 Authoritative reads: order events -> 789 ``GET /private/orders/$ORDER_ID``; inventory events -> 790 ``GET /private/products/$PRODUCT_ID``. 791 792 Exchange (honest negative declaration): 793 794 .. code-block:: rst 795 796 .. notification-page:: exchange 797 798 The exchange offers **no** push notification channels. All 799 observation is by pull; several status endpoints accept long-poll 800 parameters (``:wait: long-poll(...)`` in their method files), e.g. 801 ``GET /reserves/$RESERVE_PUB``, ``GET /purses/$PURSE_PUB/merge``, 802 ``GET /kyc-info/$ACCESS_TOKEN``. 803 804 KYC account (owner: exchange) — the D13 machine 805 ----------------------------------------------- 806 807 .. code-block:: rst 808 809 .. entity:: kyc-account 810 :owner: exchange (exposed via /kyc-check, /kyc-info, /kyc-spa and 811 the /aml/$OFFICER_PUB/... officer API; projected into the merchant's 812 /private/kyc status, the wallet's ExchangeWalletKycStatus and 813 transaction minors, and the auditor's AML-hold view) 814 815 KYC/AML state of one bank account (wire target) at one exchange. 816 There are no terminal states; the account exists as long as the 817 exchange knows it. Officer-set properties (frozen, reported, pep, 818 high-risk) and investigation status are modifiers. 819 820 .. lifecycle:: 821 :terminal: (none) 822 :modifiers: frozen, reported, pep, high-risk, under-investigation 823 824 :transition none -> clean: 825 :trigger: watch bank.incoming-wire-arrivals 826 :trigger: call <any exchange operation naming the account> 827 The exchange learns the account exists; default rules apply. 828 :transition clean -> auth-required: 829 :trigger: call <any guarded operation, e.g. POST /withdraw> 830 :if: /keys.kyc_enabled and no KYC-auth wire transfer has 831 bound the account to a public key 832 The operation is denied (409 AUTHORIZATION_KEY_UNKNOWN or 833 451 with bad_kyc_auth); the client must send a KYC-auth 834 transfer whose subject carries the account public key. 835 :transition auth-required -> clean: 836 :trigger: watch bank.incoming-kycauth-wires 837 The KYC-auth transfer binds target_pub; /kyc-check becomes 838 usable for this account. 839 :transition clean -> action-required: 840 :trigger: watch bank.incoming-wire-arrivals 841 :trigger: call POST /kyc-wallet (balance threshold pre-announcement) 842 :trigger: call POST /aml/$OFFICER_PUB/decision (new_measures) 843 :if: the active rule set's threshold for the operation type 844 (WITHDRAW/DEPOSIT/MERGE/BALANCE/CLOSE/AGGREGATE/REFUND/ 845 TRANSACTION) is crossed within its timeframe; "verboten" 846 measures deny instead of gating 847 The triggering operation is denied with 451 848 LegitimizationNeededResponse; the wallet enters pending:kyc. 849 :transition action-required -> in-process: 850 :trigger: call POST /kyc-start/$ID (via the KYC SPA) 851 An external provider process is created with its own 852 expiration. 853 :transition in-process -> action-required: 854 :trigger: timer kyc-account.process-expiration 855 :trigger: watch kyc-provider.webhook (user-failure / 856 provider-failure) 857 :transition in-process -> clean: 858 :trigger: watch kyc-provider.webhook (success) 859 :trigger: call POST /kyc-upload/$ID (form evidence) 860 :trigger: call GET /kyc-proof/$PROVIDER (OAuth redirect) 861 The AML program evaluates the outcome and installs a new 862 rule set; the blocked operation may now be retried. 863 :transition action-required|in-process -> under-review: 864 :trigger: watch aml-program.outcome (to_investigate) 865 :trigger: call POST /aml/$OFFICER_PUB/decision 866 (keep_investigating) 867 Staff review; clients only see aml_review=true and wait. 868 :transition under-review -> clean: 869 :trigger: call POST /aml/$OFFICER_PUB/decision 870 The officer's new rule set supersedes the active outcome. 871 :transition clean -> action-required: 872 :trigger: timer kyc-account.rule-set-expiration 873 :if: a successor measure is configured 874 Rule-set expiry re-triggers legitimization. 875 876 .. timer:: rule-set-expiration 877 LegitimizationRuleSet.expiration_time of the active outcome; 878 fires into the successor measure (or default rules). 879 .. timer:: process-expiration 880 Per external provider process; armed by /kyc-start, fires 881 in-process -> action-required. 882 .. timer:: kyc-auth-subject-expiration 883 Validity of KYC-auth wire instructions (merchant kycauth 884 response field ``expiration``). 885 .. timer:: wallet-threshold-expiration 886 WalletKycCheckResponse.expiration_time; the wallet's granted 887 balance threshold lapses and the wallet re-checks. 888 889 .. identifier:: kyc-access-token 890 :capability: yes 891 Bearer of the SPA session; issued per wire target, distributed 892 via /kyc-check responses to authenticated clients. 893 .. identifier:: account-pub 894 :capability: no (bound by KYC-auth wire transfer) 895 The public key a KYC-auth transfer binds to the account. 896 897 Notes: 898 899 * ``GET /kyc-check`` carries ``:observes-state: kyc-account.status``; 900 its ``min_rule`` / ``lpt`` parameters are the long-poll dialect 901 (``:wait: long-poll(timeout_ms, min_rule, lpt)``), and ``rule_gen`` is 902 the monotonic rendezvous counter. 903 * The officer decision endpoint is ``:idempotency: unsafe`` in 904 appearance but guarded: 409 ``AML_DECISION_MORE_RECENT_PRESENT`` 905 (``:temporality: fixable``, ``:recovery: retry-modified``) forces the 906 officer to re-read the ledger before re-deciding. 907 * Challenger's address validation is a separate entity 908 (``address-validation``, owner: challenger) with states 909 ``setup / challenged / solved / failed / expired``; its OAuth code is 910 consumed by ``POST /token`` and its outcome reaches ``kyc-account`` 911 only through the ``watch kyc-provider.webhook`` trigger. 912 913 Flow: bank-integrated withdrawal 914 -------------------------------- 915 916 Entities in play: ``withdrawal-operation`` (bank, shown above), 917 ``reserve`` (exchange), ``transaction-withdrawal`` (wallet-core). Join 918 key: ``reserve_pub`` — created by the wallet (``confirmWithdrawal`` / 919 ``acceptManualWithdrawal``), carried to the bank by 920 ``POST /withdrawal-operation/$WITHDRAWAL_ID``, becomes the wire subject 921 (read by the exchange's wirewatch), read by ``GET /reserves/$RESERVE_PUB`` 922 and ``POST /withdraw``. 923 924 .. code-block:: rst 925 926 .. entity:: reserve 927 :owner: exchange 928 929 Funds pooled under a reserve public key by an incoming wire 930 transfer; drained by withdrawals. 931 932 .. lifecycle:: 933 :terminal: closed 934 935 :transition none -> open: 936 :trigger: watch bank.incoming-wire-arrivals 937 The wire subject is the reserve_pub. There is no create 938 call: the wallet long-polls GET /reserves/$RESERVE_PUB 939 (404 transient-state, recovery long-poll) until the wire 940 arrives. 941 :transition open -> closed: 942 :trigger: call POST /reserves/$RESERVE_PUB/close 943 :trigger: timer reserve.expiration 944 Residual value is wired back to last_origin. 945 946 .. timer:: expiration 947 Armed by the funding wire (per exchange configuration); 948 exposed in ReserveSummary.reserve_expiration. 949 950 .. entity:: transaction-withdrawal 951 :owner: wallet-core 952 953 The wallet's withdrawal transaction group (DD37), here the 954 bank-integrated variant. Note the D10 extensions: working as a 955 modifier, implied suspend/retry edges, and watch-driven triggers. 956 957 .. lifecycle:: 958 :terminal: done, failed, aborted 959 :modifiers: working 960 :standard-edges: suspend-retry 961 962 :transition none -> dialog:proposed: 963 :trigger: call prepareBankIntegratedWithdrawal 964 The taler://withdraw URI is resolved against the 965 bank-integration API. 966 :transition dialog:proposed -> pending:bank-register-reserve/working: 967 :trigger: call confirmWithdrawal 968 The wallet creates reserve_pub and registers it with the 969 bank (POST /withdrawal-operation/$WITHDRAWAL_ID). 970 :transition pending:bank-register-reserve -> pending:bank-confirm-transfer: 971 :trigger: watch bank.withdrawal-operation.status (= selected) 972 :transition pending:bank-confirm-transfer -> pending:exchange-wait-reserve: 973 :trigger: watch bank.withdrawal-operation.status (= confirmed) 974 The account owner confirmed; the bank wires the funds. 975 :transition pending:exchange-wait-reserve -> pending:withdraw/working: 976 :trigger: watch exchange.reserve (GET /reserves/$RESERVE_PUB 977 long-poll returns 200) 978 :transition pending:withdraw -> done: 979 :trigger: internal coins withdrawn (POST /withdraw replay-safe) 980 :transition pending:* -> pending:kyc: 981 :trigger: call <guarded operation> 982 :if: exchange signals legitimization (451 / 983 wallet_balance_limit_without_kyc) 984 :transition pending:kyc -> pending:*: 985 :trigger: watch exchange.kyc-account (GET /kyc-check long-poll) 986 :transition pending:bank-confirm-transfer -> aborting:bank/working: 987 :trigger: call abortTransaction 988 :transition aborting:bank -> aborted:bank: 989 :trigger: watch bank.withdrawal-operation.status (= aborted) 990 A 409 from the bank abort means it confirmed concurrently — 991 the standard-edges retry path returns to exchange-wait-reserve. 992 993 Flow: payment from a template 994 ----------------------------- 995 996 Entities in play: ``order`` (merchant, shown above — note its 997 ``none -> unpaid`` transition already lists ``call POST 998 /templates/$TEMPLATE_ID`` as a second trigger), ``transaction-payment`` 999 (wallet-core). Identifiers: ``order-id`` (created by the template 1000 instantiation), the claim ``nonce`` (created by the wallet at claim, 1001 consumed at pay), ``h_contract_terms`` (derived from the claim response, 1002 carried into every later public call and into the exchange's deposit). 1003 1004 The template itself is a static merchant resource, not a lifecycle 1005 entity: ``POST /private/templates`` and its siblings are CRUD with no 1006 machine, and say so (``:idempotency: keyed:template_id`` and no Effects). 1007 1008 .. code-block:: rst 1009 1010 .. entity:: transaction-payment 1011 :owner: wallet-core 1012 1013 DD37 payment transaction. Demonstrates :final-reactivatable:: 1014 a done payment is reactivated by explicit refund queries. 1015 1016 .. lifecycle:: 1017 :terminal: failed, aborted, expired 1018 :final-reactivatable: done 1019 :modifiers: working 1020 :standard-edges: suspend-retry 1021 1022 :transition none -> dialog:proposed: 1023 :trigger: call preparePayForTemplateV2 1024 Instantiates the template at the merchant (public 1025 POST /templates/$TEMPLATE_ID): order none -> unpaid. 1026 :transition dialog:proposed -> pending:submit-payment/working: 1027 :trigger: call confirmPay 1028 The wallet claims the order (nonce bound) and signs. 1029 :transition pending:submit-payment -> done: 1030 :trigger: watch merchant.order (POST /orders/$ORDER_ID/pay 200) 1031 :transition done -> pending:check-refund/working: 1032 :trigger: call startRefundQuery 1033 :trigger: timer order.auto-refund (auto-refund probing) 1034 Reactivation edge out of a final-reactivatable state. 1035 :transition pending:check-refund -> done: 1036 :trigger: watch merchant.order.refunds (GET /orders/$ORDER_ID 1037 long-poll await_refund_obtained) 1038 :transition dialog:proposed -> expired: 1039 :trigger: timer order.pay-deadline 1040 :transition dialog:* -> dialog:waiting-for-other-wallet: 1041 :trigger: call unclaimPayment 1042 The claim nonce is released; another wallet may claim. 1043 1044 .. timer:: auto-refund 1045 Armed from contract terms (auto_refund); exposed to the wallet 1046 in contract terms, fires the refund-probing reactivation. 1047 1048 Flow: deposit to a bank account 1049 -------------------------------- 1050 1051 Entities in play: ``deposit`` (exchange), ``transaction-deposit`` 1052 (wallet-core), and ``kyc-account`` (the deposit flow is its most 1053 frequent client). Identifiers: ``h_wire`` (hash of the target payto), 1054 ``h_contract_terms``, ``wtid`` (created internally by the exchange's 1055 aggregator, carried into the tracking response and onward to merchant 1056 reconciliation). 1057 1058 .. code-block:: rst 1059 1060 .. entity:: deposit 1061 :owner: exchange 1062 1063 A batch deposit of coins to one wire target. 1064 1065 .. lifecycle:: 1066 :terminal: wired, refunded 1067 1068 :transition none -> accepted: 1069 :trigger: call POST /batch-deposit 1070 Coins are deposited; the exchange wires after the refund 1071 deadline and before wire_transfer_deadline. 1072 :transition accepted -> held: 1073 :trigger: watch aml-program.outcome 1074 :if: aggregation KYC triggers (tracking returns 202 with 1075 kyc_ok:false; auditor reports deferral_reason="KYC") 1076 :transition held -> wired: 1077 :trigger: watch exchange.kyc-account (clearance) 1078 :transition accepted -> wired: 1079 :trigger: timer deposit.wire-deadline (aggregation runs) 1080 GET /deposits/$H_WIRE/... now returns 200 with wtid and 1081 execution_time. 1082 :transition accepted -> refunded: 1083 :trigger: call <merchant refund before deadline> 1084 1085 .. timer:: wire-deadline 1086 Armed by BatchDepositRequest.wire_transfer_deadline ("never" 1087 is rejected); exposed in tracking responses as execution_time. 1088 1089 .. entity:: transaction-deposit 1090 :owner: wallet-core 1091 1092 DD37 deposit transaction — the KYC-richest machine: pre-submission 1093 KYC (kyc-auth transfer) and post-submission aggregation KYC are 1094 distinct guarded regions. 1095 1096 .. lifecycle:: 1097 :terminal: failed, aborted 1098 :final-reactivatable: done 1099 :modifiers: working 1100 :standard-edges: suspend-retry 1101 1102 :transition none -> pending:deposit/working: 1103 :trigger: call createDepositGroup 1104 :transition pending:deposit -> pending:kyc-auth: 1105 :trigger: call POST /batch-deposit (denied) 1106 :if: target account not yet bound (409 AUTHORIZATION_KEY_UNKNOWN) 1107 kycAuthTransferInfo names the debit account that must send 1108 the KYC-auth transfer. 1109 :transition pending:kyc-auth -> pending:deposit: 1110 :trigger: watch exchange.kyc-account (auth wire observed via 1111 GET /kyc-check long-poll lpt=1) 1112 :transition pending:deposit -> finalizing:track: 1113 :trigger: watch exchange.deposit (POST /batch-deposit 200) 1114 :transition finalizing:track -> pending:kyc: 1115 :trigger: watch exchange.deposit (tracking 202, kyc_ok:false) 1116 :if: aggregation KYC required 1117 :transition pending:kyc -> finalizing:track: 1118 :trigger: watch exchange.kyc-account (kyc_ok via lpt=2) 1119 :transition finalizing:track -> done: 1120 :trigger: watch exchange.deposit (tracking 200, wtid) 1121 :transition pending:deposit -> aborting/working: 1122 :trigger: call abortTransaction 1123 :transition aborting -> aborted:deposit-abort-recovered: 1124 :trigger: internal (refund + refresh completed) 1125 :transition aborting -> done:deposit-abort-too-late: 1126 :trigger: watch exchange.deposit (already wired) 1127 Terminal done-with-minor: the money arrived despite the abort. 1128 1129 Migration plan 1130 ============== 1131 1132 Compliance snapshot (verified file counts, 2026-10): 1133 1134 .. list-table:: 1135 :header-rows: 1 1136 1137 * - Corpus 1138 - Files 1139 - Status vs. the requirements 1140 * - wallet-core ops 1141 - 152 (plus the notifications page) 1142 - E1–E6, E11, E12 ✔; need: header options, structured errors, 1143 ``:wait:``, effects/observation links to the DD37 machine via the 1144 D10 extensions 1145 * - exchange 1146 - 65 1147 - E1–E7 ✔, retry/idempotency prose good → convert to fields; 1148 need: lifecycle registry entries (reserve, purse), 1149 ``:visible-to:``, notification page (declares: no push channels) 1150 * - merchant 1151 - 114 1152 - E1–E7 ✔; documented scopes become 1153 ``:visible-to: merchant-staff:<scope>``; webhooks move to the 1154 merchant notification page; need: order lifecycle, structured 1155 errors 1156 * - bank-side 1157 - 65 (bank-integration 4, corebank 39, bank-wire 10, 1158 bank-transfer 3, bank-revenue 2, bank-conversion-info 5, 1159 account-directory 2) 1160 - E1–E7 ✔; withdrawal/TAN states exist as unions → lifecycle 1161 registry entries; need: error classification, timers, 1162 cashout status gap flagged (API lacks a status field) 1163 * - identity/misc 1164 - 92 (challenger 7, taldir 5, mailbox 6, donau 13, ebisync 3, 1165 auditor 58) 1166 - mixed; challenger close to compliant; taldir/mailbox need 1167 timers + error fields; drafts get ``:maturity:`` banners 1168 * - sync, terminal 1169 - 3 + 10 endpoints, inline today 1170 - must be split into per-endpoint files first (D2) 1171 1172 Steps, in order: 1173 1174 1. Build the tooling in ``taler-docs/_exts/``: the new directive options, 1175 the ``entity`` / ``lifecycle`` / ``timer`` / ``identifier`` / 1176 ``notification-channel`` directives, and the lint (all rules warn-only 1177 initially). 1178 2. Backfill the bank-integrated withdrawal slice end to end: 1179 bank-integration, corebank withdrawal + TAN, exchange reserve + 1180 withdraw, wallet withdrawal ops, registry entries for 1181 ``withdrawal-operation``, ``tan-challenge``, ``reserve``, 1182 ``reserve_pub`` and ``withdrawal-id``, wallet and exchange 1183 notification pages. Flip the lint to error for this slice. This is 1184 the acceptance gate for the format itself. 1185 3. Payment/refund slice: merchant order lifecycle, wallet payment and 1186 refund ops, merchant notification page, ``order`` / ``order-id`` 1187 anchors. 1188 4. P2P slice: purse entities, wallet peer-push/peer-pull transaction 1189 entities, deposit. 1190 5. The long tail: KYC/AML (with the shared-ownership treatment of D8's 1191 owner rule), sync + terminal splitting, auditor, drafts and banners. 1192 1193 Corpus corrections folded into the requirements document 1194 -------------------------------------------------------- 1195 1196 Verification against the corpus produced these adjustments, which the 1197 requirements document incorporates: 1198 1199 * ``:maturity:`` instead of ``:status:`` (httpdomain field-name 1200 collision; D2). 1201 * Variant-group and proxy-stanza exemptions to the one-method-per-file 1202 rule (D2). 1203 * Sync and terminal APIs are to be split into per-endpoint files (D2). 1204 * The wallet notification page exists but must be *restructured* into 1205 channel form; it is not already compliant (D9). 1206 * wallet-core op count is 152, not 153 (the 153rd file is 1207 ``notifications.rst``). 1208 * KYC state is cross-cutting and gets the projections-named-under-owner 1209 treatment rather than a naive single owner. 1210 * Wallet transaction entities get the four extensions of D10 and the 1211 canonical-registry relationship to DD37 of D11. 1212 1213 Test Plan 1214 ========= 1215 1216 * The lint (V1–V21 in the requirements document) runs in the 1217 documentation CI; the bank-integrated withdrawal slice (bank- 1218 integration, corebank withdrawal + TAN, exchange reserve + withdraw, 1219 wallet withdrawal ops, registry entries for ``reserve_pub`` and 1220 ``withdrawal-id``) must lint clean as the acceptance gate for the 1221 format itself. 1222 * Codegen dry-run: generate client types and validators from the 1223 slice's ``ts:def`` layer and diff against wallet-core's and the 1224 exchange's corresponding wire types; mismatches fail the 1225 implementation CI. 1226 * Derivation smoke tests: generate the withdrawal-operation state table, 1227 the bank-customer persona projection, and a withdrawal sequence diagram 1228 from the corpus; review against the hand-written flow documentation. 1229 * Wallet extension tests: lint the ``transaction-withdrawal`` registry 1230 entry against DD37's generated tables; check that reactivation edges out 1231 of ``done`` are legal under ``:final-reactivatable:`` and rejected under 1232 ``:terminal:``. 1233 1234 Definition of Done 1235 ================== 1236 1237 * All new directives (``entity``, ``lifecycle``, ``timer``, ``identifier``, 1238 ``notification-channel``, extended ``http:*``/``ts:op`` options) are 1239 implemented in ``_exts/`` and render correctly. 1240 * The lint enforces the full rule set as errors on the backfilled slices. 1241 * Every per-endpoint file (including the newly split sync and terminal 1242 endpoints) is compliant or carries an explicit draft/banner maturity. 1243 * The entity registry covers at minimum the withdrawal, payment/refund, 1244 and p2p flows end to end, including wallet transaction entities with 1245 the D10 extensions. 1246 * DD37's state tables are generated from the registry. 1247 * At least one implementation repository runs codegen drift detection in 1248 CI against the corpus. 1249 1250 Alternatives 1251 ============ 1252 1253 **OpenAPI/Swagger as the source of truth.** Covers the shape layer well 1254 but has no home for lifecycles, temporality semantics, actor projections, 1255 identifier roles, or notification doctrine — the parts this design exists 1256 for. The ``ts:def`` layer already plays OpenAPI's shape role inside the 1257 prose-capable Sphinx corpus; replacing the corpus would lose the prose 1258 that remains the human interface. 1259 1260 **Generate docs from code.** Inverts the authority relationship: the 1261 design intent (idempotency guarantees, temporality classification, 1262 capability properties) is not present in code in checkable form, so 1263 generated docs would silently reflect implementation bugs as 1264 specification. The spec-first model with CI drift detection keeps the 1265 docs authoritative while still catching divergence. 1266 1267 **Full anchor-side declarations** (state sets, timer facts, identifier 1268 life stories restated at the registry). Rejected: every derivable fact 1269 declared twice is a drift bug waiting to happen (D1). 1270 1271 **Per-type substate unions instead of DD37's flat minor-state union.** 1272 Rejected for this design: the flat global union is the deployed, 1273 implemented vocabulary; partitioning it is a DD37 change, out of scope. 1274 1275 **Modeling ``/working`` and suspended states as literal states.** 1276 Rejected: combinatorial table explosion for zero informational gain; 1277 orthogonal modifiers plus implied standard edges carry the same semantics 1278 (D10.2). 1279 1280 **A separate ``:auth:`` field.** Rejected: actor implies mechanism; 1281 duplication invites contradiction (D5). 1282 1283 Drawbacks 1284 ========= 1285 1286 * **Authoring cost.** Compliant files are substantially more work than 1287 prose, and ~490 files need backfill. Mitigated by incremental migration 1288 and by the lint telling authors exactly what is missing. 1289 * **Format rigidity.** Edge cases that do not fit (today: variant groups, 1290 proxy stanzas, KYC's shared ownership) need explicit format extensions 1291 rather than prose escape hatches, which slows documentation of unusual 1292 endpoints. Accepted: silent exceptions are worse than visible ones. 1293 * **Two specification artifacts to maintain.** This DD, the requirements 1294 document, and (for wallets) DD37 must evolve together. Mitigated by the 1295 normative-reference hierarchy and by generating DD37's tables from the 1296 registry (D11). 1297 * **Tooling must exist before the value does.** Until the lint and at 1298 least one generator land, the new fields are overhead without payoff. 1299 Mitigated by doing the withdrawal slice end-to-end first, tooling 1300 included. 1301 * **The wallet extensions add vocabulary** (``:final-reactivatable:``, 1302 ``:standard-edges:``, modifier flags) that only wallet transaction 1303 entities use, slightly diluting the "one vocab fits all" property. 1304 1305 Discussion / Q&A 1306 ================ 1307 1308 **Why not put triggers on the method files instead of the lifecycle?** 1309 A method can honestly describe its own effects (and does, in **Effects**), 1310 but timer firings, watches of other components, and external human actions 1311 have no method file to live in. Causality is a property of the entity's 1312 machine, so triggers live on transitions; the two-sided check with method 1313 effects keeps both honest. 1314 1315 **Why is long-poll not a notification channel?** 1316 A long-poll response is authoritative state delivered late; a notification 1317 is a hint that must be backed by a read. The delivery semantics, the lint 1318 rules, and the generated sequence-diagram edges differ. Conflating them 1319 would make "pull + long-poll only" components (the exchange) look like 1320 they offer push. 1321 1322 **Why does the tax auditor have no credentials?** 1323 That is the system as designed: the auditor's access is authorized by 1324 possession of unguessable identifiers (``wtid``). The format makes this 1325 visible (``capability`` annotations resolving to ``:capability: yes`` 1326 anchors) rather than hiding it in prose. 1327 1328 **Are wallet transaction entities really "owned" by wallet-core if nothing 1329 calls wallet-core to drive them?** 1330 Yes: ownership is about authoritative state and transition execution, not 1331 about the trigger kind. Wallet-core's DB record is the truth; its 1332 transitions execute locally, driven by watches, timers, and user actions. 1333 The bank-owned withdrawal operation the wallet watches is a *different* 1334 entity with its own owner and machine — the two are joined by the 1335 ``watch`` trigger, which is exactly the cross-component causality link the 1336 old prose never made.