Status: Accepted
Date: 2026-04-13
Commit: d82a13f
ADR 016 introduced the sans-I/O state-machine pattern for the read path (Resolver). The DID write path: DidBtcr2.update({...}): was still a monolithic async function that directly:
This had the same problems the Resolver pattern was created to solve: I/O and protocol logic interleaved, hard to test without mocks, and consumers couldn’t insert their own key-handling semantics (hardware wallets, multisig prompts) without monkey-patching the library.
update() and live with the coupling.update() into free functions (construct, sign, fund, broadcast) the caller composes manually. Would work but puts the phase-sequencing burden on every caller.Resolver: emit typed DataNeed requests for the caller to fulfill, sequence phases internally.Option 3. Add Updater in packages/method/src/core/updater.ts. It mirrors the Resolver pattern with a write-path-appropriate phase sequence and its own DataNeed discriminated union:
Phases: Construct to Sign to Fund to Broadcast to Complete.
Data needs:
NeedSigningKey: caller provides the secret key bytes (or a KMS-backed signature). Includes the unsignedUpdate for inspection.NeedFunding: caller confirms the beaconAddress has a spendable UTXO (caller’s choice how to check).NeedBroadcast: caller announces the signedUpdate via the chosen beacon service (singleton, CAS, or aggregation).Static utility methods on Updater: Updater.construct(), Updater.sign(), Updater.announce(): expose each step individually for scripts that don’t want the full state machine (e.g. test-vector generation in lib/generate-vector.ts).
Factory validation. DidBtcr2.update({...}) validates that verificationMethodId is in capabilityInvocation and that beaconId matches a service in the source document before returning an Updater. Invalid inputs fail fast at the factory, not mid-state-machine.
Positive
provide() calls.NeedSigningKey on the caller side.Resolver means anyone who understands the read path understands the write path immediately.Negative
await DidBtcr2.update({...}) call would have provided. Mitigated by a helper at the API façade layer, which @did-btcr2/api does offer.Explicitly accepted trade-offs
NeedBroadcast is the seam where the caller decides between SingletonBeacon.broadcastSignal() and the aggregation subsystem (ADR 020); the Updater state machine doesn’t privilege either path.packages/method/src/core/updater.ts: state machine.packages/method/tests/updater.spec.ts: test suite.