Skip to content

feat(channels): converge on council asset disable — withdraw-only gate#122

Merged
AquiGorka merged 4 commits into
mainfrom
feat/asset-lifecycle
Jun 17, 2026
Merged

feat(channels): converge on council asset disable — withdraw-only gate#122
AquiGorka merged 4 commits into
mainfrom
feat/asset-lifecycle

Conversation

@AquiGorka

@AquiGorka AquiGorka commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Part of the multi-asset asset-lifecycle mechanism (UC6).

Providers converge on the council's asset enable/disable decisions:

  • event-watcher decodes/handles channel_state_changed; the registry gains a distinct disabled state (separate from pending/inactive), keyed by privacy-channel id.
  • Withdraw-only gate: a disabled channel rejects deposits/sends (CHANNEL_DISABLED) and allows withdrawals (incl. change). Re-enable resumes full service.
  • Convergence-by-query on boot + on out-of-retention recovery (cursor reset + council re-query); deterministic unit coverage for query→reconcile and retention detection/recovery.
  • Dashboard channel summary reports the disabled count.
  • 99 integration + unit green on deno 2.8.2. Version 0.7.10 → 0.8.0.

Note: a rare pre-existing WS-timeout flake in ws-handler_test.ts (tracked separately, out of scope) can red CI intermittently — re-run if it does.

Relates to ClickUp 86c903393.

Coordinated PRs (review/merge together)

Release on merge

Contains the version bump deno.json 0.7.10 → 0.8.0. On merge to main: auto-tag.yml → tag v0.8.0release.yml builds the GHCR image 0.8.0, dispatches the cross-repo E2E, and auto-deploys to Fly.io testnet after the E2E gate passes (blue/green; this change is additive/backward-compatible). Order-independent per DEPLOY-STRATEGY.md.

Providers now react to the channel-auth ChannelStateChanged event and converge
on per-asset channel status.

- event-watcher: decode + handle channel_state_changed (council-scoped, applied
  regardless of provider address); registry gains a distinct "disabled" state
  keyed by privacy-channel id, separate from pending/inactive.
- convergence-by-query: on boot and on out-of-retention recovery the watcher
  re-queries the council's public channel state and reconciles the registry;
  out-of-retention is now detected and recovered (cursor reset + resync).
- withdraw-only gate: a disabled channel rejects bundles that aren't
  withdraw-only (deposits and sends) with CHANNEL_DISABLED; withdrawals (incl.
  change) still pass. Re-enable resumes full service.
- dashboard channel summary reports the disabled count.
- tests: registry disable/re-enable + convergence, event decode, gate predicate.
…verage

Extract the convergence + retention logic into env-free modules so the actual
query-council-and-reconcile and out-of-retention recovery behaviors are unit
tested (not just the registry primitives):
- channel-convergence.ts: fetchCouncilConfig + reconcileChannelStatuses.
- retention.ts: isOutOfRetentionError + recoverFromOutOfRetention.
Tests assert the registry converges to a council's disabled/enabled statuses
from a (stubbed) council query, and that an out-of-retention error resets the
cursor and fires the council re-query.
The ws-handler unit test booted a real Deno.serve on a random port and waited
~6s to observe Deno's protocol-level auto-ping keep-alive — that tested Deno's
behaviour (not ours) and was timing/port flaky.

Replace it with deterministic unit tests that drive handleEventsWs with a mocked
WebSocket + mocked PP repo + the in-process event-bus loopback: auth/ownership
gating (426/401/400/403, never upgrades), the upgrade config (subprotocol +
idle-timeout), scoped delivery, the not-OPEN drop guard, and unsubscribe-on-close.
No server, no port, no wall-clock. 20/20 stable; full test:unit 104/0 across runs.
Live end-to-end delivery stays covered by tests/integration/http/events-ws.test.ts.
AquiGorka added a commit to Moonlight-Protocol/soroban-core that referenced this pull request Jun 17, 2026
#33)

Part of the multi-asset **asset-lifecycle mechanism** (UC6): a council
can enable / disable / re-enable an asset, and every provider converges
on the current state.

This repo adds the quorum-gated, **event-only** contract surface on
channel-auth:
- `enable_channel(channel, asset)` / `disable_channel(channel, asset)`,
gated by `enforce_owner_auth` (owner = council quorum), mirroring
`add_provider`/`remove_provider`.
- New event `ChannelStateChanged { channel, asset, enabled }`. The
contract holds **no** channel/asset state — the event is the only
on-chain artifact. Re-enable reuses `enable_channel`.
- Unit tests: event fields for enable/disable/re-enable + non-owner
rejection.
- Workspace version 0.2.1 → 0.3.0.

fmt/clippy/test green; `stellar contract build` exports the new
functions.

Relates to ClickUp 86c903393.

## Coordinated PRs (review/merge together)
- soroban-core: #33
- council-platform: Moonlight-Protocol/council-platform#47
- provider-platform: Moonlight-Protocol/provider-platform#122
- local-dev: Moonlight-Protocol/local-dev#119

## Release on merge
Contains the version bump `Cargo.toml` 0.2.1 → **0.3.0**. On merge to
`main`: `auto-tag.yml` → tag **v0.3.0** → `release.yml` builds
`channel_auth_contract.wasm` + `privacy_channel.wasm`, publishes the
**GitHub Release** (canonical WASM source consumed by the E2E gate), and
**dispatches the cross-repo E2E**. Per DEPLOY-STRATEGY.md, order is
irrelevant — intermediate E2E fails are expected; the last platform
release triggers the passing multi-asset run.
@AquiGorka
AquiGorka merged commit 31ebb51 into main Jun 17, 2026
7 checks passed
@AquiGorka
AquiGorka deleted the feat/asset-lifecycle branch June 17, 2026 17:18
AquiGorka added a commit to Moonlight-Protocol/local-dev that referenced this pull request Jun 17, 2026
Part of the multi-asset **asset-lifecycle mechanism** (UC6) —
validation.

Extends the multi-asset suite (carried forward from the UC6 validation
work) to drive the full lifecycle on-chain against the **real** council
watching the **real** chain:
- Provider joins the seeded council via the **real** dashboard join API
(starts its on-chain watcher); membership activates from the
`provider_added` chain event, pulling channel config (with status) from
the council.
- Lifecycle: council `disable_channel(USDC)` on-chain → council DB
tracks the chain → provider converges on the live event →
**withdraw-only** (deposit + send rejected, withdraw works, XLM
unaffected) → `enable_channel` → full service resumes.
- `lib/admin`: enable/disable helpers; compose: `COUNCIL_DATABASE_URL`.

Green via `./test.sh multi-asset` (~210s). Boot-sync + out-of-retention
are covered by deterministic provider-platform unit tests (per PM
decision).

Relates to ClickUp 86c903393.

## Coordinated PRs (review/merge together)
- soroban-core: Moonlight-Protocol/soroban-core#33
- council-platform: Moonlight-Protocol/council-platform#47
- provider-platform: Moonlight-Protocol/provider-platform#122
- local-dev: #119

## Release on merge
local-dev is **not versioned** — merging does not release. It places the
multi-asset **asset-lifecycle** suite on `main` so the cross-repo E2E
dispatched by the platform releases runs the new enable/disable →
withdraw-only assertions. The dispatched E2E checks out local-dev
`main`, so this must be on `main` **before the last platform release
dispatches**. (Node pinned via #120, merged.)
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