diff --git a/AGENTS.md b/AGENTS.md index 09f59744..6fedad1e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,6 +82,8 @@ Severity tags: - `[MUST]` Avoid unnecessary indirection layers (`executor`, `worker`, `engine`, etc.). - `[MUST]` Keep one distinct responsibility per file; merge pass-through modules into functions. - `[MUST]` Eliminate duplicated logic blocks (>10 lines) by extracting shared helpers. +- `[MUST]` Do not flatten typed `SignerError` / Coinset / Dexie outcomes to strings and re-parse + them for control flow. Retryability and 404/missing are variant matches (ADR 0027). - `[MUST]` Use allowlists for state checks; never rely on negated blocklists. - `[MUST]` For similar polling loops, match existing interval/accumulator style; warning cadence must be additive (`next_warning += warning_interval`). See `docs/plan.md` → **Delivery diff --git a/CONTEXT.md b/CONTEXT.md index 5e1465cd..4eb6fe27 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -11,3 +11,7 @@ _Avoid_: Unique pin helpers, binding exclusion set, Direct coin picker **Remaining-shape ownership**: Who finishes ladder shape after the primary row is on-chain — offer-post bootstrap versus daemon coin ops — including the inverse handoff when a low-watermark split still belongs to bootstrap combine-first. _Avoid_: Shape deferral, sub-primary policy, bootstrap vs coin-ops bounce + +**Exact-denomination combine**: +Managed and CLI combine that spends only coins of the same clip size. Shape combine-first may cover a target with mixed sizes. +_Avoid_: mixed-denomination cover (for the managed/CLI path) diff --git a/README.md b/README.md index 66378d12..dc9f35b5 100644 --- a/README.md +++ b/README.md @@ -90,8 +90,8 @@ greenfloor-manager vault-asset-trace --asset wUSDC.b # Split one coin into target denominations (waits through signature + mempool + confirmation + reorg watch) greenfloor-manager coin-split --pair TDBX:txch --coin-id --amount-per-coin 1000 --number-of-coins 10 -# Combine small coins into one larger coin (waits through signature + mempool + confirmation + reorg watch) -greenfloor-manager coin-combine --pair TDBX:txch --input-coin-count 10 --asset-id xch +# Combine same-denomination coins (waits through signature + mempool + confirmation + reorg watch) +greenfloor-manager coin-combine --pair TDBX:txch --input-coin-count 5 --asset-id xch ``` Coin-op wait diagnostics include: @@ -118,6 +118,8 @@ Run the daemon: greenfloord --program-config config/program.yaml --markets-config config/markets.yaml --once ``` +`--once` exit codes: [runbook](docs/runbook.md) §6. + ## Local dev tooling Post-Python-cutover local development uses three runtimes. None of them replace diff --git a/docs/README.md b/docs/README.md index de893df3..0ebd8b1d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,21 +5,26 @@ accepted decision** when onboarding. ## Current (operator + engine) -| ADR | Topic | -| ---------------------------------------------------------------- | ----------------------------------------------------------------------- | -| [0026](decisions/0026-combine-dust-remainder-coin.md) | **Combine dust remainder** — extra input absorbs CAT change; keep 1 CAT | -| [0025](decisions/0025-two-sided-target-spread.md) | **Two-sided spread** — bid/ask around mid; sell-only omits the field | -| [0023](decisions/0023-canonical-cat-outer-puzzle-hash.md) | **CAT outer hash** — one `coinset/cats/outer` primitive + Coinset hex | -| [0021](decisions/0021-three-ownership-simplifications.md) | **Ownership spines** — expired maker, reconcile prep, `coin_ops::shape` | -| [0020](decisions/0020-soft-expiry-stable-makers.md) | **Soft listing expiry** — stable makers, `ensure_size_n_offer` | -| [0018](decisions/0018-coinset-parse-decomposition.md) | **Coinset submodule layout** — parse, pagination, rpc_result, json_util | -| [0017](decisions/0017-offer-submodule-decompositions.md) | **Offer submodule layout** — bootstrap planner/phase + presplit | -| [0015](decisions/0015-on-chain-offer-cancel.md) | **On-chain offer cancel** — reclaim spend, `cancel_submitted` lifecycle | -| [0014](decisions/0014-offer-publish-module-decomposition.md) | **Offer publish decomposition** — bootstrap gate + publish assets | -| [0013](decisions/0013-rust-cli-daemon-native-cutover.md) | **Native Rust CLI/daemon** — production operator path | -| [0010](decisions/0010-rust-engine-crate-naming.md) | Crate and module naming (`greenfloor-engine`, `greenfloor_engine`) | -| [0007](decisions/0007-rust-signer-and-coinset-io.md) | Vault KMS signing and Coinset IO in Rust | -| [0003](decisions/0003-parallel-offer-reservation-coordinator.md) | Parallel managed-post reservation leases | +| ADR | Topic | +| ------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| [0027](decisions/0027-typed-operator-outcomes.md) | **Typed outcomes** — domain `SignerError`; no flatten-then-reparse | +| [0026](decisions/0026-combine-dust-remainder-coin.md) | **Combine** — dust remainder on covering shape; exact-denom managed/CLI | +| [0025](decisions/0025-two-sided-target-spread.md) | **Two-sided spread** — bid/ask around mid; sell-only omits the field | +| [0024](decisions/0024-coin-ops-effective-counts-and-buffer-raid.md) | **Effective counts** — free inventory vs makers; buffer-raid splits | +| [0023](decisions/0023-canonical-cat-outer-puzzle-hash.md) | **CAT outer hash** — one `coinset/cats/outer` primitive + Coinset hex | +| [0022](decisions/0022-unique-maker-coins.md) | **Unique Direct makers** — one exact-size receive CAT per open Direct offer | +| [0021](decisions/0021-three-ownership-simplifications.md) | **Ownership spines** — expired maker, reconcile prep, `coin_ops::shape` | +| [0020](decisions/0020-soft-expiry-stable-makers.md) | **Soft listing expiry** — stable makers, `ensure_size_n_offer` | +| [0019](decisions/0019-coinset-ws-local-watches.md) | **Local watches** — Coinset WS p2/coin-id; default publish venue `coinset` | +| [0018](decisions/0018-coinset-parse-decomposition.md) | **Coinset submodule layout** — parse, pagination, rpc_result, json_util | +| [0017](decisions/0017-offer-submodule-decompositions.md) | **Offer submodule layout** — bootstrap planner/phase + presplit | +| [0016](decisions/0016-sqlite-persistence-coverage-policy.md) | **SQLite coverage** — exclude `storage/sqlite/` from llvm-cov/diff-cover | +| [0015](decisions/0015-on-chain-offer-cancel.md) | **On-chain offer cancel** — reclaim spend, `cancel_submitted` lifecycle | +| [0014](decisions/0014-offer-publish-module-decomposition.md) | **Offer publish decomposition** — bootstrap gate + publish assets | +| [0013](decisions/0013-rust-cli-daemon-native-cutover.md) | **Native Rust CLI/daemon** — production operator path | +| [0010](decisions/0010-rust-engine-crate-naming.md) | Crate and module naming (`greenfloor-engine`, `greenfloor_engine`) | +| [0007](decisions/0007-rust-signer-and-coinset-io.md) | Vault KMS signing and Coinset IO in Rust | +| [0003](decisions/0003-parallel-offer-reservation-coordinator.md) | Parallel managed-post reservation leases | ## Superseded ADRs (removed from tree; see git history) diff --git a/docs/decisions/0021-three-ownership-simplifications.md b/docs/decisions/0021-three-ownership-simplifications.md index a275ec27..fc8b4423 100644 --- a/docs/decisions/0021-three-ownership-simplifications.md +++ b/docs/decisions/0021-three-ownership-simplifications.md @@ -47,6 +47,8 @@ Behavior-preserving ownership moves only (no policy changes). ladder protect flag) rather than silently merging selectors. - Daemon remains phase orchestrator (not a thin shell); path-specific gates, fee budgets, and dust Preselected submit stay path-local. +- CLI apply later lives on `WatchedOfferReconciler::fetch_and_apply` (the 0021 helper + was renamed). ## Non-goals diff --git a/docs/decisions/0026-combine-dust-remainder-coin.md b/docs/decisions/0026-combine-dust-remainder-coin.md index 155ec569..3d80326f 100644 --- a/docs/decisions/0026-combine-dust-remainder-coin.md +++ b/docs/decisions/0026-combine-dust-remainder-coin.md @@ -2,7 +2,7 @@ ## Status -Accepted (2026-08-12). +Accepted (2026-08-12; managed/CLI exact-denomination added 2026-08-13). ## Context @@ -16,6 +16,9 @@ Lowering the dust floor to 0.1 CAT would allow the 100-mojo case but still block 20-mojo (size-10) and 50-mojo (single size-25) remainders, and would mint awkward dust clips. +Managed/CLI combine separately covered a target with mixed coin sizes (`TargetCover`), +which hid denomination gaps and minted awkward clips. + ## Decision 1. **Keep the 1 CAT dust floor.** `coin_op_min_amount_mojos` stays 1,000 for CATs. @@ -25,10 +28,17 @@ dust clips. remainder coin — or on a different pair whose change is already legal. 3. **Fail closed** when no covering set within `combine_input_cap` leaves legal change. Do not emit sub-CAT outputs. +4. **Managed/CLI combine is exact-denomination.** `coin-combine` and managed combine + plans spend only coins whose amount equals the target clip, capped by YAML + `coin_ops.combine_input_coin_cap` (default 5, min 2). Shape combine-first still + covers a target and may mix sizes. No env-var cap. ## Consequences - Two 25.025 clips plus a 4.930 remainder can fund two 24.975 buy clips (change 5.030). - Two 25.025 clips alone still cannot fund that target. -- Daemon flat combine (no dust context) is unchanged: a solo covering pick is still +- Shape combine-first without dust context is unchanged: a solo covering pick is still "not a combine." +- Managed/CLI combine skips with `no_spendable_combine_coin_available` instead of + covering a target with unrelated sizes. `--input-coin-count` is + `min(requested, combine_input_coin_cap)`. diff --git a/docs/decisions/0027-typed-operator-outcomes.md b/docs/decisions/0027-typed-operator-outcomes.md new file mode 100644 index 00000000..2cc6d6c6 --- /dev/null +++ b/docs/decisions/0027-typed-operator-outcomes.md @@ -0,0 +1,31 @@ +# ADR 0027: Typed operator outcomes + +## Status + +Accepted (2026-08-13). + +## Context + +Operator control flow flattened typed Coinset, Dexie, and driver failures to strings, +then re-parsed them (retry marker lists, mixed-split normalize, forged Dexie +`success: false` JSON). That hid the real 404 / RPC / transport boundary and made +retryability a substring match. + +## Decision + +1. **`SignerError` by domain.** Variants wrap `Vault`, `CoinOps`, `Offer`, `Transport`, + `Persistence`, `Config`, and `Reconcile`. Do not flatten those to strings and + re-parse them for control flow. +2. **Typed transport.** `TransportError::from_reqwest` maps timeout, connect, decode, + request, and `HttpStatus`. `TransportError::Coinset(String)` is Coinset RPC + `success: false` only. Retryability is `is_retryable_upstream()` (variant match). + Mixed-split unspendable coins map at the driver boundary + (`VaultError::MixedSplitSelectedCoinsNotSpendable`). +3. **Typed Dexie.** HTTP status and visibility stay `SignerError` until the operator + JSON edge. `get_offer` has one 404 boundary. `DexieOfferFetch` is `Found`, `Missing`, + or `Mismatch`; Found requires a nested `offer` whose id matches the request. + +## Consequences + +- Reconcile Found fixtures nest `{ "offer": { ... } }` with a matching id. +- CLI retry/fallback reads `err.is_retryable_upstream()`, not marker lists. diff --git a/docs/plan.md b/docs/plan.md index badafb78..2d4d90d4 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -32,7 +32,8 @@ adapter unit tests ──► greenfloor_scripts/ → engine + manager CLIs `CatSelection::Preselected`. - **Shared shape planning:** `coin_ops::shape` (deficit → combine-first / single-coin funding → output amounts). Bootstrap planner and managed auto-split are thin wrappers; - `plan_coin_ops` remains the batch count/fee scheduler. See ADR 0021. + `plan_coin_ops` remains the batch count/fee scheduler. See ADR 0021. Combine policy is + ADR 0026; typed operator outcomes are ADR 0027. - **Expired / surplus makers:** `offer::lifecycle::expired_maker` (soft-mark, CAS lease, surplus reclaim plan). Daemon `soft_expire_phase` is a thin adapter; cancel stays separate (ADR 0015). @@ -56,7 +57,7 @@ Core trading/runtime (V1): 2. `config-validate` — validate program + markets YAML 3. `doctor` — readiness check (config, keys, DB, env overrides) 4. `keys-onboard` — key selection and onboarding state -5. `build-and-post-offer` — vault KMS offer build + Dexie/Splash publish +5. `build-and-post-offer` — vault KMS offer build + publish (`coinset` default; Dexie/Splash opt-in) 6. `offers-status` — offer states and recent audit events 7. `offers-reconcile` — refresh states from venue + Coinset tx signals 8. `offers-cancel` — on-chain cancel by offer id or `--cancel-open` (Dexie fetch + Coinset submit) diff --git a/docs/progress.md b/docs/progress.md index a79704cb..9f802ba5 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -16,6 +16,11 @@ Pre-Rust migration detail lives in git history and ## Milestones +### 2026-08-13 — Typed operator outcomes (ADR 0027) + +[#202](https://github.com/hoffmang9/greenfloor/pull/202). Typed Coinset/Dexie/`SignerError` +outcomes: ADR 0027. Exact-denomination managed combine: ADR 0026. + ### 2026-08-12 — Combine dusty overshoot takes a remainder coin (ADR 0026) When a covering combine would leave CAT dust change, the shaper retries while skipping diff --git a/docs/runbook.md b/docs/runbook.md index 3f428b4a..dcadfa15 100644 --- a/docs/runbook.md +++ b/docs/runbook.md @@ -53,7 +53,7 @@ Optional developer bootstrap for testnet markets: - **CAT dust via Coinset (enabled markets):** Sub-unit CAT outputs (strictly below **1000** mojos per coin) can be merged with the direct Coinset signer path, one batch per `keys.registry` signer used on the market rows. From the repo root: `PATH="$(pwd)/.venv/bin:$PATH" greenfloor-manager combine-market-cat-dust --program-config ~/.greenfloor/config/program.yaml --markets-config ~/.greenfloor/config/markets.yaml --json`. Prefer `--dry-run` or `--list-only` before live combines; `--cat-asset-id ` scopes a single asset; optional `--testnet-markets-config` matches daemon overlay semantics. - Shape denominations for the selected market context: - Split: `greenfloor-manager coin-split --pair TDBX:txch --coin-id --amount-per-coin 1000 --number-of-coins 10` - - Combine: `greenfloor-manager coin-combine --pair TDBX:txch --input-coin-count 10 --asset-id xch` + - Combine: `greenfloor-manager coin-combine --pair TDBX:txch --input-coin-count 5 --asset-id xch` - Config-driven shaping (from market `ladders.sell`): `greenfloor-manager coin-split --pair TDBX:txch --size-base-units 10` - Config-driven combine threshold (from market `ladders.sell`): `greenfloor-manager coin-combine --pair TDBX:txch --size-base-units 10` - Optional venue context annotation for prep commands: add `--venue dexie` or `--venue splash` (coin-prep works without it). @@ -269,6 +269,7 @@ Monitor `audit_event` records in `~/.greenfloor/db/greenfloor.sqlite`: - For `testnet11`, do not route to mainnet Coinset endpoint unless you explicitly set `GREENFLOOR_ALLOW_MAINNET_COINSET_FOR_TESTNET11=1` for temporary debugging. - Coin combine input cap (manager/daemon coin-op execution; `~/.greenfloor/config/program.yaml` -> `coin_ops`): - `combine_input_coin_cap` (default: `5`, min `2`) + - Managed/CLI combine is exact-denomination only. `--input-coin-count` is `min(requested, combine_input_coin_cap)`. - Daemon tx-signal ingestion controls (`~/.greenfloor/config/program.yaml` -> `chain_signals.tx_block_trigger`): - `mode`: must be `websocket` - `websocket_url`: Coinset websocket endpoint (defaults by network when blank) @@ -283,6 +284,7 @@ Monitor `audit_event` records in `~/.greenfloor/db/greenfloor.sqlite`: - Daemon singleton lock behavior: - daemon loop and `--once` both require exclusive lock on `state_dir/daemon.lock`. - if lock is held, process exits with `daemon_lock_conflict` event and non-zero exit code. + - `--once` also exits 1 when any preamble or cycle error occurred, or when markets were attempted and none processed. Isolation still runs the other markets. - Validate config + override sanity before deploy: - `greenfloor-manager doctor` (includes warnings for invalid runtime override env values) - Test-only overrides (debug builds / CI; not for production):