Implements step 4 from the architecture gap-analysis — the ledger-anchor layer (arch §4.5 + §5.7) stops being a mock and starts writing to the deployed NotaryRegistry on Chain 138. Independent of PRs A/B; can land in any order.
What lands
services/notaryChain.ts (new)
ethers-v6 adapter. Minimal ABI (two functions, two events, one view) lifted from contracts/interfaces/INotaryRegistry.sol:
computePlanHash(plan) and planIdToBytes32(planId) — deterministic helpers so the mock and chain paths publish identical hashes for identical inputs.
A singleton contract/wallet is cached after first use, keyed on contractAddress, and can be reset via __resetForTests().
Graceful mock fallback
If any of CHAIN_138_RPC_URL, NOTARY_REGISTRY_ADDRESS, ORCHESTRATOR_PRIVATE_KEY is unset, both functions return mode: 'mock' and log the reason. Unit tests, CI, and local dev require no extra wiring.
services/notary.ts (refactored)
Still the single entry point for ExecutionCoordinator; now delegates to notaryChain and surfaces the extra on-chain fields (mode, txHash, blockNumber, contractAddress, receiptHash) to callers. If the chain call throws, we log + fall back to mock so a momentary RPC outage cannot take the workflow down.
No on-chain test: the integration needs a funded signer on Chain 138; that's a manual smoke once an ORCHESTRATOR_PRIVATE_KEY is provisioned.
Steps encoding: registerPlan is called with an empty Step[] for now. PR E (SWIFT gateway) introduces stable step IDs, after which we can serialise the real steps.
getNotaryProof still returns a deterministic mock — the on-chain read will be wired in PR D alongside the events table (so a single query reconstructs { onChainProof, eventTrail }).
Series order
A → B → C → D → E → F → G → H.
Implements **step 4** from the architecture gap-analysis — the ledger-anchor layer (arch §4.5 + §5.7) stops being a mock and starts writing to the deployed `NotaryRegistry` on Chain 138. Independent of PRs A/B; can land in any order.
## What lands
### `services/notaryChain.ts` (new)
ethers-v6 adapter. Minimal ABI (two functions, two events, one view) lifted from `contracts/interfaces/INotaryRegistry.sol`:
| fn | purpose |
| --- | --- |
| `registerPlan(bytes32 planId, Step[] steps, address creator)` | anchor planHash on-chain |
| `finalizePlan(bytes32 planId, bool success)` | record commit/abort disposition |
| `getPlan(bytes32)` | read-side for `GET /api/plans/:id/proof` (wired later) |
Public surface:
- `anchorPlan(plan)` → `{ mode: 'chain'|'mock', txHash?, planHash, blockNumber?, contractAddress? }`
- `finalizeAnchor(planId, success)` → `{ mode, txHash?, receiptHash?, blockNumber? }`
- `computePlanHash(plan)` and `planIdToBytes32(planId)` — deterministic helpers so the mock and chain paths publish identical hashes for identical inputs.
A singleton contract/wallet is cached after first use, keyed on `contractAddress`, and can be reset via `__resetForTests()`.
### Graceful mock fallback
If **any** of `CHAIN_138_RPC_URL`, `NOTARY_REGISTRY_ADDRESS`, `ORCHESTRATOR_PRIVATE_KEY` is unset, both functions return `mode: 'mock'` and log the reason. Unit tests, CI, and local dev require no extra wiring.
### `services/notary.ts` (refactored)
Still the single entry point for `ExecutionCoordinator`; now delegates to `notaryChain` and surfaces the extra on-chain fields (`mode`, `txHash`, `blockNumber`, `contractAddress`, `receiptHash`) to callers. If the chain call throws, we log + fall back to mock so a momentary RPC outage cannot take the workflow down.
### `config/env.ts`
```
CHAIN_138_RPC_URL url, optional (e.g. https://rpc.d-bis.org)
CHAIN_138_CHAIN_ID digits, optional (default 138)
NOTARY_REGISTRY_ADDRESS 0x-40 hex, optional
ORCHESTRATOR_PRIVATE_KEY 0x-64 hex, optional
```
All four regex-validated. Missing envs do **not** fail startup.
### Tests
`tests/unit/notaryChain.test.ts` — 6 cases: `planIdToBytes32` determinism + collision-resistance, `computePlanHash` determinism, and the three mock-fallback paths (empty config, partial config, finalizeAnchor).
## Verification
```
$ npx tsc --noEmit # clean
$ npx jest # 51 passed, 4 suites (45 pre-existing + 6 new)
```
## Not in this PR
- No on-chain test: the integration needs a funded signer on Chain 138; that's a manual smoke once an ORCHESTRATOR_PRIVATE_KEY is provisioned.
- Steps encoding: `registerPlan` is called with an empty `Step[]` for now. PR E (SWIFT gateway) introduces stable step IDs, after which we can serialise the real steps.
- `getNotaryProof` still returns a deterministic mock — the on-chain read will be wired in PR D alongside the events table (so a single query reconstructs `{ onChainProof, eventTrail }`).
## Series order
A → B → **C** → D → E → F → G → H.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements step 4 from the architecture gap-analysis — the ledger-anchor layer (arch §4.5 + §5.7) stops being a mock and starts writing to the deployed
NotaryRegistryon Chain 138. Independent of PRs A/B; can land in any order.What lands
services/notaryChain.ts(new)ethers-v6 adapter. Minimal ABI (two functions, two events, one view) lifted from
contracts/interfaces/INotaryRegistry.sol:registerPlan(bytes32 planId, Step[] steps, address creator)finalizePlan(bytes32 planId, bool success)getPlan(bytes32)GET /api/plans/:id/proof(wired later)Public surface:
anchorPlan(plan)→{ mode: 'chain'|'mock', txHash?, planHash, blockNumber?, contractAddress? }finalizeAnchor(planId, success)→{ mode, txHash?, receiptHash?, blockNumber? }computePlanHash(plan)andplanIdToBytes32(planId)— deterministic helpers so the mock and chain paths publish identical hashes for identical inputs.A singleton contract/wallet is cached after first use, keyed on
contractAddress, and can be reset via__resetForTests().Graceful mock fallback
If any of
CHAIN_138_RPC_URL,NOTARY_REGISTRY_ADDRESS,ORCHESTRATOR_PRIVATE_KEYis unset, both functions returnmode: 'mock'and log the reason. Unit tests, CI, and local dev require no extra wiring.services/notary.ts(refactored)Still the single entry point for
ExecutionCoordinator; now delegates tonotaryChainand surfaces the extra on-chain fields (mode,txHash,blockNumber,contractAddress,receiptHash) to callers. If the chain call throws, we log + fall back to mock so a momentary RPC outage cannot take the workflow down.config/env.tsAll four regex-validated. Missing envs do not fail startup.
Tests
tests/unit/notaryChain.test.ts— 6 cases:planIdToBytes32determinism + collision-resistance,computePlanHashdeterminism, and the three mock-fallback paths (empty config, partial config, finalizeAnchor).Verification
Not in this PR
registerPlanis called with an emptyStep[]for now. PR E (SWIFT gateway) introduces stable step IDs, after which we can serialise the real steps.getNotaryProofstill returns a deterministic mock — the on-chain read will be wired in PR D alongside the events table (so a single query reconstructs{ onChainProof, eventTrail }).Series order
A → B → C → D → E → F → G → H.